Repair the 2026-07-30 review findings #1

Merged
kami merged 15 commits from repair/review-2026-07-30 into master 2026-07-30 22:48:15 +02:00
2 changed files with 72 additions and 1 deletions
Showing only changes of commit 3ffba3f24b - Show all commits
+30 -1
View File
@@ -257,13 +257,42 @@ CREATE INDEX IF NOT EXISTS idx_play_history_user_played_at ON play_history(user_
CREATE TABLE IF NOT EXISTS feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
track_id UUID REFERENCES tracks(id) ON DELETE CASCADE,
-- SET NULL, not CASCADE: feedback is an audit log and must outlive the
-- track. With CASCADE the 'deleted_permanent' row written by
-- hardDeleteTrack() deletes itself as soon as the track row goes.
track_id UUID REFERENCES tracks(id) ON DELETE SET NULL,
action TEXT NOT NULL,
-- Denormalised track identity, written on 'deleted_permanent' rows only.
-- track_id goes NULL when the track is deleted, so without these the audit
-- row survives but no longer says what was destroyed.
track_path TEXT,
track_title TEXT,
track_artist TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_feedback_user_action ON feedback(user_id, action);
-- Files whose DB row has been deleted but whose bytes are still on disk.
-- The cleanup sweep commits its transaction BEFORE unlinking, so this table is
-- the durable record of that window: a row is inserted with the deletion and
-- removed only once the unlink succeeds. A row that lingers means the unlink
-- failed (EROFS/EACCES/...) or the worker died mid-sweep; the next sweep retries
-- it. No FK to tracks — the track is already gone.
CREATE TABLE IF NOT EXISTS pending_file_deletions (
path TEXT PRIMARY KEY,
track_id UUID,
track_title TEXT,
track_artist TEXT,
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
attempts INTEGER NOT NULL DEFAULT 0,
last_attempt_at TIMESTAMP,
last_error TEXT
);
CREATE INDEX IF NOT EXISTS idx_pending_file_deletions_requested
ON pending_file_deletions(requested_at);
CREATE TABLE IF NOT EXISTS track_audio_features (
track_id UUID PRIMARY KEY REFERENCES tracks(id) ON DELETE CASCADE,
bpm REAL,
+42
View File
1
@@ -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 {