fix(db): repair responsible user migration timestamps (#9146)
## 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 <noreply@paperclip.ing>
This commit is contained in:
parent
696c694a58
commit
0f08c2b526
|
|
@ -47,6 +47,10 @@ const migrationUpdatedAtUpdateAllowlist = new Map<string, ReadonlySet<string>>([
|
|||
"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 () => {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue