@smithers-orchestrator/db 0.20.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -7
- package/src/SqlMessageStorage.js +1 -823
- package/src/adapter/SmithersDb.js +3 -2317
- package/src/adapter.js +1 -1
- package/src/internal-schema/index.js +1 -0
- package/src/internal-schema/smithersFrames.js +6 -1
- package/src/internal-schema/smithersNodeDiffs.js +6 -1
- package/src/internal-schema/smithersSchemaMigrations.js +12 -0
- package/src/internal-schema/smithersTimeTravelAudit.js +8 -2
- package/src/internal-schema.js +25 -2
- package/src/schema-migrations.js +481 -0
- package/src/sql-message-storage.js +11 -69
package/src/adapter.js
CHANGED
|
@@ -2031,7 +2031,7 @@ export class SmithersDb {
|
|
|
2031
2031
|
* @returns {RunnableEffect<void, SmithersError>}
|
|
2032
2032
|
*/
|
|
2033
2033
|
insertCache(row) {
|
|
2034
|
-
return this.write(`insert cache ${row.cacheKey}`, () => this.internalStorage.
|
|
2034
|
+
return this.write(`insert cache ${row.cacheKey}`, () => this.internalStorage.upsert("_smithers_cache", row, ["cacheKey"]));
|
|
2035
2035
|
}
|
|
2036
2036
|
/**
|
|
2037
2037
|
* @param {Record<string, unknown>} row
|
|
@@ -19,3 +19,4 @@ export { smithersMemoryThreads } from "./smithersMemoryThreads.js";
|
|
|
19
19
|
export { smithersMemoryMessages } from "./smithersMemoryMessages.js";
|
|
20
20
|
export { smithersVectors } from "./smithersVectors.js";
|
|
21
21
|
export { smithersCron } from "./smithersCron.js";
|
|
22
|
+
export { smithersSchemaMigrations } from "./smithersSchemaMigrations.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { integer, sqliteTable, text, primaryKey, } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { foreignKey, integer, sqliteTable, text, primaryKey, } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import { smithersRuns } from "./smithersRuns.js";
|
|
2
3
|
export const smithersFrames = sqliteTable("_smithers_frames", {
|
|
3
4
|
runId: text("run_id").notNull(),
|
|
4
5
|
frameNo: integer("frame_no").notNull(),
|
|
@@ -11,4 +12,8 @@ export const smithersFrames = sqliteTable("_smithers_frames", {
|
|
|
11
12
|
note: text("note"),
|
|
12
13
|
}, (t) => ({
|
|
13
14
|
pk: primaryKey({ columns: [t.runId, t.frameNo] }),
|
|
15
|
+
runFk: foreignKey({
|
|
16
|
+
columns: [t.runId],
|
|
17
|
+
foreignColumns: [smithersRuns.runId],
|
|
18
|
+
}).onDelete("cascade"),
|
|
14
19
|
}));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { foreignKey, integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import { smithersRuns } from "./smithersRuns.js";
|
|
2
3
|
export const smithersNodeDiffs = sqliteTable("_smithers_node_diffs", {
|
|
3
4
|
runId: text("run_id").notNull(),
|
|
4
5
|
nodeId: text("node_id").notNull(),
|
|
@@ -9,4 +10,8 @@ export const smithersNodeDiffs = sqliteTable("_smithers_node_diffs", {
|
|
|
9
10
|
sizeBytes: integer("size_bytes").notNull(),
|
|
10
11
|
}, (t) => ({
|
|
11
12
|
pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration, t.baseRef] }),
|
|
13
|
+
runFk: foreignKey({
|
|
14
|
+
columns: [t.runId],
|
|
15
|
+
foreignColumns: [smithersRuns.runId],
|
|
16
|
+
}).onDelete("cascade"),
|
|
12
17
|
}));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const smithersSchemaMigrations = sqliteTable("_smithers_schema_migrations", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
name: text("name").notNull(),
|
|
6
|
+
appliedAtMs: integer("applied_at_ms").notNull(),
|
|
7
|
+
checksum: text("checksum"),
|
|
8
|
+
destructive: integer("destructive", { mode: "boolean" })
|
|
9
|
+
.notNull()
|
|
10
|
+
.default(false),
|
|
11
|
+
detailsJson: text("details_json"),
|
|
12
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { foreignKey, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import { smithersRuns } from "./smithersRuns.js";
|
|
2
3
|
|
|
3
4
|
export const smithersTimeTravelAudit = sqliteTable("_smithers_time_travel_audit", {
|
|
4
5
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
@@ -9,4 +10,9 @@ export const smithersTimeTravelAudit = sqliteTable("_smithers_time_travel_audit"
|
|
|
9
10
|
timestampMs: integer("timestamp_ms").notNull(),
|
|
10
11
|
result: text("result").notNull(),
|
|
11
12
|
durationMs: integer("duration_ms"),
|
|
12
|
-
})
|
|
13
|
+
}, (t) => ({
|
|
14
|
+
runFk: foreignKey({
|
|
15
|
+
columns: [t.runId],
|
|
16
|
+
foreignColumns: [smithersRuns.runId],
|
|
17
|
+
}).onDelete("cascade"),
|
|
18
|
+
}));
|
package/src/internal-schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { blob, integer, sqliteTable, text, primaryKey, } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { blob, foreignKey, integer, sqliteTable, text, primaryKey, } from "drizzle-orm/sqlite-core";
|
|
2
2
|
export const smithersRuns = sqliteTable("_smithers_runs", {
|
|
3
3
|
runId: text("run_id").primaryKey(),
|
|
4
4
|
parentRunId: text("parent_run_id"),
|
|
@@ -63,6 +63,10 @@ export const smithersFrames = sqliteTable("_smithers_frames", {
|
|
|
63
63
|
note: text("note"),
|
|
64
64
|
}, (t) => ({
|
|
65
65
|
pk: primaryKey({ columns: [t.runId, t.frameNo] }),
|
|
66
|
+
runFk: foreignKey({
|
|
67
|
+
columns: [t.runId],
|
|
68
|
+
foreignColumns: [smithersRuns.runId],
|
|
69
|
+
}).onDelete("cascade"),
|
|
66
70
|
}));
|
|
67
71
|
export const smithersApprovals = sqliteTable("_smithers_approvals", {
|
|
68
72
|
runId: text("run_id").notNull(),
|
|
@@ -158,6 +162,10 @@ export const smithersNodeDiffs = sqliteTable("_smithers_node_diffs", {
|
|
|
158
162
|
pk: primaryKey({
|
|
159
163
|
columns: [t.runId, t.nodeId, t.iteration, t.baseRef],
|
|
160
164
|
}),
|
|
165
|
+
runFk: foreignKey({
|
|
166
|
+
columns: [t.runId],
|
|
167
|
+
foreignColumns: [smithersRuns.runId],
|
|
168
|
+
}).onDelete("cascade"),
|
|
161
169
|
}));
|
|
162
170
|
export const smithersTimeTravelAudit = sqliteTable("_smithers_time_travel_audit", {
|
|
163
171
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
@@ -168,7 +176,12 @@ export const smithersTimeTravelAudit = sqliteTable("_smithers_time_travel_audit"
|
|
|
168
176
|
timestampMs: integer("timestamp_ms").notNull(),
|
|
169
177
|
result: text("result").notNull(),
|
|
170
178
|
durationMs: integer("duration_ms"),
|
|
171
|
-
})
|
|
179
|
+
}, (t) => ({
|
|
180
|
+
runFk: foreignKey({
|
|
181
|
+
columns: [t.runId],
|
|
182
|
+
foreignColumns: [smithersRuns.runId],
|
|
183
|
+
}).onDelete("cascade"),
|
|
184
|
+
}));
|
|
172
185
|
export const smithersSandboxes = sqliteTable("_smithers_sandboxes", {
|
|
173
186
|
runId: text("run_id").notNull(),
|
|
174
187
|
sandboxId: text("sandbox_id").notNull(),
|
|
@@ -245,3 +258,13 @@ export const smithersCron = sqliteTable("_smithers_cron", {
|
|
|
245
258
|
nextRunAtMs: integer("next_run_at_ms"),
|
|
246
259
|
errorJson: text("error_json"),
|
|
247
260
|
});
|
|
261
|
+
export const smithersSchemaMigrations = sqliteTable("_smithers_schema_migrations", {
|
|
262
|
+
id: text("id").primaryKey(),
|
|
263
|
+
name: text("name").notNull(),
|
|
264
|
+
appliedAtMs: integer("applied_at_ms").notNull(),
|
|
265
|
+
checksum: text("checksum"),
|
|
266
|
+
destructive: integer("destructive", { mode: "boolean" })
|
|
267
|
+
.notNull()
|
|
268
|
+
.default(false),
|
|
269
|
+
detailsJson: text("details_json"),
|
|
270
|
+
});
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
3
|
+
import { smithersSchemaMigrations } from "./internal-schema/smithersSchemaMigrations.js";
|
|
4
|
+
|
|
5
|
+
const MIGRATION_TABLE_SQL = `CREATE TABLE IF NOT EXISTS _smithers_schema_migrations (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
applied_at_ms INTEGER NOT NULL,
|
|
9
|
+
checksum TEXT,
|
|
10
|
+
destructive INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
details_json TEXT
|
|
12
|
+
)`;
|
|
13
|
+
|
|
14
|
+
const RUN_OWNED_FOREIGN_KEY_TABLES = [
|
|
15
|
+
{
|
|
16
|
+
table: "_smithers_frames",
|
|
17
|
+
columns: [
|
|
18
|
+
"run_id",
|
|
19
|
+
"frame_no",
|
|
20
|
+
"created_at_ms",
|
|
21
|
+
"xml_json",
|
|
22
|
+
"xml_hash",
|
|
23
|
+
"encoding",
|
|
24
|
+
"mounted_task_ids_json",
|
|
25
|
+
"task_index_json",
|
|
26
|
+
"note",
|
|
27
|
+
],
|
|
28
|
+
defaults: { encoding: "'full'" },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
table: "_smithers_node_diffs",
|
|
32
|
+
columns: [
|
|
33
|
+
"run_id",
|
|
34
|
+
"node_id",
|
|
35
|
+
"iteration",
|
|
36
|
+
"base_ref",
|
|
37
|
+
"diff_json",
|
|
38
|
+
"computed_at_ms",
|
|
39
|
+
"size_bytes",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
table: "_smithers_time_travel_audit",
|
|
44
|
+
columns: [
|
|
45
|
+
"id",
|
|
46
|
+
"run_id",
|
|
47
|
+
"from_frame_no",
|
|
48
|
+
"to_frame_no",
|
|
49
|
+
"caller",
|
|
50
|
+
"timestamp_ms",
|
|
51
|
+
"result",
|
|
52
|
+
"duration_ms",
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const LEGACY_COLUMN_MIGRATIONS = [
|
|
58
|
+
{
|
|
59
|
+
id: "0002_attempt_legacy_columns",
|
|
60
|
+
name: "Add legacy attempt operational columns",
|
|
61
|
+
table: "_smithers_attempts",
|
|
62
|
+
columns: [
|
|
63
|
+
["response_text", "response_text TEXT"],
|
|
64
|
+
["jj_cwd", "jj_cwd TEXT"],
|
|
65
|
+
["heartbeat_at_ms", "heartbeat_at_ms INTEGER"],
|
|
66
|
+
["heartbeat_data_json", "heartbeat_data_json TEXT"],
|
|
67
|
+
["cached", "cached INTEGER DEFAULT 0"],
|
|
68
|
+
["meta_json", "meta_json TEXT"],
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "0003_run_legacy_columns",
|
|
73
|
+
name: "Add legacy run operational columns",
|
|
74
|
+
table: "_smithers_runs",
|
|
75
|
+
columns: [
|
|
76
|
+
["workflow_hash", "workflow_hash TEXT"],
|
|
77
|
+
["heartbeat_at_ms", "heartbeat_at_ms INTEGER"],
|
|
78
|
+
["runtime_owner_id", "runtime_owner_id TEXT"],
|
|
79
|
+
["cancel_requested_at_ms", "cancel_requested_at_ms INTEGER"],
|
|
80
|
+
["hijack_requested_at_ms", "hijack_requested_at_ms INTEGER"],
|
|
81
|
+
["hijack_target", "hijack_target TEXT"],
|
|
82
|
+
["vcs_type", "vcs_type TEXT"],
|
|
83
|
+
["vcs_root", "vcs_root TEXT"],
|
|
84
|
+
["vcs_revision", "vcs_revision TEXT"],
|
|
85
|
+
["parent_run_id", "parent_run_id TEXT"],
|
|
86
|
+
["error_json", "error_json TEXT"],
|
|
87
|
+
["config_json", "config_json TEXT"],
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "0004_approval_payload_columns",
|
|
92
|
+
name: "Add approval payload columns",
|
|
93
|
+
table: "_smithers_approvals",
|
|
94
|
+
columns: [
|
|
95
|
+
["request_json", "request_json TEXT"],
|
|
96
|
+
["decision_json", "decision_json TEXT"],
|
|
97
|
+
["auto_approved", "auto_approved INTEGER NOT NULL DEFAULT 0"],
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "0005_alert_model_extensions",
|
|
102
|
+
name: "Add alert model extension columns",
|
|
103
|
+
table: "_smithers_alerts",
|
|
104
|
+
columns: [
|
|
105
|
+
["fingerprint", "fingerprint TEXT"],
|
|
106
|
+
["node_id", "node_id TEXT"],
|
|
107
|
+
["iteration", "iteration INTEGER"],
|
|
108
|
+
["owner", "owner TEXT"],
|
|
109
|
+
["runbook", "runbook TEXT"],
|
|
110
|
+
["labels_json", "labels_json TEXT"],
|
|
111
|
+
["reaction_json", "reaction_json TEXT"],
|
|
112
|
+
["source_event_type", "source_event_type TEXT"],
|
|
113
|
+
["first_fired_at_ms", "first_fired_at_ms INTEGER"],
|
|
114
|
+
["last_fired_at_ms", "last_fired_at_ms INTEGER"],
|
|
115
|
+
["occurrence_count", "occurrence_count INTEGER DEFAULT 1"],
|
|
116
|
+
["silenced_until_ms", "silenced_until_ms INTEGER"],
|
|
117
|
+
["acknowledged_by", "acknowledged_by TEXT"],
|
|
118
|
+
["resolved_by", "resolved_by TEXT"],
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "0006_frame_encoding_column",
|
|
123
|
+
name: "Add frame encoding column",
|
|
124
|
+
table: "_smithers_frames",
|
|
125
|
+
columns: [["encoding", "encoding TEXT NOT NULL DEFAULT 'full'"]],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const EXTRA_INDEX_STATEMENTS = [
|
|
130
|
+
`CREATE INDEX IF NOT EXISTS _smithers_runs_parent_idx ON _smithers_runs (parent_run_id)`,
|
|
131
|
+
`CREATE INDEX IF NOT EXISTS _smithers_alerts_fingerprint_idx ON _smithers_alerts (fingerprint)`,
|
|
132
|
+
`CREATE INDEX IF NOT EXISTS _smithers_alerts_run_status_idx ON _smithers_alerts (run_id, status)`,
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} identifier
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
function quoteIdentifier(identifier) {
|
|
140
|
+
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
145
|
+
* @param {string} table
|
|
146
|
+
*/
|
|
147
|
+
function tableExists(sqlite, table) {
|
|
148
|
+
return Boolean(sqlite
|
|
149
|
+
.query(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
150
|
+
.get(table));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
155
|
+
* @param {string} table
|
|
156
|
+
*/
|
|
157
|
+
function tableColumnNames(sqlite, table) {
|
|
158
|
+
if (!tableExists(sqlite, table)) {
|
|
159
|
+
return new Set();
|
|
160
|
+
}
|
|
161
|
+
return new Set(sqlite
|
|
162
|
+
.query(`PRAGMA table_info(${quoteIdentifier(table)})`)
|
|
163
|
+
.all()
|
|
164
|
+
.map((row) => row.name)
|
|
165
|
+
.filter((name) => typeof name === "string"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
170
|
+
* @param {string} table
|
|
171
|
+
*/
|
|
172
|
+
function tableHasRunForeignKey(sqlite, table) {
|
|
173
|
+
if (!tableExists(sqlite, table)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return sqlite
|
|
177
|
+
.query(`PRAGMA foreign_key_list(${quoteIdentifier(table)})`)
|
|
178
|
+
.all()
|
|
179
|
+
.some((row) => row.from === "run_id" &&
|
|
180
|
+
row.table === "_smithers_runs" &&
|
|
181
|
+
row.to === "run_id" &&
|
|
182
|
+
String(row.on_delete ?? "").toUpperCase() === "CASCADE");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
187
|
+
* @param {string} table
|
|
188
|
+
* @param {string} [whereSql]
|
|
189
|
+
*/
|
|
190
|
+
function countRows(sqlite, table, whereSql = "") {
|
|
191
|
+
const row = sqlite
|
|
192
|
+
.query(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(table)} ${whereSql}`)
|
|
193
|
+
.get();
|
|
194
|
+
return Number(row?.count ?? 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
199
|
+
* @param {string} table
|
|
200
|
+
*/
|
|
201
|
+
function nextLegacyTableName(sqlite, table) {
|
|
202
|
+
let suffix = 0;
|
|
203
|
+
while (true) {
|
|
204
|
+
const candidate = `${table}__legacy_fk_${suffix}`;
|
|
205
|
+
if (!tableExists(sqlite, candidate)) {
|
|
206
|
+
return candidate;
|
|
207
|
+
}
|
|
208
|
+
suffix += 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} table
|
|
214
|
+
* @param {readonly string[]} createTableStatements
|
|
215
|
+
*/
|
|
216
|
+
function createTableStatementFor(table, createTableStatements) {
|
|
217
|
+
const statement = createTableStatements.find((candidate) => candidate.includes(`CREATE TABLE IF NOT EXISTS ${table} (`));
|
|
218
|
+
if (!statement) {
|
|
219
|
+
throw new Error(`Missing create statement for ${table}`);
|
|
220
|
+
}
|
|
221
|
+
return statement;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
226
|
+
* @param {string} table
|
|
227
|
+
* @param {string} column
|
|
228
|
+
* @param {string} definition
|
|
229
|
+
*/
|
|
230
|
+
function addColumnIfMissing(sqlite, table, column, definition) {
|
|
231
|
+
const columns = tableColumnNames(sqlite, table);
|
|
232
|
+
if (columns.has(column)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
sqlite.run(`ALTER TABLE ${quoteIdentifier(table)} ADD COLUMN ${definition}`);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
241
|
+
* @param {{ table: string; columns: readonly string[]; defaults?: Record<string, string>; }} config
|
|
242
|
+
* @param {readonly string[]} createTableStatements
|
|
243
|
+
*/
|
|
244
|
+
function rebuildRunOwnedForeignKeyTable(sqlite, config, createTableStatements) {
|
|
245
|
+
if (!tableExists(sqlite, config.table)) {
|
|
246
|
+
return { table: config.table, beforeCount: 0, keptCount: 0, droppedCount: 0, skipped: "missing" };
|
|
247
|
+
}
|
|
248
|
+
if (tableHasRunForeignKey(sqlite, config.table)) {
|
|
249
|
+
const count = countRows(sqlite, config.table);
|
|
250
|
+
return { table: config.table, beforeCount: count, keptCount: count, droppedCount: 0, skipped: "has_fk" };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const beforeCount = countRows(sqlite, config.table);
|
|
254
|
+
const legacyTable = nextLegacyTableName(sqlite, config.table);
|
|
255
|
+
sqlite.run("PRAGMA foreign_keys = OFF");
|
|
256
|
+
sqlite.run("BEGIN IMMEDIATE");
|
|
257
|
+
try {
|
|
258
|
+
sqlite.run(`ALTER TABLE ${quoteIdentifier(config.table)} RENAME TO ${quoteIdentifier(legacyTable)}`);
|
|
259
|
+
sqlite.run(createTableStatementFor(config.table, createTableStatements));
|
|
260
|
+
const legacyColumns = tableColumnNames(sqlite, legacyTable);
|
|
261
|
+
const columnSql = config.columns.map(quoteIdentifier).join(", ");
|
|
262
|
+
const selectSql = config.columns
|
|
263
|
+
.map((column) => legacyColumns.has(column)
|
|
264
|
+
? quoteIdentifier(column)
|
|
265
|
+
: (config.defaults?.[column] ?? "NULL"))
|
|
266
|
+
.join(", ");
|
|
267
|
+
const validRunWhere = legacyColumns.has("run_id")
|
|
268
|
+
? `WHERE ${quoteIdentifier("run_id")} IN (SELECT run_id FROM ${quoteIdentifier("_smithers_runs")})`
|
|
269
|
+
: "WHERE 0";
|
|
270
|
+
const keptCount = countRows(sqlite, legacyTable, validRunWhere);
|
|
271
|
+
sqlite.run(`INSERT INTO ${quoteIdentifier(config.table)} (${columnSql})
|
|
272
|
+
SELECT ${selectSql} FROM ${quoteIdentifier(legacyTable)} ${validRunWhere}`);
|
|
273
|
+
sqlite.run(`DROP TABLE ${quoteIdentifier(legacyTable)}`);
|
|
274
|
+
sqlite.run("COMMIT");
|
|
275
|
+
return {
|
|
276
|
+
table: config.table,
|
|
277
|
+
beforeCount,
|
|
278
|
+
keptCount,
|
|
279
|
+
droppedCount: beforeCount - keptCount,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
try {
|
|
284
|
+
sqlite.run("ROLLBACK");
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Preserve the original migration error.
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
sqlite.run("PRAGMA foreign_keys = ON");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {readonly string[]} statements
|
|
298
|
+
*/
|
|
299
|
+
function checksumForStatements(statements) {
|
|
300
|
+
return statements.join("\n-- statement --\n");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
305
|
+
* @param {string} id
|
|
306
|
+
*/
|
|
307
|
+
function hasMigrationRecord(sqlite, id) {
|
|
308
|
+
const db = drizzle(sqlite, { schema: { smithersSchemaMigrations } });
|
|
309
|
+
return Boolean(db
|
|
310
|
+
.select({ id: smithersSchemaMigrations.id })
|
|
311
|
+
.from(smithersSchemaMigrations)
|
|
312
|
+
.where(eq(smithersSchemaMigrations.id, id))
|
|
313
|
+
.limit(1)
|
|
314
|
+
.all()[0]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
319
|
+
*/
|
|
320
|
+
function loadAppliedMigrationIds(sqlite) {
|
|
321
|
+
const db = drizzle(sqlite, { schema: { smithersSchemaMigrations } });
|
|
322
|
+
return new Set(db
|
|
323
|
+
.select({ id: smithersSchemaMigrations.id })
|
|
324
|
+
.from(smithersSchemaMigrations)
|
|
325
|
+
.all()
|
|
326
|
+
.map((row) => row.id));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
331
|
+
* @param {{ id: string; name: string; checksum?: string; destructive?: boolean; }} migration
|
|
332
|
+
* @param {unknown} details
|
|
333
|
+
*/
|
|
334
|
+
function recordMigration(sqlite, migration, details) {
|
|
335
|
+
const db = drizzle(sqlite, { schema: { smithersSchemaMigrations } });
|
|
336
|
+
db.insert(smithersSchemaMigrations)
|
|
337
|
+
.values({
|
|
338
|
+
id: migration.id,
|
|
339
|
+
name: migration.name,
|
|
340
|
+
appliedAtMs: Date.now(),
|
|
341
|
+
checksum: migration.checksum ?? null,
|
|
342
|
+
destructive: Boolean(migration.destructive),
|
|
343
|
+
detailsJson: details === undefined ? null : JSON.stringify(details),
|
|
344
|
+
})
|
|
345
|
+
.onConflictDoNothing({ target: smithersSchemaMigrations.id })
|
|
346
|
+
.run();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @param {{ id: string; destructive?: boolean; }} migration
|
|
351
|
+
* @param {any} details
|
|
352
|
+
*/
|
|
353
|
+
function logDestructiveMigration(migration, details) {
|
|
354
|
+
if (!migration.destructive || !details?.tables) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const droppedCount = details.tables.reduce((sum, row) => sum + Number(row.droppedCount ?? 0), 0);
|
|
358
|
+
if (droppedCount <= 0) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
console.warn(`[smithers-db] migration ${migration.id} dropped ${droppedCount} orphan run-owned rows`, details.tables);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {{
|
|
366
|
+
* createTableStatements: readonly string[];
|
|
367
|
+
* createIndexStatements: readonly string[];
|
|
368
|
+
* }} context
|
|
369
|
+
*/
|
|
370
|
+
function buildMigrations(context) {
|
|
371
|
+
const columnMigrations = LEGACY_COLUMN_MIGRATIONS.map((config) => ({
|
|
372
|
+
id: config.id,
|
|
373
|
+
name: config.name,
|
|
374
|
+
checksum: checksumForStatements(config.columns.map(([, definition]) => `ALTER TABLE ${config.table} ADD COLUMN ${definition}`)),
|
|
375
|
+
isApplied: (sqlite) => {
|
|
376
|
+
const columns = tableColumnNames(sqlite, config.table);
|
|
377
|
+
return config.columns.every(([column]) => columns.has(column));
|
|
378
|
+
},
|
|
379
|
+
up: (sqlite) => {
|
|
380
|
+
const addedColumns = [];
|
|
381
|
+
for (const [column, definition] of config.columns) {
|
|
382
|
+
if (addColumnIfMissing(sqlite, config.table, column, definition)) {
|
|
383
|
+
addedColumns.push(column);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { table: config.table, addedColumns };
|
|
387
|
+
},
|
|
388
|
+
}));
|
|
389
|
+
return [
|
|
390
|
+
{
|
|
391
|
+
id: "0001_current_tables",
|
|
392
|
+
name: "Create current Smithers tables",
|
|
393
|
+
checksum: checksumForStatements(context.createTableStatements),
|
|
394
|
+
isApplied: () => false,
|
|
395
|
+
up: (sqlite) => {
|
|
396
|
+
for (const statement of context.createTableStatements) {
|
|
397
|
+
sqlite.run(statement);
|
|
398
|
+
}
|
|
399
|
+
return { statementCount: context.createTableStatements.length };
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
...columnMigrations,
|
|
403
|
+
{
|
|
404
|
+
id: "0011_add_node_diffs",
|
|
405
|
+
name: "Add node diff cache table",
|
|
406
|
+
checksum: "packages/db/migrations/0011_add_node_diffs.sql",
|
|
407
|
+
isApplied: (sqlite) => tableExists(sqlite, "_smithers_node_diffs"),
|
|
408
|
+
up: (sqlite) => {
|
|
409
|
+
sqlite.run(createTableStatementFor("_smithers_node_diffs", context.createTableStatements));
|
|
410
|
+
return { table: "_smithers_node_diffs" };
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "0012_add_time_travel_audit",
|
|
415
|
+
name: "Add time-travel audit table",
|
|
416
|
+
checksum: "packages/db/migrations/0012_add_time_travel_audit.sql",
|
|
417
|
+
isApplied: (sqlite) => tableExists(sqlite, "_smithers_time_travel_audit") &&
|
|
418
|
+
tableColumnNames(sqlite, "_smithers_time_travel_audit").has("duration_ms"),
|
|
419
|
+
up: (sqlite) => {
|
|
420
|
+
if (!tableExists(sqlite, "_smithers_time_travel_audit")) {
|
|
421
|
+
sqlite.run(createTableStatementFor("_smithers_time_travel_audit", context.createTableStatements));
|
|
422
|
+
return { table: "_smithers_time_travel_audit", created: true };
|
|
423
|
+
}
|
|
424
|
+
addColumnIfMissing(sqlite, "_smithers_time_travel_audit", "duration_ms", "duration_ms INTEGER");
|
|
425
|
+
return { table: "_smithers_time_travel_audit", addedColumns: ["duration_ms"] };
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "0013_run_owned_foreign_keys",
|
|
430
|
+
name: "Rebuild run-owned tables with cascading foreign keys",
|
|
431
|
+
checksum: checksumForStatements(RUN_OWNED_FOREIGN_KEY_TABLES.map((config) => config.table)),
|
|
432
|
+
destructive: true,
|
|
433
|
+
isApplied: (sqlite) => RUN_OWNED_FOREIGN_KEY_TABLES.every((config) => tableHasRunForeignKey(sqlite, config.table)),
|
|
434
|
+
up: (sqlite) => {
|
|
435
|
+
const tables = RUN_OWNED_FOREIGN_KEY_TABLES.map((config) => rebuildRunOwnedForeignKeyTable(sqlite, config, context.createTableStatements));
|
|
436
|
+
const violations = sqlite.query("PRAGMA foreign_key_check").all();
|
|
437
|
+
if (violations.length > 0) {
|
|
438
|
+
throw new Error(`SQLite foreign key check failed after Smithers schema migration: ${JSON.stringify(violations)}`);
|
|
439
|
+
}
|
|
440
|
+
return { tables };
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
id: "0014_current_indexes",
|
|
445
|
+
name: "Create current Smithers indexes",
|
|
446
|
+
checksum: checksumForStatements([...context.createIndexStatements, ...EXTRA_INDEX_STATEMENTS]),
|
|
447
|
+
isApplied: () => false,
|
|
448
|
+
up: (sqlite) => {
|
|
449
|
+
for (const statement of [...context.createIndexStatements, ...EXTRA_INDEX_STATEMENTS]) {
|
|
450
|
+
sqlite.run(statement);
|
|
451
|
+
}
|
|
452
|
+
return { statementCount: context.createIndexStatements.length + EXTRA_INDEX_STATEMENTS.length };
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* @param {import("bun:sqlite").Database} sqlite
|
|
460
|
+
* @param {{
|
|
461
|
+
* createTableStatements: readonly string[];
|
|
462
|
+
* createIndexStatements: readonly string[];
|
|
463
|
+
* }} context
|
|
464
|
+
*/
|
|
465
|
+
export function runSmithersSchemaMigrations(sqlite, context) {
|
|
466
|
+
sqlite.run("PRAGMA foreign_keys = ON");
|
|
467
|
+
sqlite.run(MIGRATION_TABLE_SQL);
|
|
468
|
+
const applied = loadAppliedMigrationIds(sqlite);
|
|
469
|
+
for (const migration of buildMigrations(context)) {
|
|
470
|
+
if (applied.has(migration.id) || hasMigrationRecord(sqlite, migration.id)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const alreadyApplied = migration.isApplied(sqlite);
|
|
474
|
+
const details = alreadyApplied
|
|
475
|
+
? { skipped: "schema_already_matches" }
|
|
476
|
+
: migration.up(sqlite);
|
|
477
|
+
logDestructiveMigration(migration, details);
|
|
478
|
+
recordMigration(sqlite, migration, details);
|
|
479
|
+
applied.add(migration.id);
|
|
480
|
+
}
|
|
481
|
+
}
|