diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e409b1..09a95cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -240,6 +240,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Room now upgrades with real schema migrations instead of dropping the + database. `StackDatabase` exports its schema (`app/schemas/`) and the + builder runs `MIGRATION_4_5` (adds `game_round`) plus + `fallbackToDestructiveMigrationOnDowngrade`, so the offline cache (news, + wallet, notifications) survives app updates instead of being wiped on a + version bump. Covered by the instrumented `StackDatabaseMigrationTest` + (verifies the news cache survives 4 -> 5). - `ProvablyFairCore.cpp` no longer depends on OpenSSL. The HMAC-SHA256 helper delegates to the self-contained implementation in `Sha256.h`. - JNI export symbols renamed from `com.casino.tpo.engine` to diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e6e8355..bcdf2d1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -79,6 +79,19 @@ android { compose = true buildConfig = true } + + // Expose the exported Room schemas to the migration tests so + // MigrationTestHelper can build the old database versions. + sourceSets { + getByName("androidTest").assets.srcDir(files("$projectDir/schemas")) + } +} + +// Room exports a JSON schema per version into this directory (see +// exportSchema = true on StackDatabase); the migrations and their tests +// are validated against it. +ksp { + arg("room.schemaLocation", "$projectDir/schemas") } ktlint { @@ -162,6 +175,7 @@ dependencies { testImplementation(libs.mockk) testImplementation(libs.turbine) testImplementation(libs.androidx.room.testing) + androidTestImplementation(libs.androidx.room.testing) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/schemas/com.plainstudio.stackcasino.data.local.StackDatabase/4.json b/app/schemas/com.plainstudio.stackcasino.data.local.StackDatabase/4.json new file mode 100644 index 0000000..898cc01 --- /dev/null +++ b/app/schemas/com.plainstudio.stackcasino.data.local.StackDatabase/4.json @@ -0,0 +1,273 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "00000000000000000000000000000004", + "entities": [ + { + "tableName": "app_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pushEnabled` INTEGER NOT NULL, `depositsEnabled` INTEGER NOT NULL, `withdrawalsEnabled` INTEGER NOT NULL, `securityEnabled` INTEGER NOT NULL, `hideBalanceOnLaunch` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushEnabled", + "columnName": "pushEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "depositsEnabled", + "columnName": "depositsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "withdrawalsEnabled", + "columnName": "withdrawalsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "securityEnabled", + "columnName": "securityEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideBalanceOnLaunch", + "columnName": "hideBalanceOnLaunch", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category` TEXT NOT NULL, `title` TEXT NOT NULL, `body` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `isRead` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "isRead", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "news_article", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `source` TEXT NOT NULL, `description` TEXT, `url` TEXT NOT NULL, `imageUrl` TEXT, `publishedAt` TEXT NOT NULL, `cachedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "publishedAt", + "columnName": "publishedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cachedAt", + "columnName": "cachedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "wallet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `availableBalance` REAL NOT NULL, `lockedBalance` REAL NOT NULL, `polygonAddress` TEXT NOT NULL, `currencyCode` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "availableBalance", + "columnName": "availableBalance", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lockedBalance", + "columnName": "lockedBalance", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "polygonAddress", + "columnName": "polygonAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currencyCode", + "columnName": "currencyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "wallet_transaction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `status` TEXT NOT NULL, `amount` REAL NOT NULL, `currency` TEXT NOT NULL, `externalReference` TEXT, `destinationAddress` TEXT, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "externalReference", + "columnName": "externalReference", + "affinity": "TEXT" + }, + { + "fieldPath": "destinationAddress", + "columnName": "destinationAddress", + "affinity": "TEXT" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '00000000000000000000000000000004')" + ] + } +} diff --git a/app/schemas/com.plainstudio.stackcasino.data.local.StackDatabase/5.json b/app/schemas/com.plainstudio.stackcasino.data.local.StackDatabase/5.json new file mode 100644 index 0000000..d3384f4 --- /dev/null +++ b/app/schemas/com.plainstudio.stackcasino.data.local.StackDatabase/5.json @@ -0,0 +1,369 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "5c15b0f430f02bafbc72988fb726ebe0", + "entities": [ + { + "tableName": "app_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pushEnabled` INTEGER NOT NULL, `depositsEnabled` INTEGER NOT NULL, `withdrawalsEnabled` INTEGER NOT NULL, `securityEnabled` INTEGER NOT NULL, `hideBalanceOnLaunch` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushEnabled", + "columnName": "pushEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "depositsEnabled", + "columnName": "depositsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "withdrawalsEnabled", + "columnName": "withdrawalsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "securityEnabled", + "columnName": "securityEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideBalanceOnLaunch", + "columnName": "hideBalanceOnLaunch", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category` TEXT NOT NULL, `title` TEXT NOT NULL, `body` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `isRead` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "isRead", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "news_article", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `source` TEXT NOT NULL, `description` TEXT, `url` TEXT NOT NULL, `imageUrl` TEXT, `publishedAt` TEXT NOT NULL, `cachedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "publishedAt", + "columnName": "publishedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cachedAt", + "columnName": "cachedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "wallet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `availableBalance` REAL NOT NULL, `lockedBalance` REAL NOT NULL, `polygonAddress` TEXT NOT NULL, `currencyCode` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "availableBalance", + "columnName": "availableBalance", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lockedBalance", + "columnName": "lockedBalance", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "polygonAddress", + "columnName": "polygonAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currencyCode", + "columnName": "currencyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "wallet_transaction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `status` TEXT NOT NULL, `amount` REAL NOT NULL, `currency` TEXT NOT NULL, `externalReference` TEXT, `destinationAddress` TEXT, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "externalReference", + "columnName": "externalReference", + "affinity": "TEXT" + }, + { + "fieldPath": "destinationAddress", + "columnName": "destinationAddress", + "affinity": "TEXT" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "game_round", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `game` TEXT NOT NULL, `pick` TEXT NOT NULL, `stake` REAL NOT NULL, `payout` REAL NOT NULL, `won` INTEGER NOT NULL, `multiplier` REAL NOT NULL, `currency` TEXT NOT NULL, `serverSeed` TEXT NOT NULL, `clientSeed` TEXT NOT NULL, `nonce` INTEGER NOT NULL, `hash` TEXT NOT NULL, `resultRaw` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "game", + "columnName": "game", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pick", + "columnName": "pick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stake", + "columnName": "stake", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "payout", + "columnName": "payout", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "won", + "columnName": "won", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "multiplier", + "columnName": "multiplier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverSeed", + "columnName": "serverSeed", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientSeed", + "columnName": "clientSeed", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nonce", + "columnName": "nonce", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resultRaw", + "columnName": "resultRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5c15b0f430f02bafbc72988fb726ebe0')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/plainstudio/stackcasino/data/local/StackDatabaseMigrationTest.kt b/app/src/androidTest/java/com/plainstudio/stackcasino/data/local/StackDatabaseMigrationTest.kt new file mode 100644 index 0000000..45f8951 --- /dev/null +++ b/app/src/androidTest/java/com/plainstudio/stackcasino/data/local/StackDatabaseMigrationTest.kt @@ -0,0 +1,52 @@ +package com.plainstudio.stackcasino.data.local + +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class StackDatabaseMigrationTest { + @get:Rule + val helper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + StackDatabase::class.java, + ) + + @Test + fun migrate4To5_keepsTheNewsCacheAndAddsGameRound() { + // A v4 database with a cached news article. + helper.createDatabase(TEST_DB, 4).apply { + execSQL( + "INSERT INTO news_article " + + "(id, title, source, description, url, imageUrl, publishedAt, cachedAt) " + + "VALUES ('u1', 'Cached headline', 'CryptoNews', null, 'https://x', null, '2026-06-24', 1)", + ) + close() + } + + // Run the real v4 -> v5 migration and validate against the exported v5 schema. + val db = helper.runMigrationsAndValidate(TEST_DB, 5, true, MIGRATION_4_5) + + // The cached article survived the upgrade instead of being dropped. + db.query("SELECT title FROM news_article WHERE id = 'u1'").use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals("Cached headline", cursor.getString(0)) + } + // The new game_round table was created by the migration. + db.query("SELECT COUNT(*) FROM game_round").use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals(0, cursor.getInt(0)) + } + db.close() + } + + private companion object { + const val TEST_DB = "migration-test.db" + } +} diff --git a/app/src/main/java/com/plainstudio/stackcasino/data/local/StackDatabase.kt b/app/src/main/java/com/plainstudio/stackcasino/data/local/StackDatabase.kt index 33e187d..a7f1739 100644 --- a/app/src/main/java/com/plainstudio/stackcasino/data/local/StackDatabase.kt +++ b/app/src/main/java/com/plainstudio/stackcasino/data/local/StackDatabase.kt @@ -19,7 +19,7 @@ import androidx.room.RoomDatabase GameRoundEntity::class, ], version = 5, - exportSchema = false, + exportSchema = true, ) abstract class StackDatabase : RoomDatabase() { abstract fun appSettingsDao(): AppSettingsDao diff --git a/app/src/main/java/com/plainstudio/stackcasino/data/local/StackDatabaseMigrations.kt b/app/src/main/java/com/plainstudio/stackcasino/data/local/StackDatabaseMigrations.kt new file mode 100644 index 0000000..18ecf79 --- /dev/null +++ b/app/src/main/java/com/plainstudio/stackcasino/data/local/StackDatabaseMigrations.kt @@ -0,0 +1,28 @@ +package com.plainstudio.stackcasino.data.local + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +/* + * Schema migrations for StackDatabase. These preserve the offline cache + * (news, wallet, notifications) across schema changes instead of dropping + * the whole database, so the downloaded data survives app updates. Each + * version bump adds one Migration here and exports a schema JSON under + * `app/schemas/` that the migration tests validate against. + */ + +/** v4 -> v5: adds the `game_round` ledger introduced with the games card. */ +val MIGRATION_4_5 = + object : Migration(4, 5) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS `game_round` (" + + "`id` TEXT NOT NULL, `game` TEXT NOT NULL, `pick` TEXT NOT NULL, " + + "`stake` REAL NOT NULL, `payout` REAL NOT NULL, `won` INTEGER NOT NULL, " + + "`multiplier` REAL NOT NULL, `currency` TEXT NOT NULL, " + + "`serverSeed` TEXT NOT NULL, `clientSeed` TEXT NOT NULL, `nonce` INTEGER NOT NULL, " + + "`hash` TEXT NOT NULL, `resultRaw` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, " + + "PRIMARY KEY(`id`))", + ) + } + } diff --git a/app/src/main/java/com/plainstudio/stackcasino/di/DatabaseModule.kt b/app/src/main/java/com/plainstudio/stackcasino/di/DatabaseModule.kt index 2a729e2..5ae4498 100644 --- a/app/src/main/java/com/plainstudio/stackcasino/di/DatabaseModule.kt +++ b/app/src/main/java/com/plainstudio/stackcasino/di/DatabaseModule.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.room.Room import com.plainstudio.stackcasino.data.local.AppSettingsDao import com.plainstudio.stackcasino.data.local.GameRoundDao +import com.plainstudio.stackcasino.data.local.MIGRATION_4_5 import com.plainstudio.stackcasino.data.local.NewsArticleDao import com.plainstudio.stackcasino.data.local.NotificationDao import com.plainstudio.stackcasino.data.local.StackDatabase @@ -18,10 +19,10 @@ import javax.inject.Singleton /** * Provides the Room [StackDatabase] and its DAOs. * - * Destructive migration is acceptable while the schema is still - * pre-release: the local database is a cache/preferences store that is - * rebuilt from Firestore + the seed callback, never the system of - * record, so dropping it on a version bump loses nothing durable. + * Schema upgrades run real [STACK_DB_MIGRATIONS] so the offline cache + * (news, wallet, notifications) survives app updates instead of being + * dropped. Only a downgrade (a developer installing an older build) falls + * back to recreating the database, since there is no forward path for it. */ @Module @InstallIn(SingletonComponent::class) @@ -33,7 +34,8 @@ object DatabaseModule { ): StackDatabase = Room .databaseBuilder(context, StackDatabase::class.java, StackDatabase.NAME) - .fallbackToDestructiveMigration() + .addMigrations(MIGRATION_4_5) + .fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true) .build() @Provides