infra: DB-Migration-Infrastruktur einrichten (#49)
- fallbackToDestructiveMigration() entfernt (war inakzeptabel) - addMigrations(MIGRATION_1_2) in DatabaseModule eingetragen - Migrations.kt: Migration(1,2) mit Tabellen-Neubau fuer SQLite < 3.25 (kcal_per_100g -> kcal_per_kg, min_stock entfernt) - exportSchema = true + KSP-Argument room.schemaLocation = app/schemas/ - 2.json Schema-Snapshot eingecheckt (Basis fuer kuenftige Migrationen) - androidTest-Assets zeigen auf app/schemas/ (fuer MigrationTestHelper) - KrisenvorratDatabaseMigrationTest: 4 instrumentierte Tests - Datenerhalt nach Migration - Korrekte Spalten nach Migration - Indices nach Migration - Fresh-Install ohne Migration
This commit is contained in:
parent
018d8dc7da
commit
f4b5197b06
6 changed files with 499 additions and 2 deletions
|
|
@ -41,6 +41,13 @@ android {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
sourceSets {
|
||||||
|
getByName("androidTest").assets.srcDirs("$projectDir/schemas")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "4b6e7b8b9387bc7884e449d148a05cdd",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "categories",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "locations",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "items",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `category_id` INTEGER NOT NULL, `quantity` REAL NOT NULL, `unit` TEXT NOT NULL, `unit_price` REAL NOT NULL, `kcal_per_kg` INTEGER, `expiry_date` TEXT, `location_id` INTEGER NOT NULL, `notes` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`location_id`) REFERENCES `locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "quantity",
|
||||||
|
"columnName": "quantity",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unit",
|
||||||
|
"columnName": "unit",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unitPrice",
|
||||||
|
"columnName": "unit_price",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "kcalPerKg",
|
||||||
|
"columnName": "kcal_per_kg",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expiryDate",
|
||||||
|
"columnName": "expiry_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "locationId",
|
||||||
|
"columnName": "location_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notes",
|
||||||
|
"columnName": "notes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_items_category_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_items_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_items_location_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"location_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_items_location_id` ON `${TABLE_NAME}` (`location_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "categories",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "locations",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"location_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"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, '4b6e7b8b9387bc7884e449d148a05cdd')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
package de.krisenvorrat.app.data.db
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||||
|
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumentierte Tests für Room-Migrationen.
|
||||||
|
*
|
||||||
|
* Hinweis: Ab V2 exportiert Room das Schema als JSON nach `app/schemas/`.
|
||||||
|
* Künftige Migrationen (V2 → V3 usw.) können deshalb MigrationTestHelper mit
|
||||||
|
* `createDatabase(name, version)` nutzen, das die gespeicherte Schema-Datei
|
||||||
|
* als Ausgangsbasis verwendet.
|
||||||
|
*
|
||||||
|
* Für V1 → V2 existierte noch kein exportiertes Schema, daher wird die V1-DB
|
||||||
|
* hier manuell per SQLite-API aufgebaut.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
internal class KrisenvorratDatabaseMigrationTest {
|
||||||
|
|
||||||
|
private val dbName = "krisenvorrat-migration-test.db"
|
||||||
|
private val context get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context.deleteDatabase(dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
context.deleteDatabase(dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Hilfsmethoden
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legt eine V1-Datenbank auf dem Gerät an.
|
||||||
|
* Schema: items mit kcal_per_100g + min_stock (vor dem Umbenennen/Löschen).
|
||||||
|
*/
|
||||||
|
private fun createV1Database(includeTestData: Boolean = false) {
|
||||||
|
val dbFile = context.getDatabasePath(dbName).also { it.parentFile?.mkdirs() }
|
||||||
|
SQLiteDatabase.openOrCreateDatabase(dbFile, null).use { db ->
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE `categories` " +
|
||||||
|
"(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE `locations` " +
|
||||||
|
"(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE `items` (
|
||||||
|
`id` TEXT NOT NULL,
|
||||||
|
`name` TEXT NOT NULL,
|
||||||
|
`category_id` INTEGER NOT NULL,
|
||||||
|
`quantity` REAL NOT NULL,
|
||||||
|
`unit` TEXT NOT NULL,
|
||||||
|
`unit_price` REAL NOT NULL,
|
||||||
|
`kcal_per_100g` INTEGER,
|
||||||
|
`expiry_date` TEXT,
|
||||||
|
`location_id` INTEGER NOT NULL,
|
||||||
|
`min_stock` REAL NOT NULL,
|
||||||
|
`notes` TEXT NOT NULL,
|
||||||
|
`last_updated` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE `settings` " +
|
||||||
|
"(`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))"
|
||||||
|
)
|
||||||
|
if (includeTestData) {
|
||||||
|
db.execSQL("INSERT INTO `categories` (id, name) VALUES (1, 'Lebensmittel')")
|
||||||
|
db.execSQL("INSERT INTO `locations` (id, name) VALUES (1, 'Keller')")
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO `items`
|
||||||
|
(id, name, category_id, quantity, unit, unit_price,
|
||||||
|
kcal_per_100g, expiry_date, location_id, min_stock, notes, last_updated)
|
||||||
|
VALUES
|
||||||
|
('item-uuid-1', 'Apfel', 1, 5.0, 'kg', 2.50,
|
||||||
|
52, NULL, 1, 1.0, 'Testnotiz', 1700000000000)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
db.version = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openMigratedDb() = Room.databaseBuilder(
|
||||||
|
context,
|
||||||
|
KrisenvorratDatabase::class.java,
|
||||||
|
dbName
|
||||||
|
).addMigrations(Migrations.MIGRATION_1_2).build()
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrate1To2_itemDataIsPreserved() {
|
||||||
|
createV1Database(includeTestData = true)
|
||||||
|
|
||||||
|
val db = openMigratedDb()
|
||||||
|
try {
|
||||||
|
val items = runBlocking { db.itemDao().getAll().first() }
|
||||||
|
|
||||||
|
assertEquals("Ein Item muss nach der Migration erhalten sein", 1, items.size)
|
||||||
|
val item = items[0]
|
||||||
|
assertEquals("item-uuid-1", item.id)
|
||||||
|
assertEquals("Apfel", item.name)
|
||||||
|
assertEquals(52, item.kcalPerKg)
|
||||||
|
assertEquals("Testnotiz", item.notes)
|
||||||
|
assertEquals(5.0, item.quantity, 0.0)
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrate1To2_schemaIsCorrect() {
|
||||||
|
createV1Database()
|
||||||
|
|
||||||
|
val db = openMigratedDb()
|
||||||
|
try {
|
||||||
|
val columns = mutableListOf<String>()
|
||||||
|
db.openHelper.writableDatabase.query("PRAGMA table_info(items)").use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
columns.add(cursor.getString(cursor.getColumnIndexOrThrow("name")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse("min_stock darf nicht mehr existieren", columns.contains("min_stock"))
|
||||||
|
assertFalse("kcal_per_100g darf nicht mehr existieren", columns.contains("kcal_per_100g"))
|
||||||
|
assertTrue("kcal_per_kg muss existieren", columns.contains("kcal_per_kg"))
|
||||||
|
assertTrue("id muss existieren", columns.contains("id"))
|
||||||
|
assertTrue("name muss existieren", columns.contains("name"))
|
||||||
|
assertTrue("last_updated muss existieren", columns.contains("last_updated"))
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrate1To2_indicesExist() {
|
||||||
|
createV1Database()
|
||||||
|
|
||||||
|
val db = openMigratedDb()
|
||||||
|
try {
|
||||||
|
val indices = mutableListOf<String>()
|
||||||
|
db.openHelper.writableDatabase.query("PRAGMA index_list(items)").use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
indices.add(cursor.getString(cursor.getColumnIndexOrThrow("name")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
"index_items_category_id muss vorhanden sein",
|
||||||
|
indices.any { it.contains("category_id") }
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"index_items_location_id muss vorhanden sein",
|
||||||
|
indices.any { it.contains("location_id") }
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun freshInstall_worksWithoutMigration() {
|
||||||
|
// Fresh-Install: Room.onCreate() läuft direkt, keine Migration nötig
|
||||||
|
val db = Room.inMemoryDatabaseBuilder(context, KrisenvorratDatabase::class.java)
|
||||||
|
.addMigrations(Migrations.MIGRATION_1_2)
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
// Tabellen anlegen und Basis-Operationen prüfen
|
||||||
|
runBlocking {
|
||||||
|
db.categoryDao().insert(CategoryEntity(name = "Testkat"))
|
||||||
|
db.locationDao().insert(LocationEntity(name = "Testort"))
|
||||||
|
val cats = db.categoryDao().getAll().first()
|
||||||
|
assertEquals(1, cats.size)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||||
@Database(
|
@Database(
|
||||||
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class],
|
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class],
|
||||||
version = 2,
|
version = 2,
|
||||||
exportSchema = false
|
exportSchema = true
|
||||||
)
|
)
|
||||||
@TypeConverters(LocalDateConverter::class)
|
@TypeConverters(LocalDateConverter::class)
|
||||||
internal abstract class KrisenvorratDatabase : RoomDatabase() {
|
internal abstract class KrisenvorratDatabase : RoomDatabase() {
|
||||||
|
|
|
||||||
71
app/src/main/java/de/krisenvorrat/app/data/db/Migrations.kt
Normal file
71
app/src/main/java/de/krisenvorrat/app/data/db/Migrations.kt
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package de.krisenvorrat.app.data.db
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enthält alle Room-Migrationen der Krisenvorrat-Datenbank.
|
||||||
|
*
|
||||||
|
* Checkliste für jede neue Schema-Änderung:
|
||||||
|
* 1. Migration(X, Y)-Objekt hier ergänzen
|
||||||
|
* 2. DB-Version in [KrisenvorratDatabase] hochzählen
|
||||||
|
* 3. Migration in [de.krisenvorrat.app.di.DatabaseModule].addMigrations() eintragen
|
||||||
|
* 4. Migrationspfad in [de.krisenvorrat.app.data.db.KrisenvorratDatabaseMigrationTest] testen
|
||||||
|
* 5. Bei neuen Pflichtfeldern ohne sinnvollen Default: interaktiven MigrationScreen ergänzen
|
||||||
|
*/
|
||||||
|
internal object Migrations {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V1 → V2:
|
||||||
|
* - `kcal_per_100g` umbenannt in `kcal_per_kg`
|
||||||
|
* - `min_stock`-Spalte entfernt
|
||||||
|
*
|
||||||
|
* SQLite unterstützt erst ab 3.25 (API 29) ALTER TABLE … RENAME COLUMN.
|
||||||
|
* Da minSdk = 26, wird die Tabelle neu erstellt und die Daten kopiert.
|
||||||
|
*/
|
||||||
|
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE `items_new` (
|
||||||
|
`id` TEXT NOT NULL,
|
||||||
|
`name` TEXT NOT NULL,
|
||||||
|
`category_id` INTEGER NOT NULL,
|
||||||
|
`quantity` REAL NOT NULL,
|
||||||
|
`unit` TEXT NOT NULL,
|
||||||
|
`unit_price` REAL NOT NULL,
|
||||||
|
`kcal_per_kg` INTEGER,
|
||||||
|
`expiry_date` TEXT,
|
||||||
|
`location_id` INTEGER NOT NULL,
|
||||||
|
`notes` TEXT NOT NULL,
|
||||||
|
`last_updated` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`id`),
|
||||||
|
FOREIGN KEY(`category_id`) REFERENCES `categories`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(`location_id`) REFERENCES `locations`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO `items_new`
|
||||||
|
(id, name, category_id, quantity, unit, unit_price,
|
||||||
|
kcal_per_kg, expiry_date, location_id, notes, last_updated)
|
||||||
|
SELECT
|
||||||
|
id, name, category_id, quantity, unit, unit_price,
|
||||||
|
kcal_per_100g, expiry_date, location_id, notes, last_updated
|
||||||
|
FROM `items`
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
db.execSQL("DROP TABLE `items`")
|
||||||
|
db.execSQL("ALTER TABLE `items_new` RENAME TO `items`")
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE INDEX IF NOT EXISTS `index_items_category_id` ON `items` (`category_id`)"
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE INDEX IF NOT EXISTS `index_items_location_id` ON `items` (`location_id`)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import de.krisenvorrat.app.data.db.KrisenvorratDatabase
|
import de.krisenvorrat.app.data.db.KrisenvorratDatabase
|
||||||
|
import de.krisenvorrat.app.data.db.Migrations
|
||||||
import de.krisenvorrat.app.data.db.dao.CategoryDao
|
import de.krisenvorrat.app.data.db.dao.CategoryDao
|
||||||
import de.krisenvorrat.app.data.db.dao.ItemDao
|
import de.krisenvorrat.app.data.db.dao.ItemDao
|
||||||
import de.krisenvorrat.app.data.db.dao.LocationDao
|
import de.krisenvorrat.app.data.db.dao.LocationDao
|
||||||
|
|
@ -27,7 +28,7 @@ internal object DatabaseModule {
|
||||||
fun provideDatabase(@ApplicationContext context: Context): KrisenvorratDatabase =
|
fun provideDatabase(@ApplicationContext context: Context): KrisenvorratDatabase =
|
||||||
Room.databaseBuilder(context, KrisenvorratDatabase::class.java, "krisenvorrat.db")
|
Room.databaseBuilder(context, KrisenvorratDatabase::class.java, "krisenvorrat.db")
|
||||||
.addCallback(DefaultDataCallback)
|
.addCallback(DefaultDataCallback)
|
||||||
.fallbackToDestructiveMigration()
|
.addMigrations(Migrations.MIGRATION_1_2)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private object DefaultDataCallback : RoomDatabase.Callback() {
|
private object DefaultDataCallback : RoomDatabase.Callback() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue