bollwerk/.github/skills/android-db-migration/SKILL.md
Jens Reinemann 045a4b7674 feat: Migration-Safety – Room v7, AutoMigration, Flyway, kein fallbackToDestructiveMigration (#99)
- fallbackToDestructiveMigration() aus DatabaseModule entfernt
- BollwerkDatabase auf Version 7 gebumpt
- AutoMigration(from=5, to=6) und (from=6, to=7) definiert
- MigrationTestHelper-Test migrate6To7_preservesData implementiert
- 7.json Schema-Export generiert
- Server: Flyway 9.22.3 integriert (baselineOnMigrate=true)
- V1__initial_schema.sql + V2__cleanup_user_id.sql angelegt
- Skill android-db-migration erstellt
- versionCode 5 / versionName 1.4
2026-05-17 21:17:24 +02:00

219 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: android-db-migration
description: >
Room-Datenbankmigration in der Bollwerk-App: Schema-Version erhöhen, @AutoMigration hinzufügen,
manuelle Migration schreiben, MigrationTestHelper-Tests erstellen, fallbackToDestructiveMigration
entfernen/vermeiden. Nutze diesen Skill immer wenn es um Room-Schema-Änderungen, DB-Versionen,
Migrationen, Spalten umbenennen/löschen, neue Tabellen oder Migrationstests geht.
Trigger-Phrasen: "migration", "DB-Version", "Schema", "Room", "AutoMigration", "fallback",
"Datenverlust", "Spalte hinzufügen/umbenennen", "BollwerkDatabase", "DatabaseModule".
---
# Skill: Android DB-Migration (Room)
Vollständige Anleitung zur sicheren Room-Migration in der Bollwerk-App vom Schema-Bump über @AutoMigration bis zum MigrationTestHelper-Test.
---
## Projektkontext
| Datei | Zweck |
| --------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `app/src/main/java/de/bollwerk/app/data/db/BollwerkDatabase.kt` | `@Database`-Annotation, Versionsnummer, `autoMigrations`-Liste |
| `app/src/main/java/de/bollwerk/app/data/db/Migrations.kt` | Manuelle `Migration(X, Y)`-Objekte (nur bei AutoMigration-Grenzen nötig) |
| `app/src/main/java/de/bollwerk/app/di/DatabaseModule.kt` | Room Builder enthält **kein** `fallbackToDestructiveMigration()` |
| `app/schemas/de.bollwerk.app.data.db.BollwerkDatabase/` | Exportierte Schema-JSON-Dateien (auto-generiert via KSP) |
| `app/src/androidTest/.../BollwerkDatabaseMigrationTest.kt` | `MigrationTestHelper`-Tests |
**KSP-Konfiguration** (`app/build.gradle.kts`):
```kotlin
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
```
```kotlin
sourceSets { getByName("androidTest").assets.srcDirs("$projectDir/schemas") }
```
→ Schema-JSONs werden bei jedem Build automatisch aktualisiert und sind als Assets für Migrationstests verfügbar.
---
## Goldene Regeln
1. **Niemals** `fallbackToDestructiveMigration()` verwenden die App crasht bewusst, wenn kein Pfad definiert ist (Datenverlust bleibt sichtbar).
2. **Jede** Versionserhöhung braucht einen `@AutoMigration`- oder manuellen `Migration`-Eintrag.
3. **Immer** nach dem Schema-Bump bauen, damit die neue `N.json` generiert wird, bevor Tests geschrieben werden.
4. Für Spalten-Umbenennungen **immer** `@RenameColumn`-Spec angeben (AutoMigration erkennt Rename nicht automatisch).
---
## Checkliste: Schema-Änderung
```
[ ] 1. Entity-Datei anpassen (Spalte/Tabelle hinzufügen, umbenennen, löschen)
[ ] 2. BollwerkDatabase.kt: version auf N+1 erhöhen
[ ] 3. BollwerkDatabase.kt: autoMigrations um AutoMigration(from=N, to=N+1) ergänzen
→ Bei Rename: spec = MeineRenameSpec::class angeben
→ Bei komplexer Migration: manuelles Migration-Objekt in Migrations.kt
[ ] 4. Build ausführen → N+1.json wird generiert
[ ] 5. MigrationTestHelper-Test in BollwerkDatabaseMigrationTest.kt schreiben
[ ] 6. Build + Tests grün
```
---
## BollwerkDatabase.kt Muster
```kotlin
@Database(
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class,
SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class],
version = 8, // ← erhöhen
exportSchema = true,
autoMigrations = [
AutoMigration(from = 5, to = 6), // No-op (gleiche Schemas)
AutoMigration(from = 6, to = 7), // No-op (Infrastruktur-Baseline)
AutoMigration(from = 7, to = 8), // z.B. neue Spalte
// AutoMigration(from = 8, to = 9, spec = MeineSpec::class) ← bei Rename
]
)
@TypeConverters(LocalDateConverter::class)
internal abstract class BollwerkDatabase : RoomDatabase() { ... }
```
---
## AutoMigration-Fälle
### Fall 1: Neue Spalte mit Default
```kotlin
// Entity: neue Spalte mit @ColumnInfo(defaultValue = "")
@ColumnInfo(name = "barcode", defaultValue = "") val barcode: String = ""
// @Database:
AutoMigration(from = 7, to = 8)
// Kein Spec nötig Room erkennt ADD COLUMN automatisch
```
### Fall 2: Spalte löschen
```kotlin
// Spec-Klasse anlegen (in Migrations.kt oder eigenem File):
@DeleteColumn(tableName = "items", columnName = "legacy_field")
class Migration7To8Spec : AutoMigrationSpec
// @Database:
AutoMigration(from = 7, to = 8, spec = Migration7To8Spec::class)
```
### Fall 3: Spalte umbenennen
```kotlin
@RenameColumn(tableName = "items", fromColumnName = "kcal_per_kg", toColumnName = "kcal_per_unit")
class Migration7To8Spec : AutoMigrationSpec
// @Database:
AutoMigration(from = 7, to = 8, spec = Migration7To8Spec::class)
```
### Fall 4: Komplexe Migration (Daten transformieren)
Wenn AutoMigration nicht reicht (z. B. Daten kopieren, Tabellen zusammenführen):
```kotlin
// In Migrations.kt:
internal val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE items ADD COLUMN quantity_unit TEXT NOT NULL DEFAULT 'Stück'")
db.execSQL("UPDATE items SET quantity_unit = unit WHERE unit IS NOT NULL")
}
}
// In DatabaseModule.kt:
Room.databaseBuilder(context, BollwerkDatabase::class.java, "bollwerk.db")
.addCallback(DefaultDataCallback)
.addMigrations(MIGRATION_7_8)
.build()
```
---
## MigrationTestHelper-Test
```kotlin
@get:Rule
val migrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
BollwerkDatabase::class.java
)
@Test
fun migrate7To8_preservesData() {
// 1. DB im alten Zustand anlegen und Testdaten einfügen
migrationTestHelper.createDatabase(dbName, 7).use { db ->
db.execSQL("INSERT INTO categories (name) VALUES ('Lebensmittel')")
db.execSQL(
"INSERT INTO items (id, name, category_id, quantity, unit, unit_price, " +
"kcal_per_unit, expiry_date, location_id, notes, last_updated) " +
"VALUES ('item-1', 'Wasser', 1, 10.0, 'Liter', 0.5, NULL, NULL, 1, '', 0)"
)
}
// 2. Migration durchführen und Schema validieren
migrationTestHelper.runMigrationsAndValidate(dbName, 8, true)
.use { db ->
// 3. Datenpersistenz prüfen
db.query("SELECT id, name FROM items").use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals("item-1", cursor.getString(0))
}
}
}
```
**Wichtig:** Die Schema-JSON-Dateien aus `app/schemas/` müssen als Assets im androidTest-Quellsatz liegen (bereits konfiguriert). Der `MigrationTestHelper` liest sie automatisch.
---
## Häufige Fehler & Lösungen
| Fehler | Ursache | Lösung |
| --------------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
| `IllegalStateException: A migration from X to Y was required but not found` | Version erhöht ohne AutoMigration-Eintrag | `@AutoMigration(from=X, to=Y)` in `@Database` eintragen |
| `Cannot find implementation for AutoMigration` | Build noch nicht nach Schema-Änderung ausgeführt | `./gradlew assembleDebug` ausführen, `Y.json` muss existieren |
| `Expected X migrations, found Y` | `addMigrations()` fehlt für manuelle Migration | In `DatabaseModule.kt` `.addMigrations(MIGRATION_X_Y)` eintragen |
| `MigrationTestHelper: No schema file found for version X` | Schema-JSON für Version X existiert nicht in `app/schemas/` | Build mit Version X zuerst ausführen, JSON committen |
| Rename-Spalte → Datenverlust | AutoMigration interpretiert Rename als DROP+ADD | `@RenameColumn`-Spec zwingend angeben |
---
## Aktuelle DB-Version
Aktuelle Produktionsversion: **7** (`BollwerkDatabase.kt`, `version = 7`)
Migrations-Pfade vorhanden:
- v5 → v6: No-op (identische Schemas, `@AutoMigration`)
- v6 → v7: No-op (Infrastruktur-Baseline, `@AutoMigration`)
Nächste Version für neue Schema-Änderung: **v8**
---
## Hinweis: Server-Migration (Flyway)
Für Schema-Änderungen auf dem **Server** (PostgreSQL + Exposed):
- SQL-Skripte unter `server/src/main/resources/db/migration/`
- Namenskonvention: `V{N}__{beschreibung}.sql`
- Flyway ist integriert (`baselineOnMigrate = true`) läuft beim Server-Start automatisch
- Bestehende Datenbanken werden auf V1 gebaselined; neue Skripte werden einmalig ausgeführt
```sql
-- Beispiel: server/src/main/resources/db/migration/V3__add_barcode_column.sql
ALTER TABLE items ADD COLUMN IF NOT EXISTS barcode VARCHAR(100);
```
`IF NOT EXISTS` / `IF EXISTS` in SQL verwenden, um Idempotenz zu gewährleisten.