"""Migrationen dürfen keine Daten vernichten. 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. Ein zusätzliches Feld an ``categories`` löschte so sämtliche Schlagwörter und Budget-Vorlagen und setzte die Kategorie aller Buchungen auf NULL. Dieser Test hält das fest. """ import sqlite3 import subprocess import sys import tempfile from pathlib import Path BACKEND = Path(__file__).resolve().parents[1] # Ein Datensatz je Tabelle, die per Fremdschlüssel an categories hängt. SETUP = """ INSERT INTO users (username,password_hash,role,is_active,must_change_password) VALUES ('t','x','user',1,0); INSERT INTO category_groups (owner_id,name,kind,icon,color,position,is_builtin) VALUES (1,'G','expense','list','#000',0,0); INSERT INTO categories (owner_id,group_id,name,type,icon,color,position,is_builtin,is_archived) VALUES (1,1,'Lebensmittel','expense','x','#000',0,0,0); INSERT INTO accounts (owner_id,name,type,currency,icon,color,opening_balance_cents,is_archived,position) VALUES (1,'Giro','girokonto','EUR','x','#000',0,0,0); INSERT INTO categorization_rules (owner_id,match_field,pattern,category_id,priority,hits,source,applied_count,overridden_count,confirmed,is_transfer,kind) VALUES (1,'any','rewe',1,10,0,'manual',0,0,0,0,'expense'); INSERT INTO budget_template_lines (owner_id,category_id,planned_cents) VALUES (1,1,20000); INSERT INTO transactions (owner_id,type,date,account_id,amount_cents,amount_eur_cents,category_id) VALUES (1,'expense','2026-03-01',1,5000,5000,1); """ def _alembic(data_dir: Path, ziel: str) -> None: subprocess.run( [sys.executable, "-c", f"from alembic import command; from alembic.config import Config;" f" command.upgrade(Config('alembic.ini'), {ziel!r})"], cwd=BACKEND, check=True, capture_output=True, env={"PATH": "/usr/bin:/bin", "APP_DATA_DIR": str(data_dir), "APP_ECB_FETCH_ON_START": "false", "APP_SESSION_SECRET": "test", "PYTHONPATH": str(BACKEND)}, ) def test_batch_migration_keeps_dependent_rows(): with tempfile.TemporaryDirectory(prefix="cf-mig-") as tmp: data_dir = Path(tmp) # Stand vor dem Umbau von `categories`, dann Testdaten einspielen. _alembic(data_dir, "0012") con = sqlite3.connect(data_dir / "app.db") con.executescript(SETUP) con.commit() con.close() _alembic(data_dir, "head") con = sqlite3.connect(data_dir / "app.db") zahl = lambda s: con.execute(s).fetchone()[0] # noqa: E731 assert zahl("SELECT COUNT(*) FROM categorization_rules") == 1, "Schlagwörter verloren" assert zahl("SELECT COUNT(*) FROM budget_template_lines") == 1, "Budget-Vorlage verloren" assert zahl("SELECT COUNT(*) FROM transactions WHERE category_id IS NOT NULL") == 1, \ "Kategorie der Buchung verloren" con.close()