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
27import Migration0013 from "./Migrations/013_ProjectionThreadProposedPlans.ts";27import Migration0013 from "./Migrations/013_ProjectionThreadProposedPlans.ts";
28import Migration0014 from "./Migrations/014_ProjectionThreadProposedPlanImplementation.ts";28import Migration0014 from "./Migrations/014_ProjectionThreadProposedPlanImplementation.ts";
29import Migration0015 from "./Migrations/015_ProjectionTurnsSourceProposedPlan.ts";29import Migration0015 from "./Migrations/015_ProjectionTurnsSourceProposedPlan.ts";
30import Migration0016 from "./Migrations/016_ProjectionProviders.ts";30import Migration0016 from "./Migrations/016_CanonicalizeModelSelections.ts";
31import Migration0017 from "./Migrations/017_ProjectionModelSelectionOptions.ts";
32import Migration0018 from "./Migrations/018_CanonicalizeLegacyModelSelections.ts";
33import { Effect } from "effect";31import { Effect } from "effect";
34 32
35/**33/**
@@ -58,9 +56,7 @@ const loader = Migrator.fromRecord({ · 22 unmodified lines
58 "13_ProjectionThreadProposedPlans": Migration0013,56 "13_ProjectionThreadProposedPlans": Migration0013,
59 "14_ProjectionThreadProposedPlanImplementation": Migration0014,57 "14_ProjectionThreadProposedPlanImplementation": Migration0014,
60 "15_ProjectionTurnsSourceProposedPlan": Migration0015,58 "15_ProjectionTurnsSourceProposedPlan": Migration0015,
61 "16_ProjectionProviders": Migration0016,59 "16_CanonicalizeModelSelections": Migration0016,
62 "17_ProjectionModelSelectionOptions": Migration0017,
63 "18_CanonicalizeLegacyModelSelections": Migration0018,
64});60});
65 61
66/**62/**
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts+182−0
@@ -0,0 +1,182 @@
1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
3import * as SqlClient from "effect/unstable/sql/SqlClient";
4
5import Migration0016 from "./016_CanonicalizeModelSelections.ts";
6import * as NodeSqliteClient from "../NodeSqliteClient.ts";
7
8const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
9
10layer("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
4export default Effect.gen(function* () {4export default Effect.gen(function* () {
5 const sql = yield* SqlClient.SqlClient;5 const sql = yield* SqlClient.SqlClient;
6 6
7 yield* sql`
8 ALTER TABLE projection_projects
9 ADD COLUMN default_provider TEXT
10 `;
11
7 yield* sql`12 yield* sql`
8 UPDATE projection_projects13 UPDATE projection_projects
9 SET14 SET default_provider = CASE
10 default_provider = CASE15 WHEN default_model IS NULL THEN NULL
11 WHEN default_model IS NULL THEN NULL16 WHEN lower(default_model) LIKE '%claude%' THEN 'claudeAgent'
12 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'
13 ELSE 'codex'37 ELSE 'codex'
14 END,38 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
15 default_model_options_json = CASE57 default_model_options_json = CASE
16 WHEN default_model_options_json IS NULL THEN NULL58 WHEN default_model_options_json IS NULL THEN NULL
17 WHEN json_valid(default_model_options_json) = 0 THEN default_model_options_json59 WHEN json_valid(default_model_options_json) = 0 THEN default_model_options_json
18 WHEN json_type(default_model_options_json, '$.codex') IS NOT NULL60 WHEN json_type(default_model_options_json, '$.codex') IS NOT NULL
19 OR json_type(default_model_options_json, '$.claudeAgent') IS NOT NULL61 OR json_type(default_model_options_json, '$.claudeAgent') IS NOT NULL
20 THEN CASE62 THEN CASE
21 WHEN lower(default_model) LIKE '%claude%' THEN json_extract(63 WHEN default_provider = 'claudeAgent'
22 default_model_options_json,64 THEN json_extract(default_model_options_json, '$.claudeAgent')
23 '$.claudeAgent'
24 )
25 ELSE json_extract(default_model_options_json, '$.codex')65 ELSE json_extract(default_model_options_json, '$.codex')
26 END66 END
27 ELSE default_model_options_json67 ELSE default_model_options_json
@@ -32,17 +72,14 @@ export default Effect.gen(function* () { · 4 unmodified lines
32 yield* sql`72 yield* sql`
33 UPDATE projection_threads73 UPDATE projection_threads
34 SET74 SET
35 provider = CASE
36 WHEN lower(model) LIKE '%claude%' THEN 'claudeAgent'
37 ELSE 'codex'
38 END,
39 model_options_json = CASE75 model_options_json = CASE
40 WHEN model_options_json IS NULL THEN NULL76 WHEN model_options_json IS NULL THEN NULL
41 WHEN json_valid(model_options_json) = 0 THEN model_options_json77 WHEN json_valid(model_options_json) = 0 THEN model_options_json
42 WHEN json_type(model_options_json, '$.codex') IS NOT NULL78 WHEN json_type(model_options_json, '$.codex') IS NOT NULL
43 OR json_type(model_options_json, '$.claudeAgent') IS NOT NULL79 OR json_type(model_options_json, '$.claudeAgent') IS NOT NULL
44 THEN CASE80 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')
46 ELSE json_extract(model_options_json, '$.codex')83 ELSE json_extract(model_options_json, '$.codex')
47 END84 END
48 ELSE model_options_json85 ELSE model_options_json
apps/server/src/persistence/Migrations/016_ProjectionProviders.ts+0−38
@@ -1,38 +0,0 @@
1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";
3
4export 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 @@
1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";
3
4export 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 @@
1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
3import * as SqlClient from "effect/unstable/sql/SqlClient";
4
5import Migration0018 from "./018_CanonicalizeLegacyModelSelections.ts";
6import * as NodeSqliteClient from "../NodeSqliteClient.ts";
7
8const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
9
10layer("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});