Introduction

Room database migrations require you to define explicit migration paths between schema versions. When the app updates and the database version increases, Room looks for a migration from the old version to the new version. If the migration path is missing, incomplete, or incorrect, the app crashes with an IllegalStateException about migration not being found.

Symptoms

  • java.lang.IllegalStateException: A migration from 2 to 4 was required but not found
  • App crashes on first launch after update
  • Room cannot verify the data integrity error
  • Works on fresh install but crashes for existing users
  • Expected ... but found ... column mismatch error

Example error: `` java.lang.IllegalStateException: A migration from 3 to 5 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration...) or allow destructive migrations via RoomDatabase.Builder.fallbackToDestructiveMigration()

Common Causes

  • Skipped a version number when adding migrations (e.g., 2 to 4 without 2 to 3)
  • Migration SQL does not match the actual schema change
  • Added a new @Entity field but did not write the migration
  • Changed a column type without proper SQL migration
  • autoMigrations not configured for incremental version bumps

Step-by-Step Fix

  1. 1.Define explicit migrations between consecutive versions:
  2. 2.```kotlin
  3. 3.val MIGRATION_1_2 = object : Migration(1, 2) {
  4. 4.override fun migrate(database: SupportSQLiteDatabase) {
  5. 5.database.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
  6. 6.}
  7. 7.}

val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS orders " + "(id TEXT NOT NULL, user_id TEXT NOT NULL, " + "amount REAL NOT NULL, PRIMARY KEY(id))") } }

// Room can chain 1->2->3 automatically val database = Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() ```

  1. 1.Use autoMigration for simple schema changes (Room 2.4+):
  2. 2.```kotlin
  3. 3.@Database(
  4. 4.entities = [User::class, Order::class],
  5. 5.version = 3,
  6. 6.autoMigrations = [
  7. 7.AutoMigration(from = 1, to = 2),
  8. 8.AutoMigration(from = 2, to = 3)
  9. 9.]
  10. 10.)
  11. 11.abstract class AppDatabase : RoomDatabase() {
  12. 12.abstract fun userDao(): UserDao
  13. 13.}
  14. 14.`
  15. 15.Handle complex autoMigration with a spec:
  16. 16.```kotlin
  17. 17.@Database(
  18. 18.entities = [User::class],
  19. 19.version = 3,
  20. 20.autoMigrations = [
  21. 21.AutoMigration(
  22. 22.from = 2,
  23. 23.to = 3,
  24. 24.spec = AppDatabase.AutoMigration2To3::class
  25. 25.)
  26. 26.]
  27. 27.)
  28. 28.abstract class AppDatabase : RoomDatabase() {
  29. 29.@RenameColumn(
  30. 30.tableName = "User",
  31. 31.fromColumnName = "user_name",
  32. 32.toColumnName = "name"
  33. 33.)
  34. 34.class AutoMigration2To3 : AutoMigrationSpec
  35. 35.}
  36. 36.`
  37. 37.Use destructive migration as last resort:
  38. 38.```kotlin
  39. 39.// WARNING: This deletes all user data
  40. 40.val database = Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
  41. 41..fallbackToDestructiveMigration()
  42. 42..build()

// Or only for specific versions val database = Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .fallbackToDestructiveMigrationFrom(1, 2) // Only for versions 1 and 2 .addMigrations(MIGRATION_3_4) .build() ```

  1. 1.Test migrations in unit tests:
  2. 2.```kotlin
  3. 3.@Test
  4. 4.fun migrate2To3() {
  5. 5.// Create database at version 2
  6. 6.val db2 = Room.databaseBuilder(
  7. 7.ApplicationProvider.getApplicationContext(),
  8. 8.AppDatabase::class.java,
  9. 9."migration-test"
  10. 10.)
  11. 11..addMigrations(MIGRATION_2_3)
  12. 12..build()

// Insert test data at version 2 db2.openHelper.writableDatabase.execSQL( "INSERT INTO users (id, name) VALUES ('1', 'Test')" )

// Migrate to version 3 val db3 = Room.databaseBuilder( ApplicationProvider.getApplicationContext(), AppDatabase::class.java, "migration-test" ) .addMigrations(MIGRATION_2_3) .build()

// Verify data survived migration val user = db3.userDao().getUserById("1") assertEquals("Test", user?.name)

db3.close() } ```

Prevention

  • Increment version by 1 for each schema change
  • Test every migration path from every previous version to the current version
  • Use autoMigrations for simple changes (add column, add table)
  • Keep explicit migrations for complex changes (rename, type change)
  • Add CI tests that verify migration from the oldest supported version
  • Use fallbackToDestructiveMigration only during early development