diff --git a/packages/db/src/client.test.ts b/packages/db/src/client.test.ts index 552db3b0d7..75e7effcc5 100644 --- a/packages/db/src/client.test.ts +++ b/packages/db/src/client.test.ts @@ -541,4 +541,131 @@ describeEmbeddedPostgres("applyPendingMigrations", () => { }, 20_000, ); + + it( + "replays the run responsible user repair migration when heartbeat run issue refs are identifiers", + async () => { + const connectionString = await createTempDatabase(); + + await applyPendingMigrations(connectionString); + + const sql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const runResponsibleUserRepairHash = await migrationHash( + "0131_repair_run_responsible_user_context_refs.sql", + ); + + await sql.unsafe(` + INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at") + VALUES ('00000000-0000-0000-0000-000000000130', 'Migration Test Co', 'TST130', now(), now()) + `); + await sql.unsafe(` + INSERT INTO "company_memberships" ( + "id", + "company_id", + "principal_type", + "principal_id", + "status", + "membership_role", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000131', + '00000000-0000-0000-0000-000000000130', + 'user', + 'owner-user', + 'active', + 'owner', + now(), + now() + ) + `); + await sql.unsafe(` + INSERT INTO "agents" ("id", "company_id", "name", "role", "adapter_type", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000132', + '00000000-0000-0000-0000-000000000130', + 'Migration Agent', + 'general', + 'process', + now(), + now() + ) + `); + await sql.unsafe(` + INSERT INTO "issues" ( + "id", + "company_id", + "title", + "status", + "responsible_user_id", + "identifier", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000133', + '00000000-0000-0000-0000-000000000130', + 'Identifier referenced issue', + 'todo', + 'issue-user', + 'TST130-1', + now(), + now() + ) + `); + await sql.unsafe(` + INSERT INTO "heartbeat_runs" ( + "id", + "company_id", + "agent_id", + "status", + "responsible_user_id", + "context_snapshot", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000134', + '00000000-0000-0000-0000-000000000130', + '00000000-0000-0000-0000-000000000132', + 'completed', + NULL, + '{"issueId":"TST130-1"}'::jsonb, + now(), + now() + ) + `); + await sql.unsafe( + `DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${runResponsibleUserRepairHash}'`, + ); + } finally { + await sql.end(); + } + + const pendingState = await inspectMigrations(connectionString); + expect(pendingState).toMatchObject({ + status: "needsMigrations", + pendingMigrations: ["0131_repair_run_responsible_user_context_refs.sql"], + reason: "pending-migrations", + }); + + await applyPendingMigrations(connectionString); + + const verifySql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const runs = await verifySql.unsafe<{ responsible_user_id: string | null }[]>(` + SELECT "responsible_user_id" + FROM "heartbeat_runs" + WHERE "id" = '00000000-0000-0000-0000-000000000134' + `); + expect(runs).toEqual([{ responsible_user_id: "issue-user" }]); + + } finally { + await verifySql.end(); + } + }, + 20_000, + ); }); diff --git a/packages/db/src/migrations/0130_run_responsible_user_invariant.sql b/packages/db/src/migrations/0130_run_responsible_user_invariant.sql index 87bc74d771..1ae59fa5a9 100644 --- a/packages/db/src/migrations/0130_run_responsible_user_invariant.sql +++ b/packages/db/src/migrations/0130_run_responsible_user_invariant.sql @@ -129,19 +129,74 @@ WHERE h."retry_of_run_id" = original."id" AND h."responsible_user_id" IS NULL AND original."responsible_user_id" IS NOT NULL; --> statement-breakpoint +WITH extracted_run_refs AS ( + SELECT + h."id" AS "run_id", + h."company_id", + NULLIF(h."context_snapshot" ->> 'issueId', '') AS "issue_ref", + 1 AS "ref_priority" + FROM "heartbeat_runs" AS h + WHERE h."responsible_user_id" IS NULL + AND NULLIF(h."context_snapshot" ->> 'issueId', '') IS NOT NULL + + UNION ALL + + SELECT + h."id" AS "run_id", + h."company_id", + NULLIF(h."context_snapshot" ->> 'taskId', '') AS "issue_ref", + 2 AS "ref_priority" + FROM "heartbeat_runs" AS h + WHERE h."responsible_user_id" IS NULL + AND NULLIF(h."context_snapshot" ->> 'taskId', '') IS NOT NULL +), +uuid_run_refs AS ( + SELECT + "run_id", + "company_id", + "issue_ref"::uuid AS "issue_id", + "ref_priority" + FROM extracted_run_refs + WHERE "issue_ref" ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' +), +resolved_run_users AS ( + SELECT DISTINCT ON ("run_id") + "run_id", + "responsible_user_id" + FROM ( + SELECT + refs."run_id", + i."responsible_user_id", + refs."ref_priority", + 1 AS "match_priority" + FROM uuid_run_refs AS refs + JOIN "issues" AS i + ON i."id" = refs."issue_id" + AND i."company_id" = refs."company_id" + WHERE i."responsible_user_id" IS NOT NULL + + UNION ALL + + SELECT + refs."run_id", + i."responsible_user_id", + refs."ref_priority", + 2 AS "match_priority" + FROM extracted_run_refs AS refs + JOIN "issues" AS i + ON i."identifier" = refs."issue_ref" + AND i."company_id" = refs."company_id" + WHERE refs."issue_ref" IS NOT NULL + AND i."responsible_user_id" IS NOT NULL + ) AS candidates + ORDER BY "run_id", "ref_priority" ASC, "match_priority" ASC +) UPDATE "heartbeat_runs" AS h -SET "responsible_user_id" = i."responsible_user_id", +SET "responsible_user_id" = resolved_run_users."responsible_user_id", "updated_at" = now() -FROM "issues" AS i -WHERE h."company_id" = i."company_id" - AND h."responsible_user_id" IS NULL - AND i."responsible_user_id" IS NOT NULL - AND ( - h."context_snapshot" ->> 'issueId' = i."id"::text - OR h."context_snapshot" ->> 'taskId' = i."id"::text - OR h."context_snapshot" ->> 'issueId' = i."identifier" - OR h."context_snapshot" ->> 'taskId' = i."identifier" - ); +FROM resolved_run_users +WHERE h."id" = resolved_run_users."run_id" + AND h."responsible_user_id" IS NULL; --> statement-breakpoint UPDATE "heartbeat_runs" AS h SET "responsible_user_id" = awr."requested_by_actor_id", diff --git a/packages/db/src/migrations/0131_repair_run_responsible_user_context_refs.sql b/packages/db/src/migrations/0131_repair_run_responsible_user_context_refs.sql new file mode 100644 index 0000000000..6edaa302c6 --- /dev/null +++ b/packages/db/src/migrations/0131_repair_run_responsible_user_context_refs.sql @@ -0,0 +1,67 @@ +WITH extracted_run_refs AS ( + SELECT + h."id" AS "run_id", + h."company_id", + NULLIF(h."context_snapshot" ->> 'issueId', '') AS "issue_ref", + 1 AS "ref_priority" + FROM "heartbeat_runs" AS h + WHERE h."responsible_user_id" IS NULL + AND NULLIF(h."context_snapshot" ->> 'issueId', '') IS NOT NULL + + UNION ALL + + SELECT + h."id" AS "run_id", + h."company_id", + NULLIF(h."context_snapshot" ->> 'taskId', '') AS "issue_ref", + 2 AS "ref_priority" + FROM "heartbeat_runs" AS h + WHERE h."responsible_user_id" IS NULL + AND NULLIF(h."context_snapshot" ->> 'taskId', '') IS NOT NULL +), +uuid_run_refs AS ( + SELECT + "run_id", + "company_id", + "issue_ref"::uuid AS "issue_id", + "ref_priority" + FROM extracted_run_refs + WHERE "issue_ref" ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' +), +resolved_run_users AS ( + SELECT DISTINCT ON ("run_id") + "run_id", + "responsible_user_id" + FROM ( + SELECT + refs."run_id", + i."responsible_user_id", + refs."ref_priority", + 1 AS "match_priority" + FROM uuid_run_refs AS refs + JOIN "issues" AS i + ON i."id" = refs."issue_id" + AND i."company_id" = refs."company_id" + WHERE i."responsible_user_id" IS NOT NULL + + UNION ALL + + SELECT + refs."run_id", + i."responsible_user_id", + refs."ref_priority", + 2 AS "match_priority" + FROM extracted_run_refs AS refs + JOIN "issues" AS i + ON i."identifier" = refs."issue_ref" + AND i."company_id" = refs."company_id" + WHERE i."responsible_user_id" IS NOT NULL + ) AS candidates + ORDER BY "run_id", "ref_priority" ASC, "match_priority" ASC +) +UPDATE "heartbeat_runs" AS h +SET "responsible_user_id" = resolved_run_users."responsible_user_id", + "updated_at" = now() +FROM resolved_run_users +WHERE h."id" = resolved_run_users."run_id" + AND h."responsible_user_id" IS NULL; diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index e3e0473eb2..81ba9a4972 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -918,6 +918,13 @@ "when": 1783025224120, "tag": "0130_run_responsible_user_invariant", "breakpoints": true + }, + { + "idx": 131, + "version": "7", + "when": 1783025324120, + "tag": "0131_repair_run_responsible_user_context_refs", + "breakpoints": true } ] }