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?
2. Why is renaming a column in SQLite a multi-step operation?
3. What should you do immediately after migrating SharedPreferences to DataStore?
4. When is fallbackToDestructiveMigration appropriate?
Flashcards
Question
What is the standard SQLite pattern for renaming a column?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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)withmigrate(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