Make the owner of an outgoing post a database guarantee #123
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fase-1-fundament-v2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reopened from #110, rebased onto current main (
1912808). Verified on this branch: backend suite 1334 passed with the 100% coverage gate, pre-commit clean.Update 2026-07-19: the SQLite rationale for keeping the single-column FKs was overstated and is corrected in the code comments (commit
91bd139) and below - the unnamed FK can be dropped via batch_alter_table's naming_convention; it stays because dropping it is extra migration surface for no behavioural gain.Fase 1 van "nieuw bericht opstellen en plannen". Deze PR bouwt nog geen feature: hij dicht een structurele zwakte waar de rest van dat werk bovenop komt te staan, en die vandaag al bestaat.
Het probleem
De tick-loop die geplande berichten verstuurt, leidt de afzender af uit de kaart, niet uit de rij die hij gaat versturen:
Dat werkt alleen zolang
outgoing_posts.user_idgelijk is aancards.user_id. Er was geen enkele databasegarantie dat dat zo is: geen composite FK, geen check-constraint. Het is een applicatie-invariant die op drie plekken met de hand werd volgehouden (_ensure_card, en decard_id_mapin de import).Niet exploiteerbaar vandaag - alle drie de plekken doen het goed. Maar de veiligheid was geen eigenschap van het schema, en de runner draait buiten het requestpad, dus zonder
require_user-vangrail. Eén endpoint dat de eigenaarscheck vergeet, of één handmatig bewerkte importbundel, en de runner verstuurt de tekst van gebruiker A met het token van gebruiker B. Geen FK, geen constraint en geen test zou aanslaan.Vanaf fase 4 heeft een rij geen kaart meer om zijn identiteit uit af te leiden, dus dit moest eerst.
Wat er verandert
Twee composite foreign keys.
PRAGMA foreign_keys=ONstaat aan voor elke SQLite-verbinding, dus ze worden op beide databases afgedwongen:Daarmee is het account waarvan het token een rij verstuurt, per databasegarantie een account van de eigenaar van die rij. NULL-kolommen slaan de controle over (MATCH SIMPLE), dus een rij zonder account - en straks zonder kaart - past er gewoon in.
De enkelvoudige FK's blijven staan. Ze zijn overbodig naast de composite variant, maar onschadelijk. De naamloze op
card_idis op SQLite wel degelijk te droppen -batch_alter_tableaccepteert eennaming_conventiondie gereflecteerde naamloze constraints een naam geeft, en de batch herbouwt de tabel toch al om de composite FK's toe te voegen. Het droppen is dus geen onmogelijkheid maar extra migratie-oppervlak, zonder gedragsverschil; daarom blijven ze staan.De runner leest zijn afzender van de rij. De terugval op
card.account_idverdwijnt: een rij zonder account faalt nu zichtbaar met "geen Mattermost-account gekoppeld" in plaats van een identiteit te lenen van een naburig record. En de groepering keyt op(card_id, account_id), niet op de kaart alleen - de worker bouwt één client per groep, dus een groep moet single-account zijn.Migraties worden voortaan getest. Dat gebeurde niet:
conftest.pybouwt het schema metcreate_all, dus een revisie die fout was, ontbrak of afweek van de modellen bleef groen. De nieuwe harness draaitupgrade/downgrade/upgradetegen een echte database, vergelijkt het gemigreerde schema met de modellen via Alembic's eigencompare_metadata, en schrijft er ook echt rijen in. CI draait hem ook tegen Postgres: batch-mode bouwt de tabel opnieuw op op SQLite en doetALTER TABLEop Postgres, dus een revisie kan op de één slagen en op de ander falen.Wat de review vond, en wat ik ermee deed
Ik heb er vier adversariële reviewlenzen op gezet (migratieveiligheid, security, correctheid, testkwaliteit) en de bevindingen daarna opnieuw laten weerleggen. 13 van de 14 zijn weerlegd. Wat overbleef:
Ik had een voetzoeker ingebouwd. De testfixture deed
DROP SCHEMA public CASCADEop watWAGGLE_TEST_MIGRATION_URLaanwees, en mijn eigen docstring zei: wijs 'm naar de dev-database uitcompose.dev.yaml. Wie de instructie volgde, gooide zijn dev-data weg - en de test slaagde gewoon. Er zit nu een guard op, zoalsdev_seed.pydie ook heeft.Drie vals-groene tests. De reviewer bewees het met mutaties: haal
ux_cards_id_useruit de revisie en álle tests slaagden nog steeds, terwijl elke INSERT in het gemigreerde schema faalde met "foreign key mismatch" (een composite FK zonder bijpassende unique key op de ouder is geldige DDL die simpelweg elke rij weigert). Datzelfde gold voor de backfill: sloop de hele lus, test blijft groen - want op SQLite dwingt Alembic's eigen engine geen foreign keys af, dus de constraint "raiset" helemaal niet. Beide mutaties doden nu tests.Mijn comment beloofde te veel. Ik schreef dat de FK's "één eigenaar én één account" per groep garanderen. Ze garanderen één eigenaar. Dat er ook één account is, leunde op een ongeschreven invariant. In plaats van de comment te repareren heb ik de oorzaak weggenomen:
account_idzit nu in de groeperingssleutel.De grootste verdenking - dat de SQLite-tabelherbouw de partiële index
ux_outgoing_one_draft_per_cardzou droppen - is empirisch weerlegd: hij overleeft mét zijnWHERE-clausule, net als elke andere index en constraint op de drie tabellen. Er staat nu een test op.Poorten
pre-commit run --all-filesgroen,tscschoon,eslint0 errors.Let op bij review
De backfill in de revisie draait bij het opstarten van de applicatie. Faalt hij, dan start de pod niet. Hij repareert daarom in plaats van te blokkeren: een rij waarvan de eigenaar afwijkt van de kaart wordt uitgelijnd (nul rijen verwacht - de runner verstuurde zo'n rij toch al met het token van de kaart-eigenaar), en een account dat niet van de eigenaar is wordt genulld in plaats van de upgrade te laten crashen. Zo'n rij belandt zichtbaar in Verzendproblemen.
ef14a0532291bd1395d691bd1395d6921ba9cda5backend-test-postgres faalt hier niet door deze branch: de onboarding-datamigratie c1f0a7d92b41 op main breekt op Postgres (json-kolom heeft geen =-operator; de varchar-bind wordt ook niet impliciet gecast). Jullie nieuwe migratie-testharnas vangt 'm precies. Fix staat klaar in PR #136; na merge daarvan heeft deze branch één rebase nodig. De release-scripts-failure eerder was runner-flake (lokaal groen tegen deze head).
921ba9cda5bd3684c12e