166 lines
5.7 KiB
SQL
166 lines
5.7 KiB
SQL
-- 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
|
|
);
|