Introduction

Kotlin Room requires explicit migrations when the database schema changes between app versions. When a user upgrades the app and Room finds that the existing database schema does not match the expected schema, and no migration path exists, it throws an IllegalStateException and crashes. Room provides both manual migrations (with custom SQL) and automatic migrations (generated at compile time), but both require correct version numbering, schema export configuration, and proper migration registration.

Symptoms

  • IllegalStateException: Room cannot verify the data integrity on app update
  • App works on fresh install but crashes after update
  • Room cannot find the migration from X to Y
  • SQLiteException: no such column after schema change
  • Auto migration generated at compile time but not applied at runtime

Error output: ``` java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.

java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. ```

Common Causes

  • Database version not incremented after schema change
  • Migration from version X to Y not defined
  • Schema JSON file not exported (auto migration cannot find schema diff)
  • Auto migration annotation references wrong version numbers
  • Manual migration SQL does not match actual schema changes

Step-by-Step Fix

  1. 1.Define manual migration between versions:
  2. 2.```kotlin
  3. 3.// Version 1
  4. 4.@Entity(tableName = "users")
  5. 5.data class UserV1(
  6. 6.@PrimaryKey val id: Int,
  7. 7.val name: String
  8. 8.)

// Version 2 - added email column @Entity(tableName = "users") data class UserV2( @PrimaryKey val id: Int, val name: String, val email: String // NEW COLUMN )

// Database with migration @Database( entities = [UserV2::class], version = 2, // Incremented from 1 exportSchema = true ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }

val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''") } }

// Build database with migration val db = Room.databaseBuilder( context, AppDatabase::class.java, "app-database" ) .addMigrations(MIGRATION_1_2) .build() ```

  1. 1.Use auto migration for simple schema changes:
  2. 2.```kotlin
  3. 3.// Enable schema export in build.gradle
  4. 4.android {
  5. 5.defaultConfig {
  6. 6.javaCompileOptions {
  7. 7.annotationProcessorOptions {
  8. 8.arguments += ["room.schemaLocation": "$projectDir/schemas"]
  9. 9.}
  10. 10.}
  11. 11.}
  12. 12.}

// Room generates migration automatically @Database( entities = [User::class, Order::class], version = 3, exportSchema = true ) @AutoMigration(from = 2, to = 3) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun orderDao(): OrderDao() }

// Multiple auto migrations @Database( entities = [User::class], version = 4, exportSchema = true ) @AutoMigration(from = 1, to = 2) @AutoMigration(from = 2, to = 3) @AutoMigration(from = 3, to = 4) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }

// Auto migration with custom spec for complex changes @Database( entities = [User::class], version = 5, exportSchema = true ) @AutoMigration( from = 4, to = 5, spec = Migration4to5::class ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }

class Migration4to5 : AutoMigrationSpec { override fun onPostMigrate(db: SupportSQLiteDatabase) { // Custom SQL after auto migration completes db.execSQL("UPDATE users SET status = 'active' WHERE status IS NULL") } } ```

  1. 1.Use fallbackToDestructiveMigration as last resort:
  2. 2.```kotlin
  3. 3.// WARNING: This DELETES all user data on migration failure
  4. 4.// Only use for development or when data loss is acceptable
  5. 5.val db = Room.databaseBuilder(
  6. 6.context,
  7. 7.AppDatabase::class.java,
  8. 8."app-database"
  9. 9.)
  10. 10..fallbackToDestructiveMigration() // Destroys data if migration fails
  11. 11..addMigrations(MIGRATION_1_2)
  12. 12..build()

// Better: fallback only for specific versions val db = Room.databaseBuilder( context, AppDatabase::class.java, "app-database" ) .fallbackToDestructiveMigrationFrom(3) // Only destructive from version 3 .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() ```

  1. 1.Test migrations to verify correctness:
  2. 2.```kotlin
  3. 3.// Migration test
  4. 4.class MigrationTest {
  5. 5.private lateinit var db: SupportSQLiteDatabase

@get:Rule val migrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java )

@Test fun migrate1To2() { // Create database at version 1 db = migrationTestHelper.createDatabase("app-database", 1).apply { execSQL("INSERT INTO users (id, name) VALUES (1, 'John')") close() }

// Migrate to version 2 db = migrationTestHelper.runMigrationsAndValidate( "app-database", 2, true, MIGRATION_1_2 )

// Verify data survived migration val cursor = db.query("SELECT name, email FROM users WHERE id = 1") cursor.moveToFirst() assertEquals("John", cursor.getString(0)) assertEquals("", cursor.getString(1)) // Default value } } ```

Prevention

  • Always increment database version when changing entities
  • Enable exportSchema = true and configure schema location in build.gradle
  • Prefer auto migrations for simple changes (add column, rename table)
  • Write migration tests for each version transition
  • Use fallbackToDestructiveMigration only in development builds
  • Commit schema JSON files to version control for auto migration diff
  • Test app updates on physical devices with existing databases