Migrations

Installed apps keep their database when you ship an update, so every schema change needs a migration. BookNest version 2 adds a fetchedAt column that records when each book was cached (Offline-First uses it to decide what is stale). The entity gains the property with a matching default, the version goes from 1 to 2, and a Migration object alters the existing table:

data/local/Migrations.kt: from schema version 1 to 2Kotlin
val MIGRATION_1_2 = object : Migration(1, 2) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.execSQL("ALTER TABLE books ADD COLUMN fetchedAt INTEGER NOT NULL DEFAULT 0")
    Log.d("BookNestDB", "migrated books from version 1 to 2")
  }
}

The entity's @ColumnInfo(defaultValue = "0") must match the SQL: after migrating, Room 234 compares the real schema with the expected one and throws if they differ. Installing version 2 over version 1, which held two notes:

Output of 146
D/BookNestDB: migrated books from version 1 to 2
D/BookNestDB: cache hit, last sync: SyncState(source=none, books=0, at=0)

The sync file of Proto DataStore did not exist yet, so DataStore 234 returned its default SyncState.

Pulling the file with adb exec-out run-as com.example.booknest cat databases/booknest.db (debuggable builds only) and asking sqlite3 for PRAGMA user_version returned 2, and PRAGMA table_info(books) listed fetchedAt as INTEGER, not null, default 0. user_version is the number Room compares with @Database(version = ...), and the notes survived (see the figure in Storing BookNest's Settings). Simple changes need no SQL: with exported schemas, @Database(autoMigrations = [AutoMigration(from = 1, to = 2)]) generates the migration. Test migrations with MigrationTestHelper from room3-testing, and avoid fallbackToDestructiveMigration() outside prototypes: it silently deletes every user's data when a migration is missing.