Skip to content
advanced Phase 7 · Data & Persistence

Data Migration

Migrate data between storage systems and handle Room database version upgrades.

45m
2 problems
Topic Progress 0%

Room Migration Strategies

Migration Fundamentals

A migration runs a specific SQL script to transform the database schema from one version to the next. Room chains migrations — if you go from version 1 to 4, Room executes migration 1→2, 2→3, 3→4 in sequence.

Column-Level Operations

// Add a column
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE users ADD COLUMN phone TEXT")
    }
}

// Rename a column (SQLite doesn't support RENAME COLUMN before 3.25)
val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("CREATE TABLE users_new (id INTEGER PRIMARY KEY, fullName TEXT, email TEXT)")
        db.execSQL("INSERT INTO users_new (id, fullName, email) SELECT id, name, email FROM users")
        db.execSQL("DROP TABLE users")
        db.execSQL("ALTER TABLE users_new RENAME TO users")
    }
}

Renaming a column requires creating a new table, copying data, dropping the old table, and renaming. This is the standard pattern for SQLite.

Data Transformations

val MIGRATION_3_4 = object : Migration(3, 4) {
    override fun migrate(db: SupportSQLiteDatabase) {
        // Split "John Doe" into firstName + lastName
        db.execSQL("ALTER TABLE users ADD COLUMN firstName TEXT")
        db.execSQL("ALTER TABLE users ADD COLUMN lastName TEXT")
        db.execSQL("""
            UPDATE users SET
                firstName = substr(name, 1, instr(name, ' ') - 1),
                lastName = substr(name, instr(name, ' ') + 1)
            WHERE name LIKE '% %'
        """)
    }
}

Migration with Room

@Database(version = 5)
abstract class AppDatabase : RoomDatabase() {
    abstract fun noteDao(): NoteDao
}

val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
    .build()

Fallback Strategy

When all else fails, destroy and recreate. This loses data but prevents crashes:

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .fallbackToDestructiveMigration()
    .build()

Only use this during development. In production, try to recover data first.

Cross-System Migration

When Cross-System Migration Is Needed

You may need to move data between storage systems when:

  • Upgrading from SharedPreferences to DataStore
  • Migrating from SQLite to Room
  • Moving from local database to a cloud-synced architecture
  • Changing the entire persistence layer during a rewrite

SharedPreferences to DataStore

suspend fun migrateToDataStore(context: Context) {
    val oldPrefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    val dataStore = context.settingsDataStore
    
    dataStore.edit { prefs ->
        oldPrefs.all.forEach { (key, value) ->
            when (value) {
                is Boolean -> prefs[booleanPreferencesKey(key)] = value
                is Int -> prefs[intPreferencesKey(key)] = value
                is String -> prefs[stringPreferencesKey(key)] = value
                is Float -> prefs[floatPreferencesKey(key)] = value
                is Long -> prefs[longPreferencesKey(key)] = value
            }
        }
    }
    
    // Mark migration complete, then delete old file
    oldPrefs.edit().clear().apply()
    File(oldPrefs.fileParent, oldPrefs.file.name).delete()
}

SQLite to Room

Room can import an existing SQLite database:

// Copy the legacy database to Room's location
fun migrateToRoom(context: Context, legacyDbPath: String) {
    val roomDbPath = context.getDatabasePath("room_app.db")
    roomDbPath.parentFile?.mkdirs()
    
    File(legacyDbPath).inputStream().use { input ->
        roomDbPath.outputStream().use { output ->
            input.copyTo(output)
        }
    }
}

// Then open with Room and run any needed migrations
val db = Room.databaseBuilder(context, AppDatabase::class.java, "room_app.db")
    .createFromAsset("prepopulated.db")
    .addMigrations(MIGRATION_1_2)
    .build()

Progressive Rollout

Never migrate all users at once. Use feature flags:

class MigrationManager(private val context: Context) {
    suspend fun performMigration() {
        val dataStore = context.settingsDataStore.data.first()
        val migrated = dataStore[migrationCompleteKey] ?: false
        
        if (!migrated) {
            try {
                migrateToDataStore(context)
                context.settingsDataStore.edit {
                    it[migrationCompleteKey] = true
                }
            } catch (e: Exception) {
                Log.e("Migration", "Migration failed", e)
                // Don't set flag — will retry next launch
            }
        }
    }
}

Verifying Migration Success

suspend fun verifyMigration(context: Context): Boolean {
    val oldPrefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    val newStore = context.settingsDataStore.data.first()
    
    return oldPrefs.all.all { (key, value) ->
        when (value) {
            is String -> newStore[stringPreferencesKey(key)] == value
            is Boolean -> newStore[booleanPreferencesKey(key)] == value
            is Int -> newStore[intPreferencesKey(key)] == value
            else -> true
        }
    }
}

Quiz

1. How does Room chain multiple migrations?

Question 1 options

2. Why is renaming a column in SQLite a multi-step operation?

Question 2 options

3. What should you do immediately after migrating SharedPreferences to DataStore?

Question 3 options

4. When is fallbackToDestructiveMigration appropriate?

Question 4 options

Flashcards

Question

What is the standard SQLite pattern for renaming a column?

Answer

Create a new table with the desired schema, copy data from the old table, drop the old table, rename the new table to the old name.

Question

What does fallbackToDestructiveMigration() do?

Answer

When no matching migration is found, it drops all tables and recreates the database from scratch. Data is lost.

Question

Why use feature flags for data migration?

Answer

To enable progressive rollout — you can test on a subset of users, detect issues early, and stop the migration without affecting everyone.

Question

When migrating from SQLite to Room, what does createFromAsset() do?

Answer

Pre-populates the Room database by copying a pre-built SQLite file from the assets folder on first launch.

Revision Notes

Key Takeaways

  • 1. Room chains migrations sequentially — each must transform the schema incrementally
  • 2. Column renames in SQLite require creating a new table and copying data
  • 3. Always verify migrated data before deleting the original storage
  • 4. Use feature flags for progressive rollout to catch issues early

Interview Tips

  • Walk through a column rename migration step by step — interviewers expect the create-copy-drop pattern
  • Explain why you verify migration success before cleanup
  • Discuss the tradeoff between fallbackToDestructiveMigration and recovery strategies
  • Be ready to describe a complete migration plan for a major app version upgrade

Cheat Sheet

Data Migration Cheat Sheet

Room Migrations:

  • Migration(fromVersion, toVersion) with migrate(db) override
  • Chain sequential: 1→2→3→4
  • Column add: ALTER TABLE ADD COLUMN
  • Column rename: create new table, copy, drop, rename

Cross-System Migrations:

  • SharedPrefs → DataStore: iterate all.forEach, map types
  • SQLite → Room: copy file or use createFromAsset()

Safety Rules:

  • Always verify migration success before deleting old data
  • Use feature flags for progressive rollout
  • fallbackToDestructiveMigration is last resort only
  • Wrap migrations in try-catch, retry on next launch

Testing:

  • MigrationTestHelper validates SQL against real schemas
  • Test both success and failure paths