diff --git a/packages/db/src/migrations/0077_workspace_closed_state_consistency.sql b/packages/db/src/migrations/0077_workspace_closed_state_consistency.sql new file mode 100644 index 00000000000..6ac4f108767 --- /dev/null +++ b/packages/db/src/migrations/0077_workspace_closed_state_consistency.sql @@ -0,0 +1,12 @@ +-- SHA-2492: closed_at MUST pair with a closed status (archived/cleanup_failed). +-- +-- Added with NOT VALID so the migration is fast and does not lock the table +-- while validating every existing row. The constraint takes effect for all +-- INSERTs and UPDATEs immediately. Existing violator rows (1037 known on +-- prod paperclip DB at filing time) must be backfilled to status='archived' +-- before running `ALTER TABLE ... VALIDATE CONSTRAINT +-- execution_workspaces_closed_state_consistency` as a follow-up. +ALTER TABLE "execution_workspaces" + ADD CONSTRAINT "execution_workspaces_closed_state_consistency" + CHECK ("closed_at" IS NULL OR "status" IN ('archived', 'cleanup_failed')) + NOT VALID; diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 6833d3032d3..ea437a7a16b 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -540,6 +540,13 @@ "when": 1776541198667, "tag": "0076_budget_incidents_dedupe_open_only", "breakpoints": true + }, + { + "idx": 77, + "version": "7", + "when": 1778800000000, + "tag": "0077_workspace_closed_state_consistency", + "breakpoints": true } ] } diff --git a/packages/db/src/schema/execution_workspaces.ts b/packages/db/src/schema/execution_workspaces.ts index 72e63d5b851..97abb9ecbb3 100644 --- a/packages/db/src/schema/execution_workspaces.ts +++ b/packages/db/src/schema/execution_workspaces.ts @@ -1,5 +1,7 @@ +import { sql } from "drizzle-orm"; import { type AnyPgColumn, + check, index, jsonb, pgTable, @@ -64,5 +66,10 @@ export const executionWorkspaces = pgTable( table.companyId, table.branchName, ), + // closed_at MUST pair with a closed status. See SHA-2492. + closedStateConsistency: check( + "execution_workspaces_closed_state_consistency", + sql`${table.closedAt} IS NULL OR ${table.status} IN ('archived', 'cleanup_failed')`, + ), }), );