From 0f08c2b5265ee0067a040137ddf09a38f1d4c41c Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 7 Jul 2026 05:54:17 -0500 Subject: [PATCH] fix(db): repair responsible user migration timestamps (#9146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The database migration layer keeps local and deployed instances moving forward safely as schema/data contracts evolve. > - The responsible-user backfill migration could make historical issues look newly updated by bumping user-visible `updated_at` columns during a backfill. > - That timestamp churn can invalidate inbox/archive state and make old work appear fresh even though no user-facing activity happened. > - This pull request moves the responsible-user invariant migration later in the current migration sequence, keeps it from touching user-visible timestamps, and adds a repair sweep for databases that already saw the timestamp bump. > - The benefit is safer migration replay and regression coverage for future backfills that might otherwise mutate visible timestamps. ## Linked Issues or Issue Description No public GitHub issue exists. Inline bug report: **Pre-submission checklist** - [x] I have searched existing open and closed issues and this is not a duplicate. - [x] I am on the latest released version of Paperclip (or can reproduce on `master`). - [x] I have confirmed the error originates in Paperclip itself — not in my agent adapter, API provider, or local configuration. **What happened?** A responsible-user migration backfill could update user-visible `updated_at` columns while filling missing responsible-user data. That makes historical issues/runs/routines appear newer even when no user-facing activity happened. **Expected behavior** Responsible-user backfills should populate ownership metadata without mutating user-visible recency fields, and databases already affected by a timestamp sweep should be repairable. **Steps to reproduce** 1. Start from current `master` with the responsible-user migration sequence. 2. Apply migrations to a database containing historical issues, runs, routines, and companies with older activity timestamps. 3. Replay the responsible-user invariant migration and inspect user-visible `updated_at` values. **Paperclip version or commit** `master` at the PR base. **Deployment mode** Local dev (`pnpm dev`) and deployed instances using the same migrations. **Installation method** Built from source (`pnpm dev` / `pnpm build`). **Agent adapter(s) involved** - [x] Not adapter-specific (core bug) **Database mode** External Postgres and embedded development Postgres migration paths. **Access context** Board and agent-visible issue recency can both be affected. **Relevant logs or output** Covered by the added embedded-Postgres regression tests. **Relevant config (if applicable)** Not applicable. **Additional context** This PR adapts an extracted local migration fix onto the current master migration sequence, where `0133` is already occupied. **Privacy checklist** - [x] I have reviewed all pasted output for PII (usernames, file paths, API keys, tokens, company names) and redacted where necessary. ## What Changed - Renumbered the responsible-user invariant migration onto the current master migration sequence. - Added a repair migration that detects broad timestamp sweeps and restores safer `updated_at` values for issues, heartbeat runs, routines, routine runs, and companies. - Added focused embedded-Postgres regression coverage for the relocated migration, repair migration, and updated-at backfill allowlist. ## Verification - `pnpm --filter @paperclipai/db run check:migrations` - `/srv/paperclip/home/paperclipai/paperclip/node_modules/.bin/vitest run packages/db/src/client.test.ts -t "migration 0134|migration 0135|unallowlisted migration backfills"` ## Risks Migration behavior is the main risk. The PR intentionally changes the active migration sequence by removing the old responsible-user invariant slot and replaying that work later with a repair migration. Reviewers should confirm this matches the intended release/migration policy for installations that may already have applied the earlier migration. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex, GPT-5.5 coding agent with repository tool use and local shell execution. Context window was not surfaced by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- packages/db/src/client.test.ts | 406 +++++++++++++++++- ...> 0134_run_responsible_user_invariant.sql} | 0 ..._run_responsible_user_updated_at_sweep.sql | 84 ++++ packages/db/src/migrations/meta/_journal.json | 21 +- 4 files changed, 501 insertions(+), 10 deletions(-) rename packages/db/src/migrations/{0130_run_responsible_user_invariant.sql => 0134_run_responsible_user_invariant.sql} (100%) create mode 100644 packages/db/src/migrations/0135_repair_run_responsible_user_updated_at_sweep.sql diff --git a/packages/db/src/client.test.ts b/packages/db/src/client.test.ts index fc1837962f..5a5a82697a 100644 --- a/packages/db/src/client.test.ts +++ b/packages/db/src/client.test.ts @@ -47,6 +47,10 @@ const migrationUpdatedAtUpdateAllowlist = new Map>([ "0131_repair_run_responsible_user_context_refs.sql", new Set(["heartbeat_runs"]), ], + [ + "0135_repair_run_responsible_user_updated_at_sweep.sql", + new Set(["companies", "heartbeat_runs", "issues", "routine_runs", "routines"]), + ], ]); function findUserVisibleUpdatedAtBackfillViolations( @@ -614,7 +618,7 @@ describeEmbeddedPostgres("applyPendingMigrations", () => { ); it( - "replays migration 0130 without bumping issue updated_at for inbox archives", + "replays migration 0134 without bumping issue updated_at for inbox archives", async () => { const connectionString = await createTempDatabase(); @@ -623,7 +627,7 @@ describeEmbeddedPostgres("applyPendingMigrations", () => { const sql = postgres(connectionString, { max: 1, onnotice: () => {} }); try { const runResponsibleUserHash = await migrationHash( - "0130_run_responsible_user_invariant.sql", + "0134_run_responsible_user_invariant.sql", ); await sql.unsafe(` @@ -708,7 +712,7 @@ describeEmbeddedPostgres("applyPendingMigrations", () => { const pendingState = await inspectMigrations(connectionString); expect(pendingState).toMatchObject({ status: "needsMigrations", - pendingMigrations: ["0130_run_responsible_user_invariant.sql"], + pendingMigrations: ["0134_run_responsible_user_invariant.sql"], reason: "pending-migrations", }); @@ -746,6 +750,402 @@ describeEmbeddedPostgres("applyPendingMigrations", () => { 20_000, ); + it( + "replays migration 0135 to repair updated_at sweeps and no-op when clean", + async () => { + const connectionString = await createTempDatabase(); + + await applyPendingMigrations(connectionString); + + const repairSweepHash = await migrationHash( + "0135_repair_run_responsible_user_updated_at_sweep.sql", + ); + const sql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + await sql.unsafe(` + INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000240', + 'Clean Migration Co', + 'CLN134', + '2026-04-01T09:00:00.000Z', + '2026-04-02T09:00:00.000Z' + ) + `); + await sql.unsafe(` + INSERT INTO "issues" ("id", "company_id", "title", "status", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000241', + '00000000-0000-0000-0000-000000000240', + 'Clean issue should not be touched', + 'todo', + '2026-04-01T10:00:00.000Z', + '2026-04-02T10:00:00.000Z' + ) + `); + await sql.unsafe( + `DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${repairSweepHash}'`, + ); + } finally { + await sql.end(); + } + + await applyPendingMigrations(connectionString); + + const afterCleanReplay = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const cleanRows = await afterCleanReplay.unsafe<{ updated_at: Date }[]>(` + SELECT "updated_at" + FROM "issues" + WHERE "id" = '00000000-0000-0000-0000-000000000241' + `); + expect(cleanRows[0]?.updated_at.toISOString()).toBe("2026-04-02T10:00:00.000Z"); + + await afterCleanReplay.unsafe(` + INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000250', + 'Sweep Migration Co', + 'SWP134', + '2026-01-01T00:00:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "agents" ("id", "company_id", "name", "role", "adapter_type", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000251', + '00000000-0000-0000-0000-000000000250', + 'Sweep Agent', + 'general', + 'process', + '2026-01-02T00:00:00.000Z', + '2026-01-02T00:00:00.000Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "issues" ("id", "company_id", "title", "status", "created_at", "updated_at") + SELECT + ('10000000-0000-0000-0000-' || lpad(gs::text, 12, '0'))::uuid, + '00000000-0000-0000-0000-000000000250', + 'Swept issue ' || gs::text, + 'todo', + '2026-02-01T00:00:00.000Z'::timestamptz + (gs::text || ' minutes')::interval, + '2026-04-03T12:00:00.123456Z' + FROM generate_series(1, 101) AS gs + `); + await afterCleanReplay.unsafe(` + UPDATE "issues" + SET + "status" = 'done', + "completed_at" = '2026-04-03T12:00:00.123456Z' + WHERE "id" = '10000000-0000-0000-0000-000000000003' + `); + await afterCleanReplay.unsafe(` + INSERT INTO "issue_comments" ("id", "company_id", "issue_id", "body", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000252', + '00000000-0000-0000-0000-000000000250', + '10000000-0000-0000-0000-000000000001', + 'Latest pre-sweep activity', + '2026-03-01T15:30:00.000Z', + '2026-03-02T16:45:00.000Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "heartbeat_runs" ( + "id", + "company_id", + "agent_id", + "status", + "started_at", + "finished_at", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000253', + '00000000-0000-0000-0000-000000000250', + '00000000-0000-0000-0000-000000000251', + 'completed', + '2026-02-10T10:00:00.000Z', + '2026-02-10T10:30:00.000Z', + '2026-02-10T09:55:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "heartbeat_runs" ( + "id", + "company_id", + "agent_id", + "status", + "started_at", + "last_output_at", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000256', + '00000000-0000-0000-0000-000000000250', + '00000000-0000-0000-0000-000000000251', + 'running', + '2026-02-10T11:00:00.000Z', + '2026-04-03T12:00:00.123456Z', + '2026-02-10T10:55:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "routines" ( + "id", + "company_id", + "title", + "last_triggered_at", + "last_enqueued_at", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000254', + '00000000-0000-0000-0000-000000000250', + 'Swept routine', + '2026-03-20T10:00:00.000Z', + '2026-03-21T11:00:00.000Z', + '2026-02-11T00:00:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "routines" ( + "id", + "company_id", + "title", + "last_triggered_at", + "last_enqueued_at", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000257', + '00000000-0000-0000-0000-000000000250', + 'Same-timestamp active routine', + '2026-03-20T10:00:00.000Z', + '2026-04-03T12:00:00.123456Z', + '2026-02-11T00:00:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "routine_runs" ( + "id", + "company_id", + "routine_id", + "source", + "status", + "completed_at", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000255', + '00000000-0000-0000-0000-000000000250', + '00000000-0000-0000-0000-000000000254', + 'schedule', + 'completed', + '2026-02-12T12:00:00.000Z', + '2026-02-12T11:00:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "routine_runs" ( + "id", + "company_id", + "routine_id", + "source", + "status", + "triggered_at", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000258', + '00000000-0000-0000-0000-000000000250', + '00000000-0000-0000-0000-000000000257', + 'schedule', + 'running', + '2026-04-03T12:00:00.123456Z', + '2026-02-12T13:00:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000260', + 'Coincident Timestamp Co', + 'CTS134', + '2026-01-05T00:00:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "agents" ("id", "company_id", "name", "role", "adapter_type", "created_at", "updated_at") + VALUES ( + '00000000-0000-0000-0000-000000000261', + '00000000-0000-0000-0000-000000000260', + 'Coincident Agent', + 'general', + 'process', + '2026-01-05T00:10:00.000Z', + '2026-01-05T00:10:00.000Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "issues" ("id", "company_id", "title", "status", "created_at", "updated_at") + VALUES ( + '20000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000260', + 'Coincident timestamp issue should not be touched', + 'todo', + '2026-02-05T00:00:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe(` + INSERT INTO "heartbeat_runs" ( + "id", + "company_id", + "agent_id", + "status", + "started_at", + "finished_at", + "created_at", + "updated_at" + ) + VALUES ( + '00000000-0000-0000-0000-000000000262', + '00000000-0000-0000-0000-000000000260', + '00000000-0000-0000-0000-000000000261', + 'completed', + '2026-02-05T10:00:00.000Z', + '2026-02-05T10:30:00.000Z', + '2026-02-05T09:55:00.000Z', + '2026-04-03T12:00:00.123456Z' + ) + `); + await afterCleanReplay.unsafe( + `DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${repairSweepHash}'`, + ); + } finally { + await afterCleanReplay.end(); + } + + await applyPendingMigrations(connectionString); + + const afterRepair = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const repairedRows = await afterRepair.unsafe<{ + subject: string; + updated_at: Date; + }[]>(` + SELECT 'company' AS subject, "updated_at" + FROM "companies" + WHERE "id" = '00000000-0000-0000-0000-000000000250' + UNION ALL + SELECT 'issue_with_comment' AS subject, "updated_at" + FROM "issues" + WHERE "id" = '10000000-0000-0000-0000-000000000001' + UNION ALL + SELECT 'issue_without_comment' AS subject, "updated_at" + FROM "issues" + WHERE "id" = '10000000-0000-0000-0000-000000000002' + UNION ALL + SELECT 'issue_with_state_activity' AS subject, "updated_at" + FROM "issues" + WHERE "id" = '10000000-0000-0000-0000-000000000003' + UNION ALL + SELECT 'heartbeat_run' AS subject, "updated_at" + FROM "heartbeat_runs" + WHERE "id" = '00000000-0000-0000-0000-000000000253' + UNION ALL + SELECT 'heartbeat_run_with_output' AS subject, "updated_at" + FROM "heartbeat_runs" + WHERE "id" = '00000000-0000-0000-0000-000000000256' + UNION ALL + SELECT 'other_company' AS subject, "updated_at" + FROM "companies" + WHERE "id" = '00000000-0000-0000-0000-000000000260' + UNION ALL + SELECT 'other_heartbeat_run' AS subject, "updated_at" + FROM "heartbeat_runs" + WHERE "id" = '00000000-0000-0000-0000-000000000262' + UNION ALL + SELECT 'other_issue' AS subject, "updated_at" + FROM "issues" + WHERE "id" = '20000000-0000-0000-0000-000000000001' + UNION ALL + SELECT 'routine' AS subject, "updated_at" + FROM "routines" + WHERE "id" = '00000000-0000-0000-0000-000000000254' + UNION ALL + SELECT 'routine_with_activity' AS subject, "updated_at" + FROM "routines" + WHERE "id" = '00000000-0000-0000-0000-000000000257' + UNION ALL + SELECT 'routine_run' AS subject, "updated_at" + FROM "routine_runs" + WHERE "id" = '00000000-0000-0000-0000-000000000255' + UNION ALL + SELECT 'routine_run_with_trigger' AS subject, "updated_at" + FROM "routine_runs" + WHERE "id" = '00000000-0000-0000-0000-000000000258' + ORDER BY subject + `); + const repaired = Object.fromEntries( + repairedRows.map((row) => [row.subject, row.updated_at.toISOString()]), + ); + expect(repaired).toEqual({ + company: "2026-01-01T00:00:00.000Z", + heartbeat_run: "2026-02-10T10:30:00.000Z", + heartbeat_run_with_output: "2026-04-03T12:00:00.123Z", + issue_with_comment: "2026-03-02T16:45:00.000Z", + issue_with_state_activity: "2026-04-03T12:00:00.123Z", + issue_without_comment: "2026-02-01T00:02:00.000Z", + other_company: "2026-04-03T12:00:00.123Z", + other_heartbeat_run: "2026-04-03T12:00:00.123Z", + other_issue: "2026-04-03T12:00:00.123Z", + routine: "2026-03-21T11:00:00.000Z", + routine_run_with_trigger: "2026-04-03T12:00:00.123Z", + routine_with_activity: "2026-04-03T12:00:00.123Z", + routine_run: "2026-02-12T12:00:00.000Z", + }); + + await afterRepair.unsafe( + `DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${repairSweepHash}'`, + ); + } finally { + await afterRepair.end(); + } + + await applyPendingMigrations(connectionString); + + const afterSecondRun = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const secondRunRows = await afterSecondRun.unsafe<{ updated_at: Date }[]>(` + SELECT "updated_at" + FROM "issues" + WHERE "id" = '10000000-0000-0000-0000-000000000001' + `); + expect(secondRunRows[0]?.updated_at.toISOString()).toBe("2026-03-02T16:45:00.000Z"); + } finally { + await afterSecondRun.end(); + } + }, + 20_000, + ); + it( "replays the run responsible user repair migration when heartbeat run issue refs are identifiers", async () => { diff --git a/packages/db/src/migrations/0130_run_responsible_user_invariant.sql b/packages/db/src/migrations/0134_run_responsible_user_invariant.sql similarity index 100% rename from packages/db/src/migrations/0130_run_responsible_user_invariant.sql rename to packages/db/src/migrations/0134_run_responsible_user_invariant.sql diff --git a/packages/db/src/migrations/0135_repair_run_responsible_user_updated_at_sweep.sql b/packages/db/src/migrations/0135_repair_run_responsible_user_updated_at_sweep.sql new file mode 100644 index 0000000000..b346dd1ea8 --- /dev/null +++ b/packages/db/src/migrations/0135_repair_run_responsible_user_updated_at_sweep.sql @@ -0,0 +1,84 @@ +DROP TABLE IF EXISTS "run_responsible_user_updated_at_sweeps"; +--> statement-breakpoint +CREATE TEMP TABLE "run_responsible_user_updated_at_sweeps" ON COMMIT DROP AS +SELECT i."company_id", i."updated_at" AS "sweep_at" +FROM "issues" AS i +WHERE EXISTS ( + SELECT 1 + FROM "heartbeat_runs" AS h + WHERE h."company_id" = i."company_id" + AND h."updated_at" = i."updated_at" + ) + AND EXISTS ( + SELECT 1 + FROM "companies" AS c + WHERE c."id" = i."company_id" + AND c."updated_at" = i."updated_at" + ) +GROUP BY i."company_id", i."updated_at" +HAVING count(*) > 100; +--> statement-breakpoint +UPDATE "issues" AS i +SET "updated_at" = GREATEST( + i."created_at", + COALESCE(CASE WHEN i."started_at" <= sweep."sweep_at" THEN i."started_at" END, i."created_at"), + COALESCE(CASE WHEN i."completed_at" <= sweep."sweep_at" THEN i."completed_at" END, i."created_at"), + COALESCE(CASE WHEN i."cancelled_at" <= sweep."sweep_at" THEN i."cancelled_at" END, i."created_at"), + COALESCE(CASE WHEN i."monitor_wake_requested_at" <= sweep."sweep_at" THEN i."monitor_wake_requested_at" END, i."created_at"), + COALESCE(CASE WHEN i."monitor_last_triggered_at" <= sweep."sweep_at" THEN i."monitor_last_triggered_at" END, i."created_at"), + COALESCE( + ( + SELECT max(GREATEST(c."created_at", c."updated_at")) + FROM "issue_comments" AS c + WHERE c."company_id" = i."company_id" + AND c."issue_id" = i."id" + AND c."created_at" <= sweep."sweep_at" + AND c."updated_at" <= sweep."sweep_at" + ), + i."created_at" + ) +) +FROM "run_responsible_user_updated_at_sweeps" AS sweep +WHERE i."company_id" = sweep."company_id" + AND i."updated_at" = sweep."sweep_at"; +--> statement-breakpoint +UPDATE "heartbeat_runs" AS h +SET "updated_at" = GREATEST( + h."created_at", + COALESCE(CASE WHEN h."started_at" <= sweep."sweep_at" THEN h."started_at" END, h."created_at"), + COALESCE(CASE WHEN h."process_started_at" <= sweep."sweep_at" THEN h."process_started_at" END, h."created_at"), + COALESCE(CASE WHEN h."last_output_at" <= sweep."sweep_at" THEN h."last_output_at" END, h."created_at"), + COALESCE(CASE WHEN h."scheduled_retry_at" <= sweep."sweep_at" THEN h."scheduled_retry_at" END, h."created_at"), + COALESCE(CASE WHEN h."finished_at" <= sweep."sweep_at" THEN h."finished_at" END, h."created_at") +) +FROM "run_responsible_user_updated_at_sweeps" AS sweep +WHERE h."company_id" = sweep."company_id" + AND h."updated_at" = sweep."sweep_at"; +--> statement-breakpoint +UPDATE "routine_runs" AS rr +SET "updated_at" = GREATEST( + rr."created_at", + COALESCE(CASE WHEN rr."triggered_at" <= sweep."sweep_at" THEN rr."triggered_at" END, rr."created_at"), + COALESCE(CASE WHEN rr."completed_at" <= sweep."sweep_at" THEN rr."completed_at" END, rr."created_at") +) +FROM "run_responsible_user_updated_at_sweeps" AS sweep +WHERE rr."company_id" = sweep."company_id" + AND rr."updated_at" = sweep."sweep_at"; +--> statement-breakpoint +UPDATE "routines" AS r +SET "updated_at" = GREATEST( + r."created_at", + COALESCE(CASE WHEN r."last_triggered_at" <= sweep."sweep_at" THEN r."last_triggered_at" END, r."created_at"), + COALESCE(CASE WHEN r."last_enqueued_at" <= sweep."sweep_at" THEN r."last_enqueued_at" END, r."created_at") +) +FROM "run_responsible_user_updated_at_sweeps" AS sweep +WHERE r."company_id" = sweep."company_id" + AND r."updated_at" = sweep."sweep_at"; +--> statement-breakpoint +UPDATE "companies" AS c +SET "updated_at" = c."created_at" +FROM "run_responsible_user_updated_at_sweeps" AS sweep +WHERE c."id" = sweep."company_id" + AND c."updated_at" = sweep."sweep_at"; +--> statement-breakpoint +DROP TABLE IF EXISTS "run_responsible_user_updated_at_sweeps"; diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 0152df8813..5118110ea8 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -905,13 +905,6 @@ "tag": "0129_agent_api_key_responsible_user", "breakpoints": true }, - { - "idx": 130, - "version": "7", - "when": 1783025224120, - "tag": "0130_run_responsible_user_invariant", - "breakpoints": true - }, { "idx": 131, "version": "7", @@ -932,6 +925,20 @@ "when": 1783034521000, "tag": "0133_resource_membership_stars", "breakpoints": true + }, + { + "idx": 134, + "version": "7", + "when": 1783025624120, + "tag": "0134_run_responsible_user_invariant", + "breakpoints": true + }, + { + "idx": 135, + "version": "7", + "when": 1783025724120, + "tag": "0135_repair_run_responsible_user_updated_at_sweep", + "breakpoints": true } ] }