Skip to content

Commit 7e0418a

Browse files
fix(server): drop V1 projections nothing reads after the V2 upgrade (pingdotgg#20947)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 25159b6 commit 7e0418a

6 files changed

Lines changed: 76 additions & 91 deletions

File tree

‎apps/server/src/persistence/Migrations.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import Migration0059 from "./Migrations/059_McpAppModelContext.ts";
6565
import Migration0060 from "./Migrations/060_ThreadSnapshotWindowIndexes.ts";
6666
import Migration0061 from "./Migrations/061_PullRequestReviewGuides.ts";
6767
import Migration0062 from "./Migrations/062_ThreadFailureLookupIndex.ts";
68+
import Migration0063 from "./Migrations/063_DropUnusedV1Projections.ts";
6869

6970
export const migrationEntries = [
7071
[1, "OrchestrationEvents", Migration0001],
@@ -130,6 +131,7 @@ export const migrationEntries = [
130131
[60, "ThreadSnapshotWindowIndexes", Migration0060],
131132
[61, "PullRequestReviewGuides", Migration0061],
132133
[62, "ThreadFailureLookupIndex", Migration0062],
134+
[63, "DropUnusedV1Projections", Migration0063],
133135
] as const;
134136

135137
export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const);

‎apps/server/src/persistence/Migrations/055_OrchestrationV2.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ layer("055_OrchestrationV2", (it) => {
1313
Effect.sync(() => {
1414
assert.deepStrictEqual(
1515
migrationEntries.map(([id]) => id),
16-
Array.from({ length: 62 }, (_, index) => index + 1),
16+
Array.from({ length: 63 }, (_, index) => index + 1),
1717
);
1818
}),
1919
);
@@ -34,6 +34,7 @@ layer("055_OrchestrationV2", (it) => {
3434
[60, "ThreadSnapshotWindowIndexes"],
3535
[61, "PullRequestReviewGuides"],
3636
[62, "ThreadFailureLookupIndex"],
37+
[63, "DropUnusedV1Projections"],
3738
]);
3839
assert.deepStrictEqual(yield* runMigrations(), []);
3940

@@ -62,6 +63,7 @@ layer("055_OrchestrationV2", (it) => {
6263
{ migration_id: 60, name: "ThreadSnapshotWindowIndexes" },
6364
{ migration_id: 61, name: "PullRequestReviewGuides" },
6465
{ migration_id: 62, name: "ThreadFailureLookupIndex" },
66+
{ migration_id: 63, name: "DropUnusedV1Projections" },
6567
]);
6668

6769
const tables = yield* sql<{ readonly name: string }>`
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { assert, it } from "@effect/vitest";
2+
import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
3+
import * as Effect from "effect/Effect";
4+
import * as SqlClient from "effect/sql/SqlClient";
5+
6+
import { runMigrations } from "../Migrations.ts";
7+
8+
const layer = it.layer(NodeSqliteClient.layer({ filename: ":memory:" }));
9+
10+
layer("063_DropUnusedV1Projections", (it) => {
11+
it.effect("drops V1 projections nothing reads and keeps the transcript import sources", () =>
12+
Effect.gen(function* () {
13+
const sql = yield* SqlClient.SqlClient;
14+
yield* runMigrations({ toMigrationInclusive: 62 });
15+
yield* sql`
16+
INSERT INTO projection_thread_messages
17+
(message_id, thread_id, role, text, is_streaming, created_at, updated_at)
18+
VALUES ('message-1', 'thread-1', 'user', 'hello', 0, '2026-01-01', '2026-01-01')
19+
`;
20+
21+
yield* runMigrations({ toMigrationInclusive: 63 });
22+
23+
const rows = yield* sql<{ readonly name: string }>`
24+
SELECT name FROM sqlite_master WHERE type = 'table'
25+
`;
26+
const tables = new Set(rows.map((row) => row.name));
27+
for (const dropped of [
28+
"projection_thread_activities",
29+
"projection_turns",
30+
"projection_thread_proposed_plans",
31+
"projection_pending_approvals",
32+
"projection_state",
33+
"checkpoint_diff_blobs",
34+
]) {
35+
assert.isFalse(tables.has(dropped), dropped);
36+
}
37+
for (const kept of [
38+
"projection_threads",
39+
"projection_thread_messages",
40+
"projection_thread_pull_requests",
41+
]) {
42+
assert.isTrue(tables.has(kept), kept);
43+
}
44+
assert.deepEqual(yield* sql`SELECT message_id FROM projection_thread_messages`, [
45+
{ message_id: "message-1" },
46+
]);
47+
}),
48+
);
49+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Effect from "effect/Effect";
2+
import * as SqlClient from "effect/sql/SqlClient";
3+
4+
/**
5+
* V2 databases start as a copy of the V1 database, and nothing reads these V1
6+
* projections after the copy. The V1 transcript import still reads threads,
7+
* messages and pull request links, so those stay. Builds before 0.1.0 read
8+
* `state.sqlite`, never this file, so dropping them does not affect a rollback.
9+
* The freed pages are reported as reclaimable space for `t3 doctor`.
10+
*/
11+
export default Effect.gen(function* () {
12+
const sql = yield* SqlClient.SqlClient;
13+
14+
yield* sql`DROP TABLE IF EXISTS projection_thread_activities`;
15+
yield* sql`DROP TABLE IF EXISTS projection_turns`;
16+
yield* sql`DROP TABLE IF EXISTS projection_thread_proposed_plans`;
17+
yield* sql`DROP TABLE IF EXISTS projection_pending_approvals`;
18+
yield* sql`DROP TABLE IF EXISTS projection_state`;
19+
yield* sql`DROP TABLE IF EXISTS checkpoint_diff_blobs`;
20+
});

‎apps/server/src/persistence/reconcileV2PreviewMigration.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe("V2 preview upgrade", () => {
4242
[60, "ThreadSnapshotWindowIndexes"],
4343
[61, "PullRequestReviewGuides"],
4444
[62, "ThreadFailureLookupIndex"],
45+
[63, "DropUnusedV1Projections"],
4546
]);
4647
assert.deepStrictEqual(yield* runMigrations(), []);
4748
assert.deepStrictEqual(yield* sql`SELECT * FROM orchestration_v2_legacy_imports`, imports);
@@ -127,6 +128,7 @@ describe("V2 preview upgrade", () => {
127128
[60, "ThreadSnapshotWindowIndexes"],
128129
[61, "PullRequestReviewGuides"],
129130
[62, "ThreadFailureLookupIndex"],
131+
[63, "DropUnusedV1Projections"],
130132
]);
131133
}).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
132134
);

‎scripts/mobile-showcase-environment.ts‎

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,6 @@ export const SHOWCASE_SCENES = [
2121
] as const;
2222
export type ShowcaseScene = (typeof SHOWCASE_SCENES)[number];
2323

24-
const PROJECTOR_NAMES = [
25-
"projection.projects",
26-
"projection.threads",
27-
"projection.thread-messages",
28-
"projection.thread-proposed-plans",
29-
"projection.thread-activities",
30-
"projection.thread-sessions",
31-
"projection.thread-turns",
32-
"projection.checkpoints",
33-
"projection.pending-approvals",
34-
] as const;
35-
3624
const MODEL_SELECTION = JSON.stringify({ instanceId: "codex", model: "gpt-5.4" });
3725
const PROJECT_SCRIPTS = JSON.stringify([
3826
{
@@ -392,23 +380,6 @@ function insertThread(
392380
snoozedUntil,
393381
snoozedAt,
394382
);
395-
database
396-
.prepare(
397-
`INSERT INTO projection_turns (
398-
thread_id, turn_id, pending_message_id, assistant_message_id, state, requested_at,
399-
started_at, completed_at, checkpoint_turn_count, checkpoint_ref, checkpoint_status,
400-
checkpoint_files_json, source_proposed_plan_thread_id, source_proposed_plan_id
401-
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, NULL, NULL, NULL, '[]', NULL, NULL)`,
402-
)
403-
.run(
404-
input.id,
405-
turnId,
406-
isWorking ? null : `${input.id}-answer`,
407-
isWorking ? "running" : "completed",
408-
minutesBefore(now, input.minutesAgo + 2),
409-
minutesBefore(now, input.minutesAgo + 2),
410-
isWorking ? null : updatedAt,
411-
);
412383
database
413384
.prepare(
414385
`INSERT INTO projection_thread_sessions (
@@ -424,14 +395,9 @@ function insertThread(
424395
// do not read the V1 thread rows; moving the seed to V2 is tracked in
425396
// https://github.com/pingdotgg/t3code/issues/15013.
426397
const SEEDED_V1_TABLES = [
427-
"projection_pending_approvals",
428-
"projection_thread_proposed_plans",
429-
"projection_thread_activities",
430398
"projection_thread_messages",
431399
"projection_thread_sessions",
432-
"projection_turns",
433400
"projection_threads",
434-
"projection_state",
435401
] as const;
436402

437403
const SEEDED_PROJECTION_TABLES = [...SEEDED_V1_TABLES, "projection_projects"] as const;
@@ -558,62 +524,6 @@ function seedDatabase(
558524
}
559525
}
560526

561-
const turnId = `${SHOWCASE_THREAD_ID}-turn`;
562-
const insertActivity = database.prepare(
563-
`INSERT INTO projection_thread_activities (
564-
activity_id, thread_id, turn_id, tone, kind, summary, payload_json, sequence, created_at
565-
) VALUES (?, ?, ?, 'tool', 'tool.completed', ?, ?, ?, ?)`,
566-
);
567-
insertActivity.run(
568-
"trace-remote-handoff",
569-
SHOWCASE_THREAD_ID,
570-
turnId,
571-
"Traced the remote handoff path",
572-
JSON.stringify({
573-
itemType: "command_execution",
574-
title: "Traced the remote handoff path",
575-
detail: "Three environments, one continuous workspace",
576-
status: "completed",
577-
}),
578-
1,
579-
minutesBefore(now, 8),
580-
);
581-
insertActivity.run(
582-
"sync-command-center",
583-
SHOWCASE_THREAD_ID,
584-
turnId,
585-
"Synced the command center",
586-
JSON.stringify({
587-
itemType: "file_change",
588-
title: "Synced the command center",
589-
detail: "2 files changed · instant handoffs · calm reconnects",
590-
status: "completed",
591-
}),
592-
2,
593-
minutesBefore(now, 6),
594-
);
595-
insertActivity.run(
596-
"run-changed-suite",
597-
SHOWCASE_THREAD_ID,
598-
turnId,
599-
"Ran the changed workspace",
600-
JSON.stringify({
601-
itemType: "command_execution",
602-
title: "Ran the changed workspace",
603-
detail: "612 tests passed · 3 environments online",
604-
status: "completed",
605-
}),
606-
3,
607-
minutesBefore(now, 4),
608-
);
609-
610-
for (const [index, projector] of PROJECTOR_NAMES.entries()) {
611-
database
612-
.prepare(
613-
"INSERT INTO projection_state (projector, last_applied_sequence, updated_at) VALUES (?, ?, ?)",
614-
)
615-
.run(projector, index + 1, minutesBefore(now, 1));
616-
}
617527
database.exec("COMMIT");
618528
} catch (error) {
619529
// A failed BEGIN (or an error SQLite already auto-rolled back) leaves no

0 commit comments

Comments
 (0)