@morlay/session-rdb 0.0.15 → 0.0.16-alpha.1

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.
Files changed (65) hide show
  1. package/README.md +11 -13
  2. package/dist/artifact.d.mts +30 -16
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{import-DFed8DBt.mjs → import-mZZgDBLd.mjs} +73 -83
  5. package/dist/import.d.mts +2 -3
  6. package/dist/import.mjs +2 -2
  7. package/dist/index-GdKkSSb-.d.mts +239 -0
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +763 -168
  10. package/dist/log-DBFUBPhv.mjs +394 -0
  11. package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
  12. package/dist/sqlite-CnWwGToZ.mjs +698 -0
  13. package/dist/storage.d.mts +2 -7
  14. package/dist/storage.mjs +2 -3
  15. package/dist/testing.d.mts +30 -1
  16. package/dist/testing.mjs +676 -1991
  17. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
  18. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
  19. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
  20. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
  21. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
  22. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
  23. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
  24. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
  25. package/package.json +17 -12
  26. package/src/adapters/index.ts +10 -2
  27. package/src/adapters/to-postgres.ts +19 -15
  28. package/src/adapters/to-sqlite.ts +20 -14
  29. package/src/{entities → adapters}/types.ts +7 -8
  30. package/src/backend.ts +0 -7
  31. package/src/branch.ts +29 -110
  32. package/src/drizzle/postgres-v2.ts +11 -0
  33. package/src/drizzle/postgres-v3.ts +11 -0
  34. package/src/drizzle/sqlite-v2.ts +10 -0
  35. package/src/drizzle/sqlite-v3.ts +11 -0
  36. package/src/entities/index.ts +2 -30
  37. package/src/entities/v2/index.ts +20 -0
  38. package/src/entities/v2/session-events.ts +11 -0
  39. package/src/entities/v3/events.ts +27 -0
  40. package/src/entities/v3/index.ts +27 -0
  41. package/src/entities/v3/persistence-state.ts +10 -0
  42. package/src/entities/v3/schema-meta.ts +9 -0
  43. package/src/entities/v3/session-events.ts +26 -0
  44. package/src/entities/v3/sessions.ts +20 -0
  45. package/src/import.ts +65 -57
  46. package/src/index.ts +929 -227
  47. package/src/invariant.ts +0 -2
  48. package/src/legacy.ts +110 -0
  49. package/src/log.ts +143 -91
  50. package/src/postgres.ts +75 -93
  51. package/src/schema.ts +3 -14
  52. package/src/sqlite.ts +59 -46
  53. package/src/testing/contract.ts +460 -375
  54. package/src/testing/coordinator-contract.ts +108 -1374
  55. package/src/testing.ts +1 -6
  56. package/dist/index-uUvn6gYp.d.mts +0 -111
  57. package/dist/schema-vwzwEXNE.mjs +0 -878
  58. package/dist/sqlite-CG_Qcx5C.mjs +0 -356
  59. package/src/adapters/ddl.ts +0 -77
  60. package/src/entities/events.ts +0 -27
  61. package/src/entities/persistence-state.ts +0 -10
  62. package/src/entities/schema-meta.ts +0 -9
  63. package/src/entities/session-events.ts +0 -29
  64. package/src/entities/sessions.ts +0 -20
  65. package/src/migrate.ts +0 -137
@@ -1,356 +0,0 @@
1
- import { A as sessionInsertRow, a as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, d as tPersistenceState, f as tSessionEvents, h as sqliteTableDefs, k as sessionConflictRow, o as eventDimensions, p as tSessions, t as DEFAULT_BUSY_TIMEOUT_MS, u as tEvents } from "./schema-vwzwEXNE.mjs";
2
- import { randomUUID } from "node:crypto";
3
- import { and, desc, eq, gte, sql } from "drizzle-orm";
4
- import { statSync } from "node:fs";
5
- import { mkdir, open } from "node:fs/promises";
6
- import { dirname, resolve } from "node:path";
7
- import { DatabaseSync } from "node:sqlite";
8
- import { drizzle } from "drizzle-orm/node-sqlite";
9
- //#region src/write-guard.ts
10
- var WriteGuard = class {
11
- headSeqs = /* @__PURE__ */ new Map();
12
- confirmHead(id, head) {
13
- this.headSeqs.set(id, head);
14
- }
15
- assertNoConcurrentWriter(id, storedHead) {
16
- const known = this.headSeqs.get(id);
17
- if (known === void 0) {
18
- if (storedHead !== -1) throw new Error(`session "${id}" has a persisted log this instance has not read; another writer may own it — load the session first`);
19
- return;
20
- }
21
- if (known !== storedHead) throw new Error(`session "${id}" was modified by another writer (stored head ${storedHead}, this instance last confirmed head ${known}); concurrent writers on one session are not supported`);
22
- }
23
- };
24
- //#endregion
25
- //#region src/adapters/ddl.ts
26
- function sqlType(dialect, type) {
27
- switch (type) {
28
- case "serial": return dialect === "sqlite" ? "INTEGER" : "SERIAL";
29
- case "integer": return "INTEGER";
30
- case "bigint": return dialect === "sqlite" ? "INTEGER" : "BIGINT";
31
- case "text": return "TEXT";
32
- }
33
- }
34
- function literal(value) {
35
- return typeof value === "string" ? `'${value.replace(/'/g, "''")}'` : String(value);
36
- }
37
- function quote(name) {
38
- return `"${name}"`;
39
- }
40
- function columnSql(dialect, c) {
41
- let sql = `${quote(c.name)} ${sqlType(dialect, c.type)}`;
42
- if (c.primaryKey) sql += " PRIMARY KEY";
43
- if (c.type === "serial" && dialect === "sqlite") sql += " AUTOINCREMENT";
44
- if (c.notNull) sql += " NOT NULL";
45
- if (c.default !== void 0) sql += ` DEFAULT ${literal(c.default)}`;
46
- if (c.unique) sql += " UNIQUE";
47
- if (c.references) {
48
- sql += ` REFERENCES ${quote(c.references.table)}(${quote(c.references.column)})`;
49
- if (c.references.onDelete) sql += ` ON DELETE ${c.references.onDelete.toUpperCase()}`;
50
- }
51
- return sql;
52
- }
53
- function createTableSql(dialect, def, schema) {
54
- const parts = def.columns.map((c) => columnSql(dialect, c));
55
- for (const ck of def.checks ?? []) parts.push(`CHECK (${ck.expression})`);
56
- for (const u of def.uniques ?? []) parts.push(`UNIQUE (${u.columns.map(quote).join(", ")})`);
57
- const strict = dialect === "sqlite" ? " STRICT" : "";
58
- return `CREATE TABLE IF NOT EXISTS ${schema === void 0 || schema === "public" ? quote(def.name) : `${quote(schema)}.${quote(def.name)}`} (\n ${parts.join(",\n ")}\n)${strict}`;
59
- }
60
- function createIndexSql(def, name, schema) {
61
- const idx = def.indexes?.find((i) => i.name === name);
62
- if (idx === void 0) throw new Error(`unknown index "${name}" on table "${def.name}"`);
63
- const qualified = schema === void 0 || schema === "public" ? quote(def.name) : `${quote(schema)}.${quote(def.name)}`;
64
- return `CREATE INDEX IF NOT EXISTS ${quote(idx.name)} ON ${qualified}(${idx.columns.map(quote).join(", ")})`;
65
- }
66
- function createTablesSql(dialect, defs, schema) {
67
- const statements = [];
68
- for (const def of defs) {
69
- statements.push(createTableSql(dialect, def, schema));
70
- for (const idx of def.indexes ?? []) statements.push(createIndexSql(def, idx.name, schema));
71
- }
72
- return statements;
73
- }
74
- function migrateSqliteV1ToV2(db) {
75
- const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
76
- if (onDisk === 2) return 0;
77
- if (onDisk !== 1) throw new Error(`unsupported schema version ${onDisk} (expected 1)`);
78
- db.exec(`
79
- CREATE TABLE t_events_v2 (
80
- f_id INTEGER PRIMARY KEY AUTOINCREMENT,
81
- f_event_id TEXT NOT NULL UNIQUE,
82
- f_parent_id TEXT NOT NULL DEFAULT '',
83
- f_type TEXT NOT NULL DEFAULT '',
84
- f_kind TEXT NOT NULL DEFAULT '',
85
- f_role TEXT NOT NULL DEFAULT '',
86
- f_name TEXT NOT NULL DEFAULT '',
87
- f_action_id TEXT NOT NULL DEFAULT '',
88
- f_encoding TEXT NOT NULL DEFAULT '',
89
- f_data TEXT NOT NULL,
90
- f_created_at INTEGER NOT NULL DEFAULT 0
91
- ) STRICT;
92
- CREATE TABLE t_session_events_v2 (
93
- f_id INTEGER PRIMARY KEY AUTOINCREMENT,
94
- f_session_id TEXT NOT NULL REFERENCES t_sessions(f_session_id) ON DELETE CASCADE,
95
- f_event_id TEXT NOT NULL REFERENCES t_events_v2(f_event_id) ON DELETE CASCADE,
96
- f_sequence INTEGER NOT NULL,
97
- f_original_seq INTEGER NOT NULL,
98
- f_surface_op TEXT,
99
- UNIQUE (f_session_id, f_sequence)
100
- ) STRICT;
101
- CREATE INDEX idx_events_v2_kind ON t_events_v2(f_kind);
102
- CREATE INDEX idx_events_v2_role ON t_events_v2(f_role);
103
- CREATE INDEX idx_events_v2_name ON t_events_v2(f_name);
104
- CREATE INDEX idx_events_v2_action_id ON t_events_v2(f_action_id);
105
- CREATE INDEX idx_session_events_v2_event_id ON t_session_events_v2(f_event_id);
106
- `);
107
- const oldEvents = db.prepare(`SELECT f_event_id, f_parent_id, f_kind, f_data, f_created_at FROM t_events ORDER BY f_id`).all();
108
- const insertEvent = db.prepare(`
109
- INSERT INTO t_events_v2
110
- (f_event_id, f_parent_id, f_type, f_kind, f_role, f_name, f_action_id, f_encoding, f_data, f_created_at)
111
- VALUES (?, ?, ?, ?, ?, ?, ?, 'json', ?, ?)
112
- `);
113
- for (const row of oldEvents) {
114
- let data;
115
- try {
116
- data = JSON.parse(row.f_data);
117
- } catch {
118
- throw new Error(`t_events row ${row.f_event_id} has unparsable f_data; aborting`);
119
- }
120
- const dims = eventDimensions({
121
- type: row.f_kind,
122
- seq: 0,
123
- time: row.f_created_at,
124
- data
125
- });
126
- insertEvent.run(row.f_event_id, row.f_parent_id, row.f_kind, dims.kind, dims.role, dims.name, dims.actionId, row.f_data, row.f_created_at);
127
- }
128
- const oldBridges = db.prepare(`SELECT se.f_session_id, se.f_event_id, se.f_sequence
129
- FROM t_session_events se ORDER BY se.f_id`).all();
130
- const oldEventMeta = new Map(db.prepare(`SELECT f_event_id, f_original_seq, f_surface_op FROM t_events`).all().map((row) => [row.f_event_id, row]));
131
- const insertBridge = db.prepare(`
132
- INSERT INTO t_session_events_v2
133
- (f_session_id, f_event_id, f_sequence, f_original_seq, f_surface_op)
134
- VALUES (?, ?, ?, ?, ?)
135
- `);
136
- for (const row of oldBridges) {
137
- const meta = oldEventMeta.get(row.f_event_id);
138
- if (meta === void 0) throw new Error(`bridge row ${row.f_session_id}:${row.f_sequence} references missing event ${row.f_event_id}`);
139
- insertBridge.run(row.f_session_id, row.f_event_id, row.f_sequence, meta.f_original_seq, meta.f_surface_op);
140
- }
141
- db.exec(`
142
- DROP TABLE t_session_events;
143
- DROP TABLE t_events;
144
- ALTER TABLE t_events_v2 RENAME TO t_events;
145
- ALTER TABLE t_session_events_v2 RENAME TO t_session_events;
146
- `);
147
- db.exec(`PRAGMA user_version = 2`);
148
- return oldEvents.length;
149
- }
150
- //#endregion
151
- //#region src/sqlite.ts
152
- const sqliteTxQueues = /* @__PURE__ */ new Map();
153
- function enqueueSqliteTx(path, fn) {
154
- const run = (sqliteTxQueues.get(path) ?? Promise.resolve()).then(fn);
155
- sqliteTxQueues.set(path, run.then(() => void 0, () => void 0));
156
- return run;
157
- }
158
- async function createDatabaseFile(path) {
159
- try {
160
- await (await open(path, "wx", 384)).close();
161
- } catch (error) {
162
- if (error.code !== "EEXIST") throw error;
163
- }
164
- }
165
- function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS) {
166
- const db = new DatabaseSync(path);
167
- try {
168
- configureDatabase(db, path, journalMode, busyTimeout);
169
- return db;
170
- } catch (error) {
171
- db.close();
172
- throw error;
173
- }
174
- }
175
- function configureDatabase(db, path, journalMode, busyTimeout) {
176
- db.exec("PRAGMA foreign_keys = ON");
177
- db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
178
- drizzle({ client: db }).transaction((tx) => {
179
- const { user_version: onDisk } = tx.get(sql`PRAGMA user_version`);
180
- const { application_id: applicationId } = tx.get(sql`PRAGMA application_id`);
181
- const { count: userObjectCount } = tx.get(sql`SELECT COUNT(*) AS count FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*'`);
182
- if (onDisk === 0 && (applicationId !== 0 || userObjectCount > 0)) throw new Error(`session database at "${path}" has an unversioned schema or application identity`);
183
- if (onDisk === 1) migrateSqliteV1ToV2(db);
184
- else if (onDisk !== 0 && onDisk !== 2) throw new Error(`session database at "${path}" has schema version ${onDisk}, incompatible with this build (2)`);
185
- if ((onDisk === 2 || onDisk === 1) && applicationId !== 1146308688) throw new Error(`session database at "${path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
186
- for (const statement of createTablesSql("sqlite", sqliteTableDefs)) tx.run(sql.raw(statement));
187
- tx.insert(tPersistenceState).values({
188
- fSingleton: 1,
189
- fStoreId: randomUUID()
190
- }).onConflictDoNothing().run();
191
- if (onDisk === 0) {
192
- tx.run(sql.raw(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`));
193
- tx.run(sql.raw(`PRAGMA user_version = 2`));
194
- }
195
- }, { behavior: "immediate" });
196
- db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
197
- }
198
- var SqliteBackend = class SqliteBackend {
199
- options;
200
- kind = "sqlite";
201
- storeIdentity;
202
- dbPath = "";
203
- db;
204
- constructor(options) {
205
- this.options = options;
206
- }
207
- async open() {
208
- const actual = this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
209
- this.dbPath = actual;
210
- if (actual !== ":memory:") {
211
- await mkdir(dirname(actual), {
212
- recursive: true,
213
- mode: 448
214
- });
215
- await createDatabaseFile(actual);
216
- }
217
- await enqueueSqliteTx(actual, async () => {
218
- this.db = drizzle({ client: openDatabase(actual, this.options.journalMode, this.options.busyTimeout) });
219
- });
220
- try {
221
- const row = this.db.select({ fStoreId: tPersistenceState.fStoreId }).from(tPersistenceState).where(eq(tPersistenceState.fSingleton, 1)).get();
222
- /* v8 ignore next -- openDatabase inserts the singleton before returning. */
223
- if (row === void 0) throw new Error(`session database at "${actual}" has no store identity`);
224
- if (row.fStoreId.length === 0) throw new Error(`session database at "${actual}" has no valid store identity`);
225
- if (actual !== ":memory:") {
226
- const identity = statSync(actual, { bigint: true });
227
- this.storeIdentity = `file:${identity.dev}:${identity.ino}:${identity.birthtimeNs}:store:${row.fStoreId}`;
228
- } else this.storeIdentity = `memory:store:${row.fStoreId}`;
229
- } catch (error) {
230
- this.db.$client.close();
231
- throw error;
232
- }
233
- }
234
- async close() {
235
- if (this.db === void 0) return;
236
- this.db.$client.close();
237
- }
238
- async getSession(id) {
239
- return this.db.select().from(tSessions).where(eq(tSessions.fSessionId, id)).get();
240
- }
241
- async getSeqMapRows(id) {
242
- return this.db.select({
243
- fSequence: tSessionEvents.fSequence,
244
- fOriginalSeq: tSessionEvents.fOriginalSeq
245
- }).from(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).all();
246
- }
247
- async getEventRows(id, fromSequence) {
248
- return (fromSequence === void 0 ? this.eventRows().where(eq(tSessionEvents.fSessionId, id)) : this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))).orderBy(tSessionEvents.fSequence).all();
249
- }
250
- async listSessions() {
251
- return this.db.select().from(tSessions).all();
252
- }
253
- async transaction(fn) {
254
- return enqueueSqliteTx(this.dbPath, async () => {
255
- this.db.$client.exec("BEGIN IMMEDIATE");
256
- try {
257
- const result = await fn(this.tx);
258
- this.db.$client.exec("COMMIT");
259
- return result;
260
- } catch (error) {
261
- /* v8 ignore start */
262
- try {
263
- this.db.$client.exec("ROLLBACK");
264
- } catch {}
265
- throw error;
266
- }
267
- });
268
- }
269
- tx = {
270
- upsertSession: (storage, incarnation) => this.upsertSession(storage, incarnation),
271
- getHead: (id) => this.getHead(id),
272
- getSeedLength: (id) => this.getSeedLength(id),
273
- updateSeedLength: (id, seedLength) => this.updateSeedLength(id, seedLength),
274
- insertEvents: (events) => this.insertEvents(events),
275
- insertBridges: (rows) => this.insertBridges(rows),
276
- updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
277
- bumpRevision: (id) => this.bumpRevision(id),
278
- deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
279
- getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
280
- getLastBridge: (id) => this.getLastBridge(id)
281
- };
282
- async upsertSession(storage, incarnation) {
283
- this.db.insert(tSessions).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
284
- target: tSessions.fSessionId,
285
- set: sessionConflictRow(storage)
286
- }).run();
287
- }
288
- async getHead(id) {
289
- const head = this.db.select({
290
- fHeadEventId: tSessions.fHeadEventId,
291
- fHeadSequence: tSessions.fHeadSequence
292
- }).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
293
- /* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
294
- if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
295
- return head;
296
- }
297
- async getSeedLength(id) {
298
- const row = this.db.select({ fSeedLength: tSessions.fSeedLength }).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
299
- /* v8 ignore next -- rewind always materializes the row before reading the seed length */
300
- if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
301
- return row.fSeedLength;
302
- }
303
- async updateSeedLength(id, seedLength) {
304
- this.db.update(tSessions).set({ fSeedLength: seedLength }).where(eq(tSessions.fSessionId, id)).run();
305
- }
306
- static INSERT_BATCH_ROWS = 1e3;
307
- async insertEvents(events) {
308
- if (events.length === 0) return;
309
- for (let i = 0; i < events.length; i += SqliteBackend.INSERT_BATCH_ROWS) this.db.insert(tEvents).values(events.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).run();
310
- }
311
- async insertBridges(rows) {
312
- if (rows.length === 0) return;
313
- for (let i = 0; i < rows.length; i += SqliteBackend.INSERT_BATCH_ROWS) this.db.insert(tSessionEvents).values(rows.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).run();
314
- }
315
- async updateHead(id, headEventId, headSequence) {
316
- this.db.update(tSessions).set({
317
- fHeadEventId: headEventId,
318
- fHeadSequence: headSequence
319
- }).where(eq(tSessions.fSessionId, id)).run();
320
- }
321
- async bumpRevision(id) {
322
- this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
323
- }
324
- async deleteBridgeTail(id, fromSequence) {
325
- this.db.delete(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence))).run();
326
- }
327
- async getPrevBridge(id, sequence) {
328
- return this.db.select({
329
- fEventId: tSessionEvents.fEventId,
330
- fSequence: tSessionEvents.fSequence
331
- }).from(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get();
332
- }
333
- async getLastBridge(id) {
334
- return this.db.select({
335
- fEventId: tSessionEvents.fEventId,
336
- fSequence: tSessionEvents.fSequence
337
- }).from(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).orderBy(desc(tSessionEvents.fSequence)).limit(1).get();
338
- }
339
- eventRows() {
340
- return this.db.select({
341
- fEventId: tSessionEvents.fEventId,
342
- fSequence: tSessionEvents.fSequence,
343
- fOriginalSeq: tSessionEvents.fOriginalSeq,
344
- fType: tEvents.fType,
345
- fKind: tEvents.fKind,
346
- fRole: tEvents.fRole,
347
- fName: tEvents.fName,
348
- fActionId: tEvents.fActionId,
349
- fCreatedAt: tEvents.fCreatedAt,
350
- fData: tEvents.fData,
351
- fSurfaceOp: tSessionEvents.fSurfaceOp
352
- }).from(tSessionEvents).innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId));
353
- }
354
- };
355
- //#endregion
356
- export { WriteGuard as i, openDatabase as n, createTablesSql as r, SqliteBackend as t };
@@ -1,77 +0,0 @@
1
- import type { ColumnDef, TableDef } from "../entities/types.ts";
2
-
3
- export type Dialect = "sqlite" | "postgres";
4
-
5
- function sqlType(dialect: Dialect, type: ColumnDef["type"]): string {
6
- switch (type) {
7
- case "serial":
8
- return dialect === "sqlite" ? "INTEGER" : "SERIAL";
9
- case "integer":
10
- return "INTEGER";
11
- case "bigint":
12
- return dialect === "sqlite" ? "INTEGER" : "BIGINT";
13
- case "text":
14
- return "TEXT";
15
- }
16
- }
17
-
18
- function literal(value: string | number): string {
19
- return typeof value === "string" ? `'${value.replace(/'/g, "''")}'` : String(value);
20
- }
21
-
22
- function quote(name: string): string {
23
- return `"${name}"`;
24
- }
25
-
26
- function columnSql(dialect: Dialect, c: ColumnDef): string {
27
- let sql = `${quote(c.name)} ${sqlType(dialect, c.type)}`;
28
- if (c.primaryKey) sql += " PRIMARY KEY";
29
- if (c.type === "serial" && dialect === "sqlite") sql += " AUTOINCREMENT";
30
- if (c.notNull) sql += " NOT NULL";
31
- if (c.default !== undefined) sql += ` DEFAULT ${literal(c.default)}`;
32
- if (c.unique) sql += " UNIQUE";
33
- if (c.references) {
34
- sql += ` REFERENCES ${quote(c.references.table)}(${quote(c.references.column)})`;
35
- if (c.references.onDelete) sql += ` ON DELETE ${c.references.onDelete.toUpperCase()}`;
36
- }
37
- return sql;
38
- }
39
-
40
- export function createTableSql(dialect: Dialect, def: TableDef, schema?: string): string {
41
- const parts = def.columns.map((c) => columnSql(dialect, c));
42
- for (const ck of def.checks ?? []) parts.push(`CHECK (${ck.expression})`);
43
- for (const u of def.uniques ?? []) {
44
- parts.push(`UNIQUE (${u.columns.map(quote).join(", ")})`);
45
- }
46
- const strict = dialect === "sqlite" ? " STRICT" : "";
47
- const qualified =
48
- schema === undefined || schema === "public"
49
- ? quote(def.name)
50
- : `${quote(schema)}.${quote(def.name)}`;
51
- return `CREATE TABLE IF NOT EXISTS ${qualified} (\n ${parts.join(",\n ")}\n)${strict}`;
52
- }
53
-
54
- export function createIndexSql(def: TableDef, name: string, schema?: string): string {
55
- const idx = def.indexes?.find((i) => i.name === name);
56
- if (idx === undefined) throw new Error(`unknown index "${name}" on table "${def.name}"`);
57
- const qualified =
58
- schema === undefined || schema === "public"
59
- ? quote(def.name)
60
- : `${quote(schema)}.${quote(def.name)}`;
61
- return `CREATE INDEX IF NOT EXISTS ${quote(idx.name)} ON ${qualified}(${idx.columns
62
- .map(quote)
63
- .join(", ")})`;
64
- }
65
-
66
- export function createTablesSql(
67
- dialect: Dialect,
68
- defs: readonly TableDef[],
69
- schema?: string,
70
- ): string[] {
71
- const statements: string[] = [];
72
- for (const def of defs) {
73
- statements.push(createTableSql(dialect, def, schema));
74
- for (const idx of def.indexes ?? []) statements.push(createIndexSql(def, idx.name, schema));
75
- }
76
- return statements;
77
- }
@@ -1,27 +0,0 @@
1
- import type { TableDef } from "./types.ts";
2
-
3
- export const events: TableDef = {
4
- name: "t_events",
5
- columns: [
6
- { name: "f_id", type: "serial", primaryKey: true },
7
- { name: "f_event_id", type: "text", notNull: true, unique: true },
8
- { name: "f_parent_id", type: "text", notNull: true, default: "" },
9
- { name: "f_type", type: "text", notNull: true, default: "" },
10
- { name: "f_kind", type: "text", notNull: true, default: "" },
11
- { name: "f_role", type: "text", notNull: true, default: "" },
12
- { name: "f_name", type: "text", notNull: true, default: "" },
13
- { name: "f_action_id", type: "text", notNull: true, default: "" },
14
- { name: "f_encoding", type: "text", notNull: true, default: "" },
15
- { name: "f_data", type: "text", notNull: true },
16
- { name: "f_created_at", type: "bigint", notNull: true, default: 0 },
17
- ],
18
- // 查询经 f_event_id(列级 UNIQUE 唯一索引)与 t_session_events 复合索引
19
- // (按 session 过滤后回表);f_parent_id 仅写路径构造;维度列索引为
20
- // 审计/UI 过滤预留。
21
- indexes: [
22
- { name: "idx_events_kind", columns: ["f_kind"] },
23
- { name: "idx_events_role", columns: ["f_role"] },
24
- { name: "idx_events_name", columns: ["f_name"] },
25
- { name: "idx_events_action_id", columns: ["f_action_id"] },
26
- ],
27
- };
@@ -1,10 +0,0 @@
1
- import type { TableDef } from "./types.ts";
2
-
3
- export const persistenceState: TableDef = {
4
- name: "t_persistence_state",
5
- columns: [
6
- { name: "f_singleton", type: "integer", primaryKey: true },
7
- { name: "f_store_id", type: "text", notNull: true },
8
- ],
9
- checks: [{ name: "ck_persistence_state_singleton", expression: "f_singleton = 1" }],
10
- };
@@ -1,9 +0,0 @@
1
- import type { TableDef } from "./types.ts";
2
-
3
- export const schemaMeta: TableDef = {
4
- name: "t_schema_meta",
5
- columns: [
6
- { name: "f_key", type: "text", primaryKey: true },
7
- { name: "f_value", type: "text", notNull: true },
8
- ],
9
- };
@@ -1,29 +0,0 @@
1
- import type { TableDef } from "./types.ts";
2
-
3
- export const sessionEvents: TableDef = {
4
- name: "t_session_events",
5
- columns: [
6
- { name: "f_id", type: "serial", primaryKey: true },
7
- {
8
- name: "f_session_id",
9
- type: "text",
10
- notNull: true,
11
- references: { table: "t_sessions", column: "f_session_id", onDelete: "cascade" },
12
- },
13
- {
14
- name: "f_event_id",
15
- type: "text",
16
- notNull: true,
17
- references: { table: "t_events", column: "f_event_id", onDelete: "cascade" },
18
- },
19
- { name: "f_sequence", type: "integer", notNull: true },
20
- { name: "f_original_seq", type: "integer", notNull: true },
21
- { name: "f_surface_op", type: "text" },
22
- ],
23
- uniques: [
24
- { name: "uq_session_events_session_sequence", columns: ["f_session_id", "f_sequence"] },
25
- ],
26
- // UNIQUE(f_session_id, f_sequence) 自动建唯一索引(按 session 过滤 + seq
27
- // 范围/排序/取尾);f_event_id 索引覆盖反向查找(孤儿事件行清理)。
28
- indexes: [{ name: "idx_session_events_event_id", columns: ["f_event_id"] }],
29
- };
@@ -1,20 +0,0 @@
1
- import type { TableDef } from "./types.ts";
2
-
3
- export const sessions: TableDef = {
4
- name: "t_sessions",
5
- columns: [
6
- { name: "f_id", type: "serial", primaryKey: true },
7
- { name: "f_session_id", type: "text", notNull: true, unique: true },
8
- { name: "f_head_event_id", type: "text", notNull: true, default: "" },
9
- { name: "f_head_sequence", type: "integer", notNull: true, default: -1 },
10
- { name: "f_version", type: "integer", notNull: true },
11
- { name: "f_created_at", type: "bigint", notNull: true },
12
- { name: "f_cwd", type: "text" },
13
- { name: "f_parent_session", type: "text" },
14
- { name: "f_seed_length", type: "integer" },
15
- { name: "f_origin", type: "text" },
16
- { name: "f_delegation_depth", type: "integer" },
17
- { name: "f_incarnation", type: "text", notNull: true },
18
- { name: "f_revision", type: "integer", notNull: true },
19
- ],
20
- };
package/src/migrate.ts DELETED
@@ -1,137 +0,0 @@
1
- import { DatabaseSync } from "node:sqlite";
2
- import { eventDimensions } from "./schema.ts";
3
-
4
- export const SCHEMA_VERSION_V2 = 2;
5
-
6
- export function migrateSqliteV1ToV2(db: DatabaseSync): number {
7
- const { user_version: onDisk } = db.prepare("PRAGMA user_version").get() as {
8
- user_version: number;
9
- };
10
- if (onDisk === SCHEMA_VERSION_V2) return 0;
11
- if (onDisk !== 1) {
12
- throw new Error(`unsupported schema version ${onDisk} (expected 1)`);
13
- }
14
-
15
- // 1. 建 v2 表(临时名)。
16
- db.exec(`
17
- CREATE TABLE t_events_v2 (
18
- f_id INTEGER PRIMARY KEY AUTOINCREMENT,
19
- f_event_id TEXT NOT NULL UNIQUE,
20
- f_parent_id TEXT NOT NULL DEFAULT '',
21
- f_type TEXT NOT NULL DEFAULT '',
22
- f_kind TEXT NOT NULL DEFAULT '',
23
- f_role TEXT NOT NULL DEFAULT '',
24
- f_name TEXT NOT NULL DEFAULT '',
25
- f_action_id TEXT NOT NULL DEFAULT '',
26
- f_encoding TEXT NOT NULL DEFAULT '',
27
- f_data TEXT NOT NULL,
28
- f_created_at INTEGER NOT NULL DEFAULT 0
29
- ) STRICT;
30
- CREATE TABLE t_session_events_v2 (
31
- f_id INTEGER PRIMARY KEY AUTOINCREMENT,
32
- f_session_id TEXT NOT NULL REFERENCES t_sessions(f_session_id) ON DELETE CASCADE,
33
- f_event_id TEXT NOT NULL REFERENCES t_events_v2(f_event_id) ON DELETE CASCADE,
34
- f_sequence INTEGER NOT NULL,
35
- f_original_seq INTEGER NOT NULL,
36
- f_surface_op TEXT,
37
- UNIQUE (f_session_id, f_sequence)
38
- ) STRICT;
39
- CREATE INDEX idx_events_v2_kind ON t_events_v2(f_kind);
40
- CREATE INDEX idx_events_v2_role ON t_events_v2(f_role);
41
- CREATE INDEX idx_events_v2_name ON t_events_v2(f_name);
42
- CREATE INDEX idx_events_v2_action_id ON t_events_v2(f_action_id);
43
- CREATE INDEX idx_session_events_v2_event_id ON t_session_events_v2(f_event_id);
44
- `);
45
-
46
- // 2. 搬运 t_events:f_type = 旧 f_kind,维度列重算。
47
- const oldEvents = db
48
- .prepare(
49
- `SELECT f_event_id, f_parent_id, f_kind, f_data, f_created_at FROM t_events ORDER BY f_id`,
50
- )
51
- .all() as Array<{
52
- f_event_id: string;
53
- f_parent_id: string;
54
- f_kind: string;
55
- f_data: string;
56
- f_created_at: number;
57
- }>;
58
- const insertEvent = db.prepare(`
59
- INSERT INTO t_events_v2
60
- (f_event_id, f_parent_id, f_type, f_kind, f_role, f_name, f_action_id, f_encoding, f_data, f_created_at)
61
- VALUES (?, ?, ?, ?, ?, ?, ?, 'json', ?, ?)
62
- `);
63
- for (const row of oldEvents) {
64
- let data: unknown;
65
- try {
66
- data = JSON.parse(row.f_data);
67
- } catch {
68
- throw new Error(`t_events row ${row.f_event_id} has unparsable f_data; aborting`);
69
- }
70
- const dims = eventDimensions({
71
- type: row.f_kind,
72
- seq: 0,
73
- time: row.f_created_at,
74
- data,
75
- } as never);
76
- insertEvent.run(
77
- row.f_event_id,
78
- row.f_parent_id,
79
- row.f_kind,
80
- dims.kind,
81
- dims.role,
82
- dims.name,
83
- dims.actionId,
84
- row.f_data,
85
- row.f_created_at,
86
- );
87
- }
88
-
89
- // 3. 搬运 t_session_events:f_original_seq / f_surface_op 从旧 t_events 取。
90
- const oldBridges = db
91
- .prepare(
92
- `SELECT se.f_session_id, se.f_event_id, se.f_sequence
93
- FROM t_session_events se ORDER BY se.f_id`,
94
- )
95
- .all() as Array<{ f_session_id: string; f_event_id: string; f_sequence: number }>;
96
- const oldEventMeta = new Map(
97
- (
98
- db.prepare(`SELECT f_event_id, f_original_seq, f_surface_op FROM t_events`).all() as Array<{
99
- f_event_id: string;
100
- f_original_seq: number;
101
- f_surface_op: string | null;
102
- }>
103
- ).map((row) => [row.f_event_id, row]),
104
- );
105
- const insertBridge = db.prepare(`
106
- INSERT INTO t_session_events_v2
107
- (f_session_id, f_event_id, f_sequence, f_original_seq, f_surface_op)
108
- VALUES (?, ?, ?, ?, ?)
109
- `);
110
- for (const row of oldBridges) {
111
- const meta = oldEventMeta.get(row.f_event_id);
112
- if (meta === undefined) {
113
- throw new Error(
114
- `bridge row ${row.f_session_id}:${row.f_sequence} references missing event ${row.f_event_id}`,
115
- );
116
- }
117
- insertBridge.run(
118
- row.f_session_id,
119
- row.f_event_id,
120
- row.f_sequence,
121
- meta.f_original_seq,
122
- meta.f_surface_op,
123
- );
124
- }
125
-
126
- // 4. 删旧表、重命名新表。
127
- db.exec(`
128
- DROP TABLE t_session_events;
129
- DROP TABLE t_events;
130
- ALTER TABLE t_events_v2 RENAME TO t_events;
131
- ALTER TABLE t_session_events_v2 RENAME TO t_session_events;
132
- `);
133
-
134
- // 5. user_version = 2。
135
- db.exec(`PRAGMA user_version = ${SCHEMA_VERSION_V2}`);
136
- return oldEvents.length;
137
- }