You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Five foreign keys declare a referential action in shared/database/src/schema.ts that the database does not have. The deployed constraints are all plain NO ACTION. This is invisible to drizzle-kit generate — the snapshots agree with schema.ts, so no catch-up migration is ever emitted, and the drift will not surface on its own.
Verified against both the CI database built from the full migration chain (a ci:db-init from empty) and production on 2026-08-21, which rules out a local-only artifact. pg_constraint.confdeltype = 'a' in both:
The audit that produced the table (run against either database):
SELECTc.conname, c.confdeltype, cl.relnameAS child, fcl.relnameAS parent
FROM pg_constraint c
JOIN pg_class cl ONcl.oid=c.conrelidJOIN pg_class fcl ONfcl.oid=c.confrelidJOIN pg_namespace n ONn.oid=cl.relnamespaceWHEREc.contype='f'ANDc.confdeltype='a'ANDn.nspnameIN ('wxyc_schema', 'public')
ORDER BYcl.relname, c.conname;
Ten other FKs also come back NO ACTION from that query (library.*, bins.album_id, flowsheet.label_id, library_identity*, shift_covers.schedule_id, shows.specialty_id). Those are correct — schema.ts declares no action for them either. Only the five above disagree.
How it was found
Writing the teardown for tests/integration/flowsheet-open-shows.spec.js in #2238. DELETE FROM shows raised
PostgresError: update or delete on table "shows" violates foreign key constraint
"show_djs_show_id_shows_id_fk" on table "show_djs"
against rows that schema.ts says should have cascaded. The spec now deletes show_djs explicitly, with a comment pointing here.
Why it matters
A declared-but-absent ON DELETE is worse than no declaration, because callers are written against the declaration:
The artist crossreference FKs are the sharper edge.jobs/artist-unicode-dedup (Unicode-form artist duplication at the catalog write boundary (artistIdFromName) #1897) repoints every artist_id FK explicitly before deleting a duplicate artist, so it is safe because it does not rely on the cascade. Anything that does rely on it — a future artist-merge path, a DELETE /library/:id extension — will fail at runtime against a constraint the schema promised would clean up after it.
A reader of schema.ts is being told something false. That file is the reference every job and service is written from.
Desired end state
schema.ts and the database agree, in one direction or the other, for all five. Either is defensible per constraint — the point is that the two stop disagreeing silently.
Suggested approach
Decide direction per constraint before writing anything. They are not one decision:
show_djs.show_id — cascade is almost certainly right. A show_djs row has no meaning without its show.
genre_artist_crossreference.*, artist_library_crossreference.artist_id — cascade is right for a junction table, but deleting an artist row is not currently a routine operation, and artist-unicode-dedup already handles its own repointing. Worth confirming nothing depends on the absence of the cascade as a safety brake before adding one.
schedule.specialty_id — SET NULL on a deleted specialty show. Check whether schedule readers tolerate a NULL there.
Then either:
Align the database — a migration issuing ALTER TABLE … DROP CONSTRAINT … ; ALTER TABLE … ADD CONSTRAINT … ON DELETE … per constraint. Read docs/migrations.md first; a constraint-adding migration wants a precondition guard, and scripts/validate-migrations.mjs check 8 warns without one. Note the re-add takes a lock and validates existing rows.
Align schema.ts — drop the onDelete clauses so the file describes what is actually deployed, and fix any caller that was relying on the promise.
A guard is worth more than the fix. The reason this sat undetected is that drizzle-kit does not diff referential actions, so nothing in CI can see it. A scripts/-level check comparing pg_constraint.confdeltype against the onDelete clauses parsed out of schema.ts — run in the migrate-dryrun job, which already has a migrated database in hand — would catch the next one at PR time. Same shape as scripts/check-auth-tables-doc.mjs. Consider splitting that into its own follow-up if the per-constraint decisions above take longer than the guard.
Acceptance criteria
A direction is chosen and recorded for each of the five constraints
schema.ts and a freshly-migrated database agree on confdeltype for all five
Production verified to match after deploy (the audit query above returns none of the five)
Any caller relying on a cascade that is being removed — rather than added — is fixed in the same PR
Ideally: a CI check that fails on the next referential-action drift
tests/integration/flowsheet-open-shows.spec.js's teardown comment updated or removed if show_djs gains its cascade
Problem
Five foreign keys declare a referential action in
shared/database/src/schema.tsthat the database does not have. The deployed constraints are all plainNO ACTION. This is invisible todrizzle-kit generate— the snapshots agree withschema.ts, so no catch-up migration is ever emitted, and the drift will not surface on its own.Verified against both the CI database built from the full migration chain (a
ci:db-initfrom empty) and production on 2026-08-21, which rules out a local-only artifact.pg_constraint.confdeltype = 'a'in both:schema.tsdeclaresshow_djs_show_id_shows_id_fkonDelete: 'cascade'NO ACTIONartist_library_crossreference_artist_id_artists_id_fkonDelete: 'cascade'NO ACTIONgenre_artist_crossreference_artist_id_artists_id_fkonDelete: 'cascade'NO ACTIONgenre_artist_crossreference_genre_id_genres_id_fkonDelete: 'cascade'NO ACTIONschedule_specialty_id_specialty_shows_id_fkonDelete: 'set null'NO ACTIONThe audit that produced the table (run against either database):
Ten other FKs also come back
NO ACTIONfrom that query (library.*,bins.album_id,flowsheet.label_id,library_identity*,shift_covers.schedule_id,shows.specialty_id). Those are correct —schema.tsdeclares no action for them either. Only the five above disagree.How it was found
Writing the teardown for
tests/integration/flowsheet-open-shows.spec.jsin #2238.DELETE FROM showsraisedagainst rows that
schema.tssays should have cascaded. The spec now deletesshow_djsexplicitly, with a comment pointing here.Why it matters
A declared-but-absent
ON DELETEis worse than no declaration, because callers are written against the declaration:DELETE FROM showswith liveshow_djsrows raises instead of cascading. That is now a live path:POST /flowsheet/shows/:id/force-end(Operator close for an abandoned show: GET /flowsheet/open-shows + force-end (tubafrenzy parity, dies 8/31) #2235) does not delete shows, but the operator tooling around abandoned shows is the area where someone will reach for one.jobs/artist-unicode-dedup(Unicode-form artist duplication at the catalog write boundary (artistIdFromName) #1897) repoints everyartist_idFK explicitly before deleting a duplicate artist, so it is safe because it does not rely on the cascade. Anything that does rely on it — a future artist-merge path, aDELETE /library/:idextension — will fail at runtime against a constraint the schema promised would clean up after it.schema.tsis being told something false. That file is the reference every job and service is written from.Desired end state
schema.tsand the database agree, in one direction or the other, for all five. Either is defensible per constraint — the point is that the two stop disagreeing silently.Suggested approach
Decide direction per constraint before writing anything. They are not one decision:
show_djs.show_id— cascade is almost certainly right. Ashow_djsrow has no meaning without its show.genre_artist_crossreference.*,artist_library_crossreference.artist_id— cascade is right for a junction table, but deleting an artist row is not currently a routine operation, andartist-unicode-dedupalready handles its own repointing. Worth confirming nothing depends on the absence of the cascade as a safety brake before adding one.schedule.specialty_id—SET NULLon a deleted specialty show. Check whetherschedulereaders tolerate a NULL there.Then either:
ALTER TABLE … DROP CONSTRAINT … ; ALTER TABLE … ADD CONSTRAINT … ON DELETE …per constraint. Readdocs/migrations.mdfirst; a constraint-adding migration wants a precondition guard, andscripts/validate-migrations.mjscheck 8 warns without one. Note the re-add takes a lock and validates existing rows.schema.ts— drop theonDeleteclauses so the file describes what is actually deployed, and fix any caller that was relying on the promise.A guard is worth more than the fix. The reason this sat undetected is that drizzle-kit does not diff referential actions, so nothing in CI can see it. A
scripts/-level check comparingpg_constraint.confdeltypeagainst theonDeleteclauses parsed out ofschema.ts— run in themigrate-dryrunjob, which already has a migrated database in hand — would catch the next one at PR time. Same shape asscripts/check-auth-tables-doc.mjs. Consider splitting that into its own follow-up if the per-constraint decisions above take longer than the guard.Acceptance criteria
schema.tsand a freshly-migrated database agree onconfdeltypefor all fivetests/integration/flowsheet-open-shows.spec.js's teardown comment updated or removed ifshow_djsgains its cascadeRelated
artist-unicode-dedup, which repoints artist FKs by hand and is therefore unaffectedcheck-auth-tables-doc.mjs, the precedent for a schema-vs-documentation CI guard