fix: Migrationen löschen keine abhängigen Daten mehr

Auf SQLite baut Alembic (render_as_batch) eine Tabelle um, indem es eine Kopie
anlegt und das Original löscht. Bei DROP TABLE führt SQLite ein implizites DELETE
aus – mit aktiven Fremdschlüsseln schlägt das per ON DELETE CASCADE auf abhängige
Tabellen durch.

Migration 0013 fügte `categories` nur ein Feld hinzu und vernichtete dabei alle
Schlagwörter und Budget-Vorlagen und setzte die Kategorie sämtlicher Buchungen
auf NULL. Reproduziert und behoben: Fremdschlüssel werden für die Dauer der
Migration abgeschaltet (vor der Transaktion, sonst ignoriert SQLite den Befehl)
und danach wieder aktiviert. Lässt sich das Abschalten nicht bestätigen, bricht
die Migration ab, statt Daten zu riskieren.

Regressionstest legt Daten auf Stand 0012 an, migriert auf head und prüft, dass
Schlagwörter, Budget-Vorlagen und Kategoriezuordnungen erhalten bleiben.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 14:21:03 +02:00
co-authored by Claude Opus 4.8
parent af865a5c7b
commit 35d89b2b8f
2 changed files with 99 additions and 8 deletions
+31 -8
View File
@@ -34,14 +34,37 @@ def run_migrations_offline() -> None:
def run_migrations_online() -> None:
with engine.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
# Fremdschlüssel für die Dauer der Migration abschalten.
#
# ``render_as_batch`` baut eine Tabelle um, indem es eine Kopie anlegt und
# das Original löscht. SQLite führt bei DROP TABLE ein implizites DELETE
# aus – mit aktiven Fremdschlüsseln schlägt das per ON DELETE CASCADE auf
# abhängige Tabellen durch. Ein zusätzliches Feld an ``categories`` löschte
# so sämtliche Schlagwörter und Budget-Vorlagen und setzte die Kategorie
# aller Buchungen auf NULL.
#
# Der Befehl muss VOR der Transaktion laufen, sonst ignoriert SQLite ihn.
roh = connection.connection
roh.rollback()
roh.execute("PRAGMA foreign_keys=OFF")
aktiv = roh.execute("PRAGMA foreign_keys").fetchone()[0]
if aktiv:
raise RuntimeError(
"Fremdschlüssel liessen sich nicht abschalten – Migration abgebrochen, "
"sonst droht Datenverlust beim Tabellen-Umbau."
)
try:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
finally:
roh.rollback()
roh.execute("PRAGMA foreign_keys=ON")
if context.is_offline_mode():