diff --git a/migrations/032_fix_session_sequence_drift.sql b/migrations/032_fix_session_sequence_drift.sql new file mode 100644 index 0000000..3264070 --- /dev/null +++ b/migrations/032_fix_session_sequence_drift.sql @@ -0,0 +1,57 @@ +-- Migration 032: Fix session_sequences drift (CF-816) +-- Problem: Retro-imported sessions with explicit session_number bypass the trigger, +-- leaving session_sequences.next_number behind the actual MAX(session_number). +-- Next auto-assigned number then collides with the unique index. +-- Fix: Make get_next_session_number() self-healing by always checking actual max. + +-- Step 1: Replace the function with a self-healing version +CREATE OR REPLACE FUNCTION get_next_session_number(p_project TEXT) +RETURNS INTEGER AS $$ +DECLARE + v_seq_number INTEGER; + v_max_number INTEGER; + v_number INTEGER; +BEGIN + -- Insert project if doesn't exist + INSERT INTO projects (key, name) VALUES (p_project, p_project) + ON CONFLICT (key) DO NOTHING; + + -- Insert sequence if doesn't exist + INSERT INTO session_sequences (project, next_number) + VALUES (p_project, 1) + ON CONFLICT (project) DO NOTHING; + + -- Get the actual max session_number for this project (handles external inserts) + SELECT COALESCE(MAX(session_number), 0) INTO v_max_number + FROM sessions + WHERE project = p_project; + + -- Sync sequence forward if it fell behind (self-healing) + UPDATE session_sequences + SET next_number = GREATEST(next_number, v_max_number + 1), + last_updated = NOW() + WHERE project = p_project; + + -- Now atomically increment and return + UPDATE session_sequences + SET next_number = next_number + 1, + last_updated = NOW() + WHERE project = p_project + RETURNING next_number - 1 INTO v_number; + + RETURN v_number; +END; +$$ LANGUAGE plpgsql; + +-- Step 2: Sync all existing sequences to match actual data +UPDATE session_sequences sq +SET next_number = GREATEST(sq.next_number, sub.actual_max + 1), + last_updated = NOW() +FROM ( + SELECT project, COALESCE(MAX(session_number), 0) AS actual_max + FROM sessions + WHERE project IS NOT NULL + GROUP BY project +) sub +WHERE sq.project = sub.project + AND sq.next_number <= sub.actual_max;