diff --git a/internal/db/migrations/0001_cloud_core_skeleton.sql b/internal/db/migrations/0001_cloud_core_skeleton.sql index 8dff62e7..d56be2d0 100644 --- a/internal/db/migrations/0001_cloud_core_skeleton.sql +++ b/internal/db/migrations/0001_cloud_core_skeleton.sql @@ -294,6 +294,25 @@ CREATE TABLE IF NOT EXISTS workbench_trace_events ( ); CREATE INDEX IF NOT EXISTS idx_workbench_trace_events_trace_seq ON workbench_trace_events(trace_id, source_seq, projected_seq, id); CREATE INDEX IF NOT EXISTS idx_workbench_trace_events_session_seq ON workbench_trace_events(session_id, source_seq, projected_seq, id); +WITH traces_needing_resequence AS ( + SELECT trace_id + FROM workbench_trace_events + WHERE trace_id IS NOT NULL + GROUP BY trace_id + HAVING COUNT(*) <> COUNT(DISTINCT projected_seq) +), ranked_trace_events AS ( + SELECT + workbench_trace_events.id, + (ROW_NUMBER() OVER (PARTITION BY workbench_trace_events.trace_id ORDER BY workbench_trace_events.occurred_at, workbench_trace_events.id))::integer AS durable_projection_seq + FROM workbench_trace_events + INNER JOIN traces_needing_resequence + ON traces_needing_resequence.trace_id = workbench_trace_events.trace_id +) +UPDATE workbench_trace_events AS existing_trace_events +SET projected_seq = ranked_trace_events.durable_projection_seq +FROM ranked_trace_events +WHERE existing_trace_events.id = ranked_trace_events.id + AND existing_trace_events.projected_seq IS DISTINCT FROM ranked_trace_events.durable_projection_seq; CREATE UNIQUE INDEX IF NOT EXISTS idx_workbench_trace_events_trace_source_event ON workbench_trace_events(trace_id, source_event_id) WHERE source_event_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_workbench_trace_events_trace_projected_seq ON workbench_trace_events(trace_id, projected_seq); diff --git a/internal/db/schema.test.ts b/internal/db/schema.test.ts index 528715f7..d6d176c3 100644 --- a/internal/db/schema.test.ts +++ b/internal/db/schema.test.ts @@ -84,6 +84,10 @@ test("initial migration declares Workbench fact backfill sources", async () => { assert.ok(traceBackfillMatch, "missing workbench_trace_events idempotent backfill"); assert.match(traceBackfillMatch[0], /ROW_NUMBER\(\) OVER \(PARTITION BY trace_id ORDER BY occurred_at, id\)/u); assert.match(traceBackfillMatch[0], /durable_projection_seq/u); + const traceResequenceMatch = sql.match(/WITH traces_needing_resequence AS \([\s\S]*?CREATE UNIQUE INDEX IF NOT EXISTS idx_workbench_trace_events_trace_projected_seq/u); + assert.ok(traceResequenceMatch, "missing workbench_trace_events projected_seq resequence before unique index"); + assert.match(traceResequenceMatch[0], /COUNT\(\*\) <> COUNT\(DISTINCT projected_seq\)/u); + assert.match(traceResequenceMatch[0], /UPDATE workbench_trace_events AS existing_trace_events/u); }); test("protocol record guards catch schema drift before runtime writes", () => {