-- 002_backfill_inbox_ats.sql -- Manual one-shot: backfill the inbox-side ATS columns that gained a writer in -- job/candidate/views.py::CandidateScoring._sync_inbox_ats. Before that change, -- inbox_messages.ats_score / ats_band were never written and ats_results held -- 0 rows, so messages scored earlier show null on the Applications tab. -- -- Idempotent: the UPDATE only touches rows still missing a score, and the -- INSERT skips any application that already has an ats_results row. -- Applied automatically at startup by alembic_setup.run_manual_sql() once the -- schema is at head; recorded in manual_migrations. Safe to re-run by hand. -- -- The Candidates.inbox_message_id join is historical: that column is dropped -- after revision b7d4e8f1a203. Those three statements run only when the column -- is still present (a DB that has not yet reached that revision). Fresh installs -- skip them — there is no pre-denorm history to copy. DO $backfill$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'app' AND table_name = 'candidates' AND column_name = 'inbox_message_id' ) THEN -- Winning score per message, mirroring CandidateView._recommendation precedence: -- the completed score against the ASSIGNED job wins, else the newest completed. EXECUTE $sql$ WITH ranked AS ( SELECT c.inbox_message_id, c.job_id, c.match_score, c.model, c.updated_at, ROW_NUMBER() OVER ( PARTITION BY c.inbox_message_id ORDER BY (c.job_id = m.assigned_job_post_id) DESC NULLS LAST, c.updated_at DESC ) AS rn FROM app.candidates c JOIN app.inbox_messages m ON m.id = c.inbox_message_id WHERE c.status = 'completed' AND c.inbox_message_id IS NOT NULL ) UPDATE app.inbox_messages m SET ats_score = r.match_score, ats_band = CASE WHEN r.match_score >= 82 THEN 'Strong Match' WHEN r.match_score >= 65 THEN 'Potential Match' ELSE 'Weak Match' END FROM ranked r WHERE r.rn = 1 AND m.id = r.inbox_message_id AND m.ats_score IS NULL $sql$; -- One current ats_results row per already-scored application. computed_at takes -- the candidates row's timestamp so the history reflects when the score happened. -- inbox can hold several join rows per message; DISTINCT ON keeps the newest. EXECUTE $sql$ WITH ranked AS ( SELECT c.inbox_message_id, c.job_id, c.match_score, c.model, c.updated_at, ROW_NUMBER() OVER ( PARTITION BY c.inbox_message_id ORDER BY (c.job_id = m.assigned_job_post_id) DESC NULLS LAST, c.updated_at DESC ) AS rn FROM app.candidates c JOIN app.inbox_messages m ON m.id = c.inbox_message_id WHERE c.status = 'completed' AND c.inbox_message_id IS NOT NULL ), links AS ( SELECT DISTINCT ON (message_id) message_id, id AS inbox_id FROM app.inbox ORDER BY message_id, created_at DESC ) INSERT INTO app.ats_results (id, inbox_id, job_post_id, overall_score, band, is_current, superseded_by_id, model_name, computed_at, created_at) SELECT gen_random_uuid(), l.inbox_id, r.job_id, r.match_score, CASE WHEN r.match_score >= 82 THEN 'Strong Match' WHEN r.match_score >= 65 THEN 'Potential Match' ELSE 'Weak Match' END, true, NULL, r.model, r.updated_at, NOW() FROM ranked r JOIN links l ON l.message_id = r.inbox_message_id WHERE r.rn = 1 AND NOT EXISTS ( SELECT 1 FROM app.ats_results a WHERE a.inbox_id = l.inbox_id ) $sql$; -- Older inbox history rows predate candidate_id; link the current ones back to -- the candidates row that produced them. EXECUTE $sql$ UPDATE app.ats_results a SET candidate_id = c.id FROM app.inbox i JOIN app.candidates c ON c.inbox_message_id = i.message_id AND c.status = 'completed' WHERE a.inbox_id = i.id AND a.candidate_id IS NULL AND c.job_id = a.job_post_id $sql$; END IF; END $backfill$; -- inbox.ats_id -> the CURRENT ats_results row for that application, so the -- score is one direct id join away. Newest current row wins if several exist. UPDATE app.inbox i SET ats_id = a.id, updated_at = NOW() FROM ( SELECT DISTINCT ON (inbox_id) inbox_id, id FROM app.ats_results WHERE is_current AND inbox_id IS NOT NULL ORDER BY inbox_id, computed_at DESC ) a WHERE a.inbox_id = i.id AND i.ats_id IS DISTINCT FROM a.id; -- Upload-sourced scores get history rows too: inbox_id stays NULL (no inbox -- application exists) and the row links via candidate_id instead. INSERT INTO app.ats_results (id, inbox_id, candidate_id, job_post_id, overall_score, band, is_current, superseded_by_id, model_name, computed_at, created_at) SELECT gen_random_uuid(), NULL, c.id, c.job_id, c.match_score, CASE WHEN c.match_score >= 82 THEN 'Strong Match' WHEN c.match_score >= 65 THEN 'Potential Match' ELSE 'Weak Match' END, true, NULL, c.model, c.updated_at, NOW() FROM app.candidates c WHERE c.status = 'completed' AND c.source = 'upload' AND NOT EXISTS ( SELECT 1 FROM app.ats_results a WHERE a.candidate_id = c.id );