fix: stop the deleted_permanent audit row from destroying itself

db.service inserted the 'deleted_permanent' feedback row and then deleted
the track, but feedback.track_id was ON DELETE CASCADE (verified on the
live DB: confdeltype = 'c'), so the audit row deleted itself. feedback
contains zero deleted_permanent rows.

feedback is an audit log and must outlive its subject: the FK becomes
ON DELETE SET NULL. track_id was already nullable, and nothing in backend/
or workers/ SELECTs from feedback — the only other reference is
mergeTracks()'s UPDATE feedback SET track_id, which re-points to the
survivor — so no caller assumed non-null.

Migration 20260730_feedback_track_id_set_null drops the constraint by
matching confdeltype rather than by name, since the live schema has
drifted. Verified on a scratch PG16: confdeltype flips 'c' -> 'n' and a
deleted_permanent row survives its track's deletion.

Correct under either resolution of the dislike-lifecycle decision, so it
lands independently of it.

REVIEW-2026-07-30.md finding 6 (cascade only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kami
2026-07-30 23:49:34 +04:00
parent 1e59d21dee
commit 3ffba3f24b
2 changed files with 72 additions and 1 deletions
+42
View File
@@ -631,6 +631,48 @@ const MIGRATIONS: { id: string; sql: string }[] = [
END $mig$;
`,
},
{
id: '20260730_feedback_track_id_set_null',
sql: `
-- feedback is an audit log, but feedback.track_id was
-- REFERENCES tracks(id) ON DELETE CASCADE. hardDeleteTrack() /
-- permanentlyDeleteTrack() insert a 'deleted_permanent' row and then
-- delete the track, so the audit row deleted itself — which is exactly
-- why the live feedback table contains zero 'deleted_permanent' rows.
-- Switch to ON DELETE SET NULL so audit rows outlive their track. Nothing
-- reads feedback.track_id expecting non-null (there are no SELECTs against
-- it at all; the only other reference is the dedup merge in
-- mergeTracks(), which rewrites track_id to the survivor).
ALTER TABLE feedback ALTER COLUMN track_id DROP NOT NULL;
DO $mig$
DECLARE
cname TEXT;
BEGIN
FOR cname IN
SELECT con.conname
FROM pg_constraint con
WHERE con.conrelid = 'feedback'::regclass
AND con.contype = 'f'
AND con.confrelid = 'tracks'::regclass
AND con.confdeltype <> 'n' -- 'n' = SET NULL; anything else is wrong
LOOP
EXECUTE format('ALTER TABLE feedback DROP CONSTRAINT %I', cname);
END LOOP;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'feedback'::regclass
AND contype = 'f'
AND confrelid = 'tracks'::regclass
) THEN
ALTER TABLE feedback
ADD CONSTRAINT feedback_track_id_fkey
FOREIGN KEY (track_id) REFERENCES tracks(id) ON DELETE SET NULL;
END IF;
END $mig$;
`,
},
];
export class DbService {