Room migrations and migration testing
Migration is production code running on irreplaceable user data before the app can open.
Additive automatic change
Room auto migrations handle changes it can infer; ambiguous rename/delete needs a specification or manual migration. Never model a rename as drop + add if data must survive.
Expand, migrate, contract
For a complex transformation:
- add new schema while old remains
- backfill deterministically
- make code read/write compatible during transition if needed
- validate counts/invariants
- remove old schema in later version
SQLite table recreation pattern:
CREATE TABLE orders_new (... desired schema ...);
INSERT INTO orders_new (...) SELECT ... FROM orders;
DROP TABLE orders;
ALTER TABLE orders_new RENAME TO orders;
CREATE INDEX ...;
Recreate foreign keys, indexes, triggers, defaults, nullability, and constraints. Copy in one transaction and handle malformed legacy values explicitly.
Destructive migration risk
fallbackToDestructiveMigration() deletes local database when no path exists. Acceptable only for genuinely disposable cache that can be fully restored and does not contain offline drafts, pending sync, user choices, or costly downloads.
Document which tables are authoritative/pending before enabling it. “Server-backed app” does not mean all local data is disposable.
Test every supported path
Export Room schema JSON into version control. Migration tests should:
- create an old-version database with representative/edge rows
- run full migration path to current
- validate schema
- assert values, constraints, indexes, relationships, and queries
- test empty, large, null/legacy malformed, Unicode, max/min values
helper.createDatabase(TEST_DB, 3).apply {
execSQL("INSERT INTO ...")
close()
}
helper.runMigrationsAndValidate(TEST_DB, CURRENT_VERSION, true, *ALL_MIGRATIONS)
Test direct supported paths and chained paths (1→current, 2→current), not only latest one-step migration.
Operational concerns
Large backfills can extend startup/ANR risk and require free disk for table copies. Measure upgrade time and storage on low-end devices/large realistic databases. Consider lazy/background data backfill only if schema and feature behavior safely support partial state.
Do not ship an entity schema change without exported schema, migration path policy, migration tests, rollback/data-loss analysis, and measured worst-case upgrade.
Sources
Exercises
- Safely split
full_nameinto two nullable fields. - Audit whether destructive migration is safe for an offline notes app.
- Build a migration test data matrix.