"""Die Kennung der Quelle steht an der Buchung selbst. Bisher lag sie nur am Eingangs-Eintrag (``inbox_items.external_id``). Das hat gereicht, solange dieser Eintrag da war – und genau das ist er nicht immer: Er faellt weg, wenn ein Konto geloescht wird, er fehlt in aelteren Backups, und nach einem neu verbundenen Bankkonto passt sein Praefix nicht mehr. Dann war die Zahlung fuer den naechsten Abruf neu, obwohl sie laengst gebucht war. Die Kennung gehoert deshalb an die Buchung. Dort ueberlebt sie alles, was mit dem Eingang passiert, und die Eindeutigkeit haelt die Datenbank selbst durch – nicht erst der pruefende Code davor. Vorhandene Buchungen bekommen die Kennung nachgetragen, solange ihr Eingangs-Eintrag noch existiert. Was jetzt schon verloren ist, bleibt verloren; ab hier faellt nichts Neues mehr aus. Revision ID: 0027 Revises: 0026 """ import sqlalchemy as sa from alembic import op revision = "0027" down_revision = "0026" branch_labels = None depends_on = None def upgrade() -> None: with op.batch_alter_table("transactions") as batch: batch.add_column(sa.Column("source_uid", sa.String(length=320), nullable=True)) # Nachtragen, was der Eingang noch weiss. Der Name der Quelle kommt davor: # Eine Nachrichten-Kennung aus dem Postfach und eine Umsatzkennung der Bank # duerfen nie zufaellig gleich lauten. # # Zwei Eingangs-Eintraege koennen auf dieselbe Buchung zeigen (die beiden # Seiten eines Uebertrags). Dann gewinnt der aeltere – ``MIN(id)``. Zwei # verschiedene Buchungen koennen dagegen nicht dieselbe Kennung bekommen: # (owner_id, source, external_id) ist im Eingang bereits eindeutig. op.execute(sa.text(""" UPDATE transactions SET source_uid = ( SELECT i.source || ':' || i.external_id FROM inbox_items i WHERE i.transaction_id = transactions.id AND i.owner_id = transactions.owner_id AND i.external_id IS NOT NULL ORDER BY i.id LIMIT 1 ) WHERE source_uid IS NULL """)) # Erst jetzt eindeutig stellen – vorher stuende die Regel ueber leeren # Spalten und der Nachtrag koennte sie nicht mehr fuellen. # # Ein INDEX, keine UNIQUE-Spaltenregel: Letztere ginge nur ueber einen # Tabellen-Umbau, und der loescht auf SQLite per ON DELETE CASCADE die # Uebertrags-Verknuepfungen und abgelehnten Vorschlaege mit. Genau davor # steht tests/test_migrations.py. Der Index haelt dasselbe durch, ohne die # Tabelle anzufassen. op.create_index("uq_txn_owner_source_uid", "transactions", ["owner_id", "source_uid"], unique=True) def downgrade() -> None: op.drop_index("uq_txn_owner_source_uid", table_name="transactions") with op.batch_alter_table("transactions") as batch: batch.drop_column("source_uid")