History

simplify migrations

Julius Marminge committed 7 months ago

Commit c44d17f1 parent e12d2906 files · +234 −265Browse files

Unified · Split

6 files
  1. apps/server/src/persistence/Migrations.ts +2 −6
  2. apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts +182 −0
  3. apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.ts +50 −13
  4. apps/server/src/persistence/Migrations/016_ProjectionProviders.ts +0 −38
  5. apps/server/src/persistence/Migrations/017_ProjectionModelSelectionOptions.ts +0 −28
  6. apps/server/src/persistence/Migrations/018_CanonicalizeLegacyModelSelections.test.ts +0 −180
apps/server/src/persistence/Migrations.ts+2−6
@@ -27,9 +27,7 @@ import Migration0012 from "./Migrations/012_ProjectionThreadsInteractionMode.ts" · 26 unmodified lines
2727 import Migration0013 from "./Migrations/013_ProjectionThreadProposedPlans.ts";
2828 import Migration0014 from "./Migrations/014_ProjectionThreadProposedPlanImplementation.ts";
2929 import Migration0015 from "./Migrations/015_ProjectionTurnsSourceProposedPlan.ts";
30−import Migration0016 from "./Migrations/016_ProjectionProviders.ts";
31−import Migration0017 from "./Migrations/017_ProjectionModelSelectionOptions.ts";
32−import Migration0018 from "./Migrations/018_CanonicalizeLegacyModelSelections.ts";
30+import Migration0016 from "./Migrations/016_CanonicalizeModelSelections.ts";
3331 import { Effect } from "effect";
3432
3533 /**
@@ -58,9 +56,7 @@ const loader = Migrator.fromRecord({ · 22 unmodified lines
5856 "13_ProjectionThreadProposedPlans": Migration0013,
5957 "14_ProjectionThreadProposedPlanImplementation": Migration0014,
6058 "15_ProjectionTurnsSourceProposedPlan": Migration0015,
61− "16_ProjectionProviders": Migration0016,
62− "17_ProjectionModelSelectionOptions": Migration0017,
63− "18_CanonicalizeLegacyModelSelections": Migration0018,
59+ "16_CanonicalizeModelSelections": Migration0016,
6460 });
6561
6662 /**
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts+182−0
@@ -0,0 +1,182 @@
1+import { assert, it } from "@effect/vitest";
2+import { Effect, Layer } from "effect";
3+import * as SqlClient from "effect/unstable/sql/SqlClient";
4+
5+import Migration0016 from "./016_CanonicalizeModelSelections.ts";
6+import * as NodeSqliteClient from "../NodeSqliteClient.ts";
7+
8+const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
9+
10+layer("016_CanonicalizeModelSelections", (it) => {
11+ it.effect(
12+ "migrates legacy projection rows and event payloads to the canonical model-selection shape",
13+ () =>
14+ Effect.gen(function* () {
15+ const sql = yield* SqlClient.SqlClient;
16+
17+ yield* sql`
18+ CREATE TABLE projection_projects (
19+ project_id TEXT PRIMARY KEY,
20+ default_model TEXT
21+ )
22+ `;
23+ yield* sql`
24+ CREATE TABLE projection_threads (
25+ thread_id TEXT PRIMARY KEY,
26+ model TEXT NOT NULL
27+ )
28+ `;
29+ yield* sql`
30+ CREATE TABLE projection_thread_sessions (
31+ thread_id TEXT PRIMARY KEY,
32+ provider_name TEXT
33+ )
34+ `;
35+ yield* sql`
36+ CREATE TABLE orchestration_events (
37+ event_type TEXT NOT NULL,
38+ payload_json TEXT NOT NULL
39+ )
40+ `;
41+
42+ yield* sql`
43+ INSERT INTO projection_projects (project_id, default_model)
44+ VALUES
45+ ('project-codex', 'gpt-5.4'),
46+ ('project-claude', 'claude-sonnet-4-6'),
47+ ('project-null', NULL)
48+ `;
49+ yield* sql`
50+ UPDATE projection_projects
51+ SET default_model = 'claude-opus-4-6'
52+ WHERE project_id = 'project-claude'
53+ `;
54+ yield* sql`
55+ INSERT INTO projection_threads (thread_id, model)
56+ VALUES
57+ ('thread-session', 'gpt-5.4'),
58+ ('thread-claude', 'claude-opus-4-6'),
59+ ('thread-codex', 'gpt-5.4'),
60+ ('thread-legacy-options', 'claude-opus-4-6')
61+ `;
62+ yield* sql`
63+ INSERT INTO projection_thread_sessions (thread_id, provider_name)
64+ VALUES ('thread-session', 'claudeAgent')
65+ `;
66+ yield* sql`
67+ INSERT INTO orchestration_events (
68+ event_type,
69+ payload_json
70+ )
71+ VALUES
72+ (
73+ 'project.created',
74+ '{"projectId":"project-1","title":"Project","workspaceRoot":"/tmp/project","defaultModel":"claude-opus-4-6","defaultModelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max"}},"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}'
75+ ),
76+ (
77+ 'thread.created',
78+ '{"threadId":"thread-1","projectId":"project-1","title":"Thread","model":"claude-opus-4-6","modelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max","thinking":false}},"runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}'
79+ ),
80+ (
81+ 'thread.turn-start-requested',
82+ '{"threadId":"thread-1","turnId":"turn-1","input":"hi","model":"gpt-5.4","modelOptions":{"codex":{"fastMode":true},"claudeAgent":{"effort":"max"}},"deliveryMode":"buffered"}'
83+ )
84+ `;
85+
86+ yield* Migration0016;
87+
88+ const projectRows = yield* sql<{
89+ readonly projectId: string;
90+ readonly defaultProvider: string | null;
91+ }>`
92+ SELECT
93+ project_id AS "projectId",
94+ default_provider AS "defaultProvider"
95+ FROM projection_projects
96+ ORDER BY project_id
97+ `;
98+ assert.deepStrictEqual(projectRows, [
99+ { projectId: "project-claude", defaultProvider: "claudeAgent" },
100+ { projectId: "project-codex", defaultProvider: "codex" },
101+ { projectId: "project-null", defaultProvider: null },
102+ ]);
103+
104+ const threadRows = yield* sql<{
105+ readonly threadId: string;
106+ readonly provider: string | null;
107+ readonly modelOptions: string | null;
108+ }>`
109+ SELECT
110+ thread_id AS "threadId",
111+ provider,
112+ model_options_json AS "modelOptions"
113+ FROM projection_threads
114+ ORDER BY thread_id
115+ `;
116+ assert.deepStrictEqual(threadRows, [
117+ { threadId: "thread-claude", provider: "claudeAgent", modelOptions: null },
118+ { threadId: "thread-codex", provider: "codex", modelOptions: null },
119+ { threadId: "thread-legacy-options", provider: "claudeAgent", modelOptions: null },
120+ { threadId: "thread-session", provider: "claudeAgent", modelOptions: null },
121+ ]);
122+
123+ const eventRows = yield* sql<{
124+ readonly payloadJson: string;
125+ }>`
126+ SELECT payload_json AS "payloadJson"
127+ FROM orchestration_events
128+ ORDER BY rowid ASC
129+ `;
130+
131+ assert.deepStrictEqual(JSON.parse(eventRows[0]!.payloadJson), {
132+ projectId: "project-1",
133+ title: "Project",
134+ workspaceRoot: "/tmp/project",
135+ defaultModelSelection: {
136+ provider: "claudeAgent",
137+ model: "claude-opus-4-6",
138+ options: {
139+ effort: "max",
140+ },
141+ },
142+ scripts: [],
143+ createdAt: "2026-01-01T00:00:00.000Z",
144+ updatedAt: "2026-01-01T00:00:00.000Z",
145+ });
146+
147+ assert.deepStrictEqual(JSON.parse(eventRows[1]!.payloadJson), {
148+ threadId: "thread-1",
149+ projectId: "project-1",
150+ title: "Thread",
151+ modelSelection: {
152+ provider: "claudeAgent",
153+ model: "claude-opus-4-6",
154+ options: {
155+ effort: "max",
156+ thinking: false,
157+ },
158+ },
159+ runtimeMode: "full-access",
160+ interactionMode: "default",
161+ branch: null,
162+ worktreePath: null,
163+ createdAt: "2026-01-01T00:00:00.000Z",
164+ updatedAt: "2026-01-01T00:00:00.000Z",
165+ });
166+
167+ assert.deepStrictEqual(JSON.parse(eventRows[2]!.payloadJson), {
168+ threadId: "thread-1",
169+ turnId: "turn-1",
170+ input: "hi",
171+ modelSelection: {
172+ provider: "codex",
173+ model: "gpt-5.4",
174+ options: {
175+ fastMode: true,
176+ },
177+ },
178+ deliveryMode: "buffered",
179+ });
180+ }),
181+ );
182+});
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.tsrenamed from apps/server/src/persistence/Migrations/018_CanonicalizeLegacyModelSelections.ts+50−13
@@ -4,24 +4,64 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; · 3 unmodified lines
44 export default Effect.gen(function* () {
55 const sql = yield* SqlClient.SqlClient;
66
7+ yield* sql`
8+ ALTER TABLE projection_projects
9+ ADD COLUMN default_provider TEXT
10+ `;
11+
712 yield* sql`
813 UPDATE projection_projects
9− SET
10− default_provider = CASE
11− WHEN default_model IS NULL THEN NULL
12− WHEN lower(default_model) LIKE '%claude%' THEN 'claudeAgent'
14+ SET default_provider = CASE
15+ WHEN default_model IS NULL THEN NULL
16+ WHEN lower(default_model) LIKE '%claude%' THEN 'claudeAgent'
17+ ELSE 'codex'
18+ END
19+ WHERE default_provider IS NULL
20+ `;
21+
22+ yield* sql`
23+ ALTER TABLE projection_threads
24+ ADD COLUMN provider TEXT
25+ `;
26+
27+ yield* sql`
28+ UPDATE projection_threads
29+ SET provider = COALESCE(
30+ (
31+ SELECT provider_name
32+ FROM projection_thread_sessions
33+ WHERE projection_thread_sessions.thread_id = projection_threads.thread_id
34+ ),
35+ CASE
36+ WHEN lower(model) LIKE '%claude%' THEN 'claudeAgent'
1337 ELSE 'codex'
1438 END,
39+ 'codex'
40+ )
41+ WHERE provider IS NULL
42+ `;
43+
44+ yield* sql`
45+ ALTER TABLE projection_projects
46+ ADD COLUMN default_model_options_json TEXT
47+ `;
48+
49+ yield* sql`
50+ ALTER TABLE projection_threads
51+ ADD COLUMN model_options_json TEXT
52+ `;
53+
54+ yield* sql`
55+ UPDATE projection_projects
56+ SET
1557 default_model_options_json = CASE
1658 WHEN default_model_options_json IS NULL THEN NULL
1759 WHEN json_valid(default_model_options_json) = 0 THEN default_model_options_json
1860 WHEN json_type(default_model_options_json, '$.codex') IS NOT NULL
1961 OR json_type(default_model_options_json, '$.claudeAgent') IS NOT NULL
2062 THEN CASE
21− WHEN lower(default_model) LIKE '%claude%' THEN json_extract(
22− default_model_options_json,
23− '$.claudeAgent'
24− )
63+ WHEN default_provider = 'claudeAgent'
64+ THEN json_extract(default_model_options_json, '$.claudeAgent')
2565 ELSE json_extract(default_model_options_json, '$.codex')
2666 END
2767 ELSE default_model_options_json
@@ -32,17 +72,14 @@ export default Effect.gen(function* () { · 4 unmodified lines
3272 yield* sql`
3373 UPDATE projection_threads
3474 SET
35− provider = CASE
36− WHEN lower(model) LIKE '%claude%' THEN 'claudeAgent'
37− ELSE 'codex'
38− END,
3975 model_options_json = CASE
4076 WHEN model_options_json IS NULL THEN NULL
4177 WHEN json_valid(model_options_json) = 0 THEN model_options_json
4278 WHEN json_type(model_options_json, '$.codex') IS NOT NULL
4379 OR json_type(model_options_json, '$.claudeAgent') IS NOT NULL
4480 THEN CASE
45− WHEN lower(model) LIKE '%claude%' THEN json_extract(model_options_json, '$.claudeAgent')
81+ WHEN provider = 'claudeAgent'
82+ THEN json_extract(model_options_json, '$.claudeAgent')
4683 ELSE json_extract(model_options_json, '$.codex')
4784 END
4885 ELSE model_options_json
apps/server/src/persistence/Migrations/016_ProjectionProviders.ts+0−38
@@ -1,38 +0,0 @@
1−import * as Effect from "effect/Effect";
2−import * as SqlClient from "effect/unstable/sql/SqlClient";
3−
4−export default Effect.gen(function* () {
5− const sql = yield* SqlClient.SqlClient;
6−
7− yield* sql`
8− ALTER TABLE projection_projects
9− ADD COLUMN default_provider TEXT
10− `;
11−
12− yield* sql`
13− UPDATE projection_projects
14− SET default_provider = CASE
15− WHEN default_model IS NULL THEN NULL
16− ELSE 'codex'
17− END
18− WHERE default_provider IS NULL
19− `;
20−
21− yield* sql`
22− ALTER TABLE projection_threads
23− ADD COLUMN provider TEXT
24− `;
25−
26− yield* sql`
27− UPDATE projection_threads
28− SET provider = COALESCE(
29− (
30− SELECT provider_name
31− FROM projection_thread_sessions
32− WHERE projection_thread_sessions.thread_id = projection_threads.thread_id
33− ),
34− 'codex'
35− )
36− WHERE provider IS NULL
37− `;
38−});
apps/server/src/persistence/Migrations/017_ProjectionModelSelectionOptions.ts+0−28
@@ -1,28 +0,0 @@
1−import * as Effect from "effect/Effect";
2−import * as SqlClient from "effect/unstable/sql/SqlClient";
3−
4−export default Effect.gen(function* () {
5− const sql = yield* SqlClient.SqlClient;
6−
7− yield* sql`
8− ALTER TABLE projection_projects
9− ADD COLUMN default_model_options_json TEXT
10− `;
11−
12− yield* sql`
13− UPDATE projection_projects
14− SET default_model_options_json = NULL
15− WHERE default_model_options_json IS NULL
16− `;
17−
18− yield* sql`
19− ALTER TABLE projection_threads
20− ADD COLUMN model_options_json TEXT
21− `;
22−
23− yield* sql`
24− UPDATE projection_threads
25− SET model_options_json = NULL
26− WHERE model_options_json IS NULL
27− `;
28−});
apps/server/src/persistence/Migrations/018_CanonicalizeLegacyModelSelections.test.ts+0−180
@@ -1,180 +0,0 @@
1−import { assert, it } from "@effect/vitest";
2−import { Effect, Layer } from "effect";
3−import * as SqlClient from "effect/unstable/sql/SqlClient";
4−
5−import Migration0018 from "./018_CanonicalizeLegacyModelSelections.ts";
6−import * as NodeSqliteClient from "../NodeSqliteClient.ts";
7−
8−const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
9−
10−layer("018_CanonicalizeLegacyModelSelections", (it) => {
11− it.effect("canonicalizes legacy projection rows and orchestration event payloads", () =>
12− Effect.gen(function* () {
13− const sql = yield* SqlClient.SqlClient;
14−
15− yield* sql`
16− CREATE TABLE projection_projects (
17− project_id TEXT PRIMARY KEY,
18− default_provider TEXT,
19− default_model TEXT,
20− default_model_options_json TEXT
21− )
22− `;
23− yield* sql`
24− CREATE TABLE projection_threads (
25− thread_id TEXT PRIMARY KEY,
26− provider TEXT,
27− model TEXT NOT NULL,
28− model_options_json TEXT
29− )
30− `;
31− yield* sql`
32− CREATE TABLE orchestration_events (
33− event_type TEXT NOT NULL,
34− payload_json TEXT NOT NULL
35− )
36− `;
37−
38− yield* sql`
39− INSERT INTO projection_projects (
40− project_id,
41− default_provider,
42− default_model,
43− default_model_options_json
44− )
45− VALUES (
46− 'project-1',
47− 'codex',
48− 'claude-opus-4-6',
49− '{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max","thinking":false}}'
50− )
51− `;
52− yield* sql`
53− INSERT INTO projection_threads (
54− thread_id,
55− provider,
56− model,
57− model_options_json
58− )
59− VALUES (
60− 'thread-1',
61− 'codex',
62− 'claude-opus-4-6',
63− '{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max"}}'
64− )
65− `;
66− yield* sql`
67− INSERT INTO orchestration_events (
68− event_type,
69− payload_json
70− )
71− VALUES
72− (
73− 'project.created',
74− '{"projectId":"project-1","title":"Project","workspaceRoot":"/tmp/project","defaultModel":"claude-opus-4-6","defaultModelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max"}},"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}'
75− ),
76− (
77− 'thread.created',
78− '{"threadId":"thread-1","projectId":"project-1","title":"Thread","model":"claude-opus-4-6","modelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max","thinking":false}},"runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}'
79− ),
80− (
81− 'thread.turn-start-requested',
82− '{"threadId":"thread-1","turnId":"turn-1","input":"hi","model":"gpt-5.4","modelOptions":{"codex":{"fastMode":true},"claudeAgent":{"effort":"max"}},"deliveryMode":"buffered"}'
83− )
84− `;
85−
86− yield* Migration0018;
87−
88− const projectRows = yield* sql<{
89− readonly defaultProvider: string | null;
90− readonly defaultModelOptions: string | null;
91− }>`
92− SELECT
93− default_provider AS "defaultProvider",
94− default_model_options_json AS "defaultModelOptions"
95− FROM projection_projects
96− WHERE project_id = 'project-1'
97− `;
98− assert.deepStrictEqual(projectRows[0], {
99− defaultProvider: "claudeAgent",
100− defaultModelOptions: '{"effort":"max","thinking":false}',
101− });
102−
103− const threadRows = yield* sql<{
104− readonly provider: string | null;
105− readonly modelOptions: string | null;
106− }>`
107− SELECT
108− provider,
109− model_options_json AS "modelOptions"
110− FROM projection_threads
111− WHERE thread_id = 'thread-1'
112− `;
113− assert.deepStrictEqual(threadRows[0], {
114− provider: "claudeAgent",
115− modelOptions: '{"effort":"max"}',
116− });
117−
118− const eventRows = yield* sql<{
119− readonly eventType: string;
120− readonly payloadJson: string;
121− }>`
122− SELECT
123− event_type AS "eventType",
124− payload_json AS "payloadJson"
125− FROM orchestration_events
126− ORDER BY rowid ASC
127− `;
128−
129− assert.deepStrictEqual(JSON.parse(eventRows[0]!.payloadJson), {
130− projectId: "project-1",
131− title: "Project",
132− workspaceRoot: "/tmp/project",
133− defaultModelSelection: {
134− provider: "claudeAgent",
135− model: "claude-opus-4-6",
136− options: {
137− effort: "max",
138− },
139− },
140− scripts: [],
141− createdAt: "2026-01-01T00:00:00.000Z",
142− updatedAt: "2026-01-01T00:00:00.000Z",
143− });
144−
145− assert.deepStrictEqual(JSON.parse(eventRows[1]!.payloadJson), {
146− threadId: "thread-1",
147− projectId: "project-1",
148− title: "Thread",
149− modelSelection: {
150− provider: "claudeAgent",
151− model: "claude-opus-4-6",
152− options: {
153− effort: "max",
154− thinking: false,
155− },
156− },
157− runtimeMode: "full-access",
158− interactionMode: "default",
159− branch: null,
160− worktreePath: null,
161− createdAt: "2026-01-01T00:00:00.000Z",
162− updatedAt: "2026-01-01T00:00:00.000Z",
163− });
164−
165− assert.deepStrictEqual(JSON.parse(eventRows[2]!.payloadJson), {
166− threadId: "thread-1",
167− turnId: "turn-1",
168− input: "hi",
169− modelSelection: {
170− provider: "codex",
171− model: "gpt-5.4",
172− options: {
173− fastMode: true,
174− },
175− },
176− deliveryMode: "buffered",
177− });
178− }),
179− );
180−});