@loomwork/plugin-sqlite 0.0.0-stage → 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +62 -2
  4. package/dist/SqliteArchiveStore.d.ts +22 -0
  5. package/dist/SqliteArchiveStore.d.ts.map +1 -0
  6. package/dist/SqliteArchiveStore.js +129 -0
  7. package/dist/SqliteArchiveStore.js.map +1 -0
  8. package/dist/SqliteBranchRegistry.d.ts +26 -0
  9. package/dist/SqliteBranchRegistry.d.ts.map +1 -0
  10. package/dist/SqliteBranchRegistry.js +154 -0
  11. package/dist/SqliteBranchRegistry.js.map +1 -0
  12. package/dist/SqliteContentStore.d.ts +55 -0
  13. package/dist/SqliteContentStore.d.ts.map +1 -0
  14. package/dist/SqliteContentStore.js +150 -0
  15. package/dist/SqliteContentStore.js.map +1 -0
  16. package/dist/SqliteDurableStateStore.d.ts +26 -0
  17. package/dist/SqliteDurableStateStore.d.ts.map +1 -0
  18. package/dist/SqliteDurableStateStore.js +81 -0
  19. package/dist/SqliteDurableStateStore.js.map +1 -0
  20. package/dist/SqliteEventStore.d.ts +55 -0
  21. package/dist/SqliteEventStore.d.ts.map +1 -0
  22. package/dist/SqliteEventStore.js +297 -0
  23. package/dist/SqliteEventStore.js.map +1 -0
  24. package/dist/SqliteLeaseStore.d.ts +25 -0
  25. package/dist/SqliteLeaseStore.d.ts.map +1 -0
  26. package/dist/SqliteLeaseStore.js +107 -0
  27. package/dist/SqliteLeaseStore.js.map +1 -0
  28. package/dist/SqliteMemoryStore.d.ts +52 -0
  29. package/dist/SqliteMemoryStore.d.ts.map +1 -0
  30. package/dist/SqliteMemoryStore.js +149 -0
  31. package/dist/SqliteMemoryStore.js.map +1 -0
  32. package/dist/SqliteReadModelStore.d.ts +38 -0
  33. package/dist/SqliteReadModelStore.d.ts.map +1 -0
  34. package/dist/SqliteReadModelStore.js +292 -0
  35. package/dist/SqliteReadModelStore.js.map +1 -0
  36. package/dist/SqliteSnapshotStore.d.ts +31 -0
  37. package/dist/SqliteSnapshotStore.d.ts.map +1 -0
  38. package/dist/SqliteSnapshotStore.js +101 -0
  39. package/dist/SqliteSnapshotStore.js.map +1 -0
  40. package/dist/SqliteTimerStore.d.ts +19 -0
  41. package/dist/SqliteTimerStore.d.ts.map +1 -0
  42. package/dist/SqliteTimerStore.js +106 -0
  43. package/dist/SqliteTimerStore.js.map +1 -0
  44. package/dist/index.d.ts +13 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +26 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/preset.d.ts +31 -0
  49. package/dist/preset.d.ts.map +1 -0
  50. package/dist/preset.js +51 -0
  51. package/dist/preset.js.map +1 -0
  52. package/dist/schema.d.ts +3 -0
  53. package/dist/schema.d.ts.map +1 -0
  54. package/dist/schema.js +103 -0
  55. package/dist/schema.js.map +1 -0
  56. package/dist/snapshotSchema.d.ts +3 -0
  57. package/dist/snapshotSchema.d.ts.map +1 -0
  58. package/dist/snapshotSchema.js +19 -0
  59. package/dist/snapshotSchema.js.map +1 -0
  60. package/package.json +62 -3
  61. package/src/SqliteArchiveStore.ts +189 -0
  62. package/src/SqliteBranchRegistry.ts +199 -0
  63. package/src/SqliteContentStore.ts +195 -0
  64. package/src/SqliteDurableStateStore.ts +101 -0
  65. package/src/SqliteEventStore.ts +414 -0
  66. package/src/SqliteLeaseStore.ts +165 -0
  67. package/src/SqliteMemoryStore.ts +160 -0
  68. package/src/SqliteReadModelStore.ts +378 -0
  69. package/src/SqliteSnapshotStore.ts +157 -0
  70. package/src/SqliteTimerStore.ts +124 -0
  71. package/src/index.ts +13 -0
  72. package/src/preset.ts +66 -0
  73. package/src/schema.ts +100 -0
  74. package/src/snapshotSchema.ts +17 -0
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SqliteMemoryStore = void 0;
7
+ const memory_1 = require("@loomwork/memory");
8
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
9
+ /**
10
+ * SQLite-backed {@link MemoryStore} — the zero-server durable agent memory. Point it
11
+ * at the same DB file as the SqliteEventStore so one backup captures events + memory.
12
+ * Own idempotent migration, same pattern as SqliteTimerStore/SqliteDurableStateStore.
13
+ *
14
+ * A key names an ordered list, so the table needs an insertion key of its own: entries
15
+ * routinely share a timestamp and `ORDER BY timestamp` would reshuffle a conversation.
16
+ * `AUTOINCREMENT` (not bare rowid) makes the monotonicity a schema-level guarantee
17
+ * rather than an argument about rowid reuse after a `clear`.
18
+ *
19
+ * `replace` is one transaction, which is the whole reason it is on the SPI: MemoryRuntime
20
+ * rewrites a list on every TTL/maxEntries eviction, and a crash between the delete and the
21
+ * re-insert would make routine eviction the thing that loses the scope.
22
+ *
23
+ * `remove` (ADR 0091) is one indexed `DELETE` matching `json_extract(entry, '$.id')` — the
24
+ * id is read from the stored JSON rather than duplicated into a column, so nothing can
25
+ * drift between what `read` returns and what a delete matches, and a database written
26
+ * before this shipped needs no migration.
27
+ *
28
+ * `search` rides an FTS5 index over `searchableText(entry)`, held in an EXTERNAL-CONTENT
29
+ * table so the text is indexed but not stored twice, and synchronised by triggers so a
30
+ * `clear`/`replace` cannot leave the index describing entries that no longer exist. FTS5's
31
+ * `rank` is a BM25 score where LOWER is better, hence `ORDER BY rank` ascending.
32
+ */
33
+ class SqliteMemoryStore {
34
+ db;
35
+ appendStmt;
36
+ readStmt;
37
+ clearStmt;
38
+ removeStmt;
39
+ searchStmt;
40
+ replaceTxn;
41
+ constructor(dbPath) {
42
+ this.db = new better_sqlite3_1.default(dbPath);
43
+ try {
44
+ this.db.pragma("journal_mode = WAL");
45
+ this.db.pragma("synchronous = NORMAL");
46
+ this.db.exec(`
47
+ CREATE TABLE IF NOT EXISTS memory_entries (
48
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ key TEXT NOT NULL,
50
+ entry TEXT NOT NULL,
51
+ text TEXT NOT NULL
52
+ );
53
+ CREATE INDEX IF NOT EXISTS idx_memory_entries_key
54
+ ON memory_entries (key, seq ASC);
55
+ -- Removal by entry id (ADR 0091). An EXPRESSION index over the stored JSON, not
56
+ -- a column: the entry is already persisted whole, so an id round-trips with no
57
+ -- schema change and no migration of rows written before this shipped -- and a
58
+ -- column would be a second copy able to disagree with the one a read returns.
59
+ CREATE INDEX IF NOT EXISTS idx_memory_entries_id
60
+ ON memory_entries (key, json_extract(entry, '$.id'))
61
+ WHERE json_extract(entry, '$.id') IS NOT NULL;
62
+
63
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
64
+ text, content='memory_entries', content_rowid='seq'
65
+ );
66
+ -- External content: the index holds no copy of the row, so it must be told about
67
+ -- every insert and delete. Entries are never updated, so there is no update trigger.
68
+ CREATE TRIGGER IF NOT EXISTS memory_entries_ai AFTER INSERT ON memory_entries BEGIN
69
+ INSERT INTO memory_fts(rowid, text) VALUES (new.seq, new.text);
70
+ END;
71
+ CREATE TRIGGER IF NOT EXISTS memory_entries_ad AFTER DELETE ON memory_entries BEGIN
72
+ INSERT INTO memory_fts(memory_fts, rowid, text) VALUES ('delete', old.seq, old.text);
73
+ END;
74
+ `);
75
+ this.appendStmt = this.db.prepare("INSERT INTO memory_entries (key, entry, text) VALUES (?, ?, ?)");
76
+ this.readStmt = this.db.prepare("SELECT entry FROM memory_entries WHERE key = ? ORDER BY seq ASC");
77
+ this.clearStmt = this.db.prepare("DELETE FROM memory_entries WHERE key = ?");
78
+ // `json_each` over ONE json-array parameter, not a generated `IN (?,?,?…)`: a
79
+ // document with 200 chunks would otherwise be a 200-placeholder statement that has
80
+ // never been prepared before, and better-sqlite3 caches by SQL text.
81
+ this.removeStmt = this.db.prepare(`DELETE FROM memory_entries
82
+ WHERE key = ?
83
+ AND json_extract(entry, '$.id') IN (SELECT value FROM json_each(?))`);
84
+ this.searchStmt = this.db.prepare(`SELECT e.entry FROM memory_fts
85
+ JOIN memory_entries e ON e.seq = memory_fts.rowid
86
+ WHERE memory_fts MATCH ? AND e.key = ?
87
+ ORDER BY memory_fts.rank
88
+ LIMIT ?`);
89
+ this.replaceTxn = this.db.transaction((key, entries) => {
90
+ this.clearStmt.run(key);
91
+ for (const entry of entries)
92
+ this.appendStmt.run(key, JSON.stringify(entry), (0, memory_1.searchableText)(entry));
93
+ });
94
+ }
95
+ catch (err) {
96
+ this.db.close(); // don't leak the file handle if setup fails on a bad/locked file
97
+ throw err;
98
+ }
99
+ }
100
+ async append(key, entry) {
101
+ this.appendStmt.run(key, JSON.stringify(entry), (0, memory_1.searchableText)(entry));
102
+ }
103
+ async read(key) {
104
+ const rows = this.readStmt.all(key);
105
+ return rows.map((row) => JSON.parse(row.entry));
106
+ }
107
+ async clear(key) {
108
+ this.clearStmt.run(key);
109
+ }
110
+ async replace(key, entries) {
111
+ this.replaceTxn(key, entries);
112
+ }
113
+ /**
114
+ * Delete by entry id. The FTS index needs no special handling: rows are deleted, so the
115
+ * existing AFTER DELETE trigger takes them out of `memory_fts` — which is the reason
116
+ * this is a row delete and not a rewrite of the `entry` JSON.
117
+ *
118
+ * An entry with no id has `json_extract(...) IS NULL`, and `NULL IN (...)` is NULL —
119
+ * never true — so anonymous entries cannot be removed by any list of ids, including one
120
+ * containing the strings "null" or "undefined".
121
+ */
122
+ async remove(key, ids) {
123
+ if (ids.length === 0)
124
+ return;
125
+ this.removeStmt.run(key, JSON.stringify(ids));
126
+ }
127
+ async search(key, query, opts = {}) {
128
+ const terms = (0, memory_1.queryTerms)(query);
129
+ if (terms.length === 0)
130
+ return [];
131
+ // `queryTerms`, not `tokenize`: OR-ing the same term 20,000 times cannot change which
132
+ // rows match and took 32.7 SECONDS here before it was deduplicated.
133
+ //
134
+ // Building the MATCH from TERMS, never from the raw question, is what keeps FTS5's
135
+ // expression syntax (`*`, `NEAR`, `AND`, a bare `-`) out of it: a user question passed
136
+ // through is parsed as operators, and either raises a syntax error or quietly means
137
+ // something else. `tokenize` already yields lowercase alphanumerics — which cannot be
138
+ // FTS5 keywords, those being uppercase — and the quoting below is the second belt, so
139
+ // that widening what counts as a term later cannot reopen this.
140
+ const match = terms.map((term) => `"${term}"`).join(" OR ");
141
+ const rows = this.searchStmt.all(match, key, (0, memory_1.clampSearchLimit)(opts.limit));
142
+ return rows.map((row) => JSON.parse(row.entry));
143
+ }
144
+ async close() {
145
+ this.db.close();
146
+ }
147
+ }
148
+ exports.SqliteMemoryStore = SqliteMemoryStore;
149
+ //# sourceMappingURL=SqliteMemoryStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SqliteMemoryStore.js","sourceRoot":"","sources":["../src/SqliteMemoryStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { FoldedApply, FoldedApplyResult, FoldedRecord, FoldedSelector, ReadModelPage, ReadModelQuery, ReadModelRow, ReadModelScalar, ReadModelStore } from "@loomwork/plugin-read-model";
2
+ /**
3
+ * SQLite-backed {@link ReadModelStore} (RM, issue #183). Point it at the same
4
+ * DB file as the SqliteEventStore so one backup captures log + read models.
5
+ * Own idempotent migration, same pattern as SqliteDurableStateStore. Field
6
+ * equality queries use `json_extract` over the JSON `fields` column; keyset
7
+ * pagination uses row-value comparison over (timestamp, sequence, row_id).
8
+ */
9
+ export declare class SqliteReadModelStore implements ReadModelStore {
10
+ private readonly db;
11
+ private readonly insertRowStmt;
12
+ private readonly upsertMarkStmt;
13
+ private readonly watermarkStmt;
14
+ private readonly watermarkedStmt;
15
+ private readonly clearRowsStmt;
16
+ private readonly clearMarksStmt;
17
+ private readonly foldHeaderStmt;
18
+ private readonly upsertFoldHeaderStmt;
19
+ private readonly dropFoldStatesStmt;
20
+ private readonly dropFoldHeaderStmt;
21
+ private readonly applyFoldedStmt;
22
+ constructor(dbPath: string);
23
+ apply(modelId: string, executionId: string, upToSequence: number, rows: readonly ReadModelRow[]): Promise<void>;
24
+ watermark(modelId: string, executionId: string): Promise<number | null>;
25
+ watermarkedExecutions(modelId: string): Promise<string[]>;
26
+ query(modelId: string, q?: ReadModelQuery): Promise<ReadModelPage>;
27
+ purge(modelId: string, where: Record<string, ReadModelScalar>): Promise<number>;
28
+ clear(modelId: string): Promise<void>;
29
+ beginFold(modelId: string, foldVersion: string): Promise<{
30
+ rebuilt: boolean;
31
+ }>;
32
+ applyFolded(a: FoldedApply): Promise<FoldedApplyResult>;
33
+ getFolded(modelId: string, executionIds: readonly string[]): Promise<FoldedRecord[]>;
34
+ getFoldedByKey(modelId: string, keys: readonly string[]): Promise<FoldedRecord[]>;
35
+ purgeFolded(modelId: string, selector: FoldedSelector): Promise<number>;
36
+ close(): Promise<void>;
37
+ }
38
+ //# sourceMappingURL=SqliteReadModelStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SqliteReadModelStore.d.ts","sourceRoot":"","sources":["../src/SqliteReadModelStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,cAAc,EACd,YAAY,EACZ,eAAe,EACf,cAAc,EACf,MAAM,6BAA6B,CAAC;AAyCrC;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IAGvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;IAC1D,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;gBAEzC,MAAM,EAAE,MAAM;IA6FpB,KAAK,CACT,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,SAAS,YAAY,EAAE,GAC5B,OAAO,CAAC,IAAI,CAAC;IAkBV,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAKvE,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKzD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuDtE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAyB/E,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAc9E,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAqBvD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAiBpF,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAgBjF,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAavE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SqliteReadModelStore = void 0;
7
+ const plugin_read_model_1 = require("@loomwork/plugin-read-model");
8
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
9
+ function toFolded(r) {
10
+ return {
11
+ executionId: r.execution_id,
12
+ key: r.app_key,
13
+ scope: r.scope,
14
+ sequence: r.sequence,
15
+ state: r.state === null ? null : JSON.parse(r.state),
16
+ erased: r.erased === 1,
17
+ };
18
+ }
19
+ /**
20
+ * SQLite-backed {@link ReadModelStore} (RM, issue #183). Point it at the same
21
+ * DB file as the SqliteEventStore so one backup captures log + read models.
22
+ * Own idempotent migration, same pattern as SqliteDurableStateStore. Field
23
+ * equality queries use `json_extract` over the JSON `fields` column; keyset
24
+ * pagination uses row-value comparison over (timestamp, sequence, row_id).
25
+ */
26
+ class SqliteReadModelStore {
27
+ db;
28
+ // Audit fix (DS&A rank 2): statements are prepared ONCE — apply-heavy
29
+ // catch-ups no longer re-parse identical SQL per call.
30
+ insertRowStmt;
31
+ upsertMarkStmt;
32
+ watermarkStmt;
33
+ watermarkedStmt;
34
+ clearRowsStmt;
35
+ clearMarksStmt;
36
+ foldHeaderStmt;
37
+ upsertFoldHeaderStmt;
38
+ dropFoldStatesStmt;
39
+ dropFoldHeaderStmt;
40
+ applyFoldedStmt;
41
+ constructor(dbPath) {
42
+ this.db = new better_sqlite3_1.default(dbPath);
43
+ try {
44
+ this.db.pragma("journal_mode = WAL");
45
+ this.db.pragma("synchronous = NORMAL");
46
+ this.db.exec(`
47
+ CREATE TABLE IF NOT EXISTS read_model_rows (
48
+ model_id TEXT NOT NULL,
49
+ row_id TEXT NOT NULL,
50
+ execution_id TEXT NOT NULL,
51
+ sequence INTEGER NOT NULL,
52
+ timestamp INTEGER NOT NULL,
53
+ kind TEXT NOT NULL,
54
+ fields TEXT NOT NULL,
55
+ value TEXT NOT NULL,
56
+ PRIMARY KEY (model_id, row_id)
57
+ );
58
+ CREATE INDEX IF NOT EXISTS idx_rm_rows_order
59
+ ON read_model_rows (model_id, kind, timestamp, sequence);
60
+ CREATE TABLE IF NOT EXISTS read_model_watermarks (
61
+ model_id TEXT NOT NULL,
62
+ execution_id TEXT NOT NULL,
63
+ sequence INTEGER NOT NULL,
64
+ PRIMARY KEY (model_id, execution_id)
65
+ );
66
+ CREATE TABLE IF NOT EXISTS read_model_folds (
67
+ model_id TEXT PRIMARY KEY,
68
+ version TEXT NOT NULL
69
+ );
70
+ CREATE TABLE IF NOT EXISTS read_model_state (
71
+ model_id TEXT NOT NULL,
72
+ execution_id TEXT NOT NULL,
73
+ app_key TEXT,
74
+ scope TEXT,
75
+ sequence INTEGER NOT NULL,
76
+ state TEXT,
77
+ erased INTEGER NOT NULL DEFAULT 0,
78
+ PRIMARY KEY (model_id, execution_id)
79
+ );
80
+ CREATE INDEX IF NOT EXISTS idx_rm_state_key
81
+ ON read_model_state (model_id, app_key) WHERE app_key IS NOT NULL;
82
+ CREATE INDEX IF NOT EXISTS idx_rm_state_scope
83
+ ON read_model_state (model_id, scope) WHERE scope IS NOT NULL;
84
+ `);
85
+ this.insertRowStmt = this.db.prepare(`INSERT OR IGNORE INTO read_model_rows
86
+ (model_id, row_id, execution_id, sequence, timestamp, kind, fields, value)
87
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
88
+ this.upsertMarkStmt = this.db.prepare(`INSERT INTO read_model_watermarks (model_id, execution_id, sequence)
89
+ VALUES (?, ?, ?)
90
+ ON CONFLICT (model_id, execution_id)
91
+ DO UPDATE SET sequence = MAX(sequence, excluded.sequence)`);
92
+ this.watermarkStmt = this.db.prepare("SELECT sequence FROM read_model_watermarks WHERE model_id = ? AND execution_id = ?");
93
+ this.watermarkedStmt = this.db.prepare("SELECT execution_id FROM read_model_watermarks WHERE model_id = ?");
94
+ this.clearRowsStmt = this.db.prepare("DELETE FROM read_model_rows WHERE model_id = ?");
95
+ this.clearMarksStmt = this.db.prepare("DELETE FROM read_model_watermarks WHERE model_id = ?");
96
+ this.foldHeaderStmt = this.db.prepare("SELECT version FROM read_model_folds WHERE model_id = ?");
97
+ this.upsertFoldHeaderStmt = this.db.prepare(`INSERT INTO read_model_folds (model_id, version) VALUES (?, ?)
98
+ ON CONFLICT (model_id) DO UPDATE SET version = excluded.version`);
99
+ this.dropFoldStatesStmt = this.db.prepare("DELETE FROM read_model_state WHERE model_id = ?");
100
+ this.dropFoldHeaderStmt = this.db.prepare("DELETE FROM read_model_folds WHERE model_id = ?");
101
+ // One statement: header check + monotonic CAS. RETURNING reports the (unchanged)
102
+ // erased flag so advancing a tombstone is "applied-erased" and keeps state NULL.
103
+ this.applyFoldedStmt = this.db.prepare(`INSERT INTO read_model_state
104
+ (model_id, execution_id, app_key, scope, sequence, state, erased)
105
+ SELECT ?, ?, ?, ?, ?, ?, 0
106
+ WHERE EXISTS (SELECT 1 FROM read_model_folds WHERE model_id = ? AND version = ?)
107
+ ON CONFLICT (model_id, execution_id) DO UPDATE
108
+ SET sequence = excluded.sequence,
109
+ state = CASE WHEN read_model_state.erased = 1 THEN NULL ELSE excluded.state END,
110
+ app_key = COALESCE(excluded.app_key, read_model_state.app_key),
111
+ scope = COALESCE(excluded.scope, read_model_state.scope)
112
+ WHERE read_model_state.sequence < excluded.sequence
113
+ RETURNING erased`);
114
+ }
115
+ catch (err) {
116
+ this.db.close(); // don't leak the file handle if setup fails on a bad/locked file
117
+ throw err;
118
+ }
119
+ }
120
+ async apply(modelId, executionId, upToSequence, rows) {
121
+ this.db.transaction(() => {
122
+ for (const row of rows) {
123
+ this.insertRowStmt.run(modelId, row.rowId, row.executionId, row.sequence, row.timestamp, row.kind, JSON.stringify(row.fields), JSON.stringify(row.value ?? null));
124
+ }
125
+ this.upsertMarkStmt.run(modelId, executionId, upToSequence);
126
+ })();
127
+ }
128
+ async watermark(modelId, executionId) {
129
+ const row = this.watermarkStmt.get(modelId, executionId);
130
+ return row?.sequence ?? null;
131
+ }
132
+ async watermarkedExecutions(modelId) {
133
+ const rows = this.watermarkedStmt.all(modelId);
134
+ return rows.map((r) => r.execution_id);
135
+ }
136
+ async query(modelId, q = {}) {
137
+ const desc = q.order === "desc";
138
+ const limit = (0, plugin_read_model_1.clampQueryLimit)(q.limit);
139
+ const clauses = ["model_id = ?"];
140
+ const params = [modelId];
141
+ if (q.kind !== undefined) {
142
+ clauses.push("kind = ?");
143
+ params.push(q.kind);
144
+ }
145
+ for (const [field, value] of Object.entries(q.where ?? {})) {
146
+ const path = `$."${field.split('"').join('""')}"`;
147
+ if (value === null) {
148
+ clauses.push("json_extract(fields, ?) IS NULL");
149
+ params.push(path);
150
+ }
151
+ else {
152
+ clauses.push("json_extract(fields, ?) = ?");
153
+ params.push(path, typeof value === "boolean" ? (value ? 1 : 0) : value);
154
+ }
155
+ }
156
+ if (q.cursor !== undefined) {
157
+ const [ts, seq, rowId] = (0, plugin_read_model_1.parseQueryCursor)(q.cursor);
158
+ clauses.push(`(timestamp, sequence, row_id) ${desc ? "<" : ">"} (?, ?, ?)`);
159
+ params.push(ts, seq, rowId);
160
+ }
161
+ const dir = desc ? "DESC" : "ASC";
162
+ const records = this.db
163
+ .prepare(`SELECT row_id, execution_id, sequence, timestamp, kind, fields, value
164
+ FROM read_model_rows WHERE ${clauses.join(" AND ")}
165
+ ORDER BY timestamp ${dir}, sequence ${dir}, row_id ${dir}
166
+ LIMIT ?`)
167
+ .all(...params, limit + 1);
168
+ const page = records.slice(0, limit).map((r) => ({
169
+ rowId: r.row_id,
170
+ executionId: r.execution_id,
171
+ sequence: r.sequence,
172
+ timestamp: r.timestamp,
173
+ kind: r.kind,
174
+ fields: JSON.parse(r.fields),
175
+ value: JSON.parse(r.value),
176
+ }));
177
+ const last = page[page.length - 1];
178
+ return {
179
+ rows: page,
180
+ ...(records.length > limit &&
181
+ last !== undefined && {
182
+ nextCursor: JSON.stringify([last.timestamp, last.sequence, last.rowId]),
183
+ }),
184
+ };
185
+ }
186
+ async purge(modelId, where) {
187
+ (0, plugin_read_model_1.assertPurgeFilter)(where);
188
+ // Same `json_extract` equality the query path uses, so a purge deletes exactly the
189
+ // rows the equivalent query would have returned. Not prepared once: the clause shape
190
+ // varies with the filter's field set, and an erasure is a lifecycle call, not a hot path.
191
+ const clauses = ["model_id = ?"];
192
+ const params = [modelId];
193
+ for (const [field, value] of Object.entries(where)) {
194
+ const path = `$."${field.split('"').join('""')}"`;
195
+ if (value === null) {
196
+ clauses.push("json_extract(fields, ?) IS NULL");
197
+ params.push(path);
198
+ }
199
+ else {
200
+ clauses.push("json_extract(fields, ?) = ?");
201
+ params.push(path, typeof value === "boolean" ? (value ? 1 : 0) : value);
202
+ }
203
+ }
204
+ // Watermarks are deliberately left alone (SPI contract) — deleting one would make the
205
+ // next catch-up re-derive the rows just erased.
206
+ const info = this.db
207
+ .prepare(`DELETE FROM read_model_rows WHERE ${clauses.join(" AND ")}`)
208
+ .run(...params);
209
+ return info.changes;
210
+ }
211
+ async clear(modelId) {
212
+ this.db.transaction(() => {
213
+ this.clearRowsStmt.run(modelId);
214
+ this.clearMarksStmt.run(modelId);
215
+ // Folded state (ADR 0132): this model only. Tables are created with the others in
216
+ // the constructor, so clear never has to create anything.
217
+ this.dropFoldStatesStmt.run(modelId);
218
+ this.dropFoldHeaderStmt.run(modelId);
219
+ })();
220
+ }
221
+ // ── Folded state (ADR 0132 D3, D5, D6, D12) ──────────────────────────────
222
+ async beginFold(modelId, foldVersion) {
223
+ // IMMEDIATE: read-then-write must take the write lock up front, or a second process
224
+ // in WAL mode fails with SQLITE_BUSY_SNAPSHOT when upgrading a stale read snapshot.
225
+ return this.db
226
+ .transaction(() => {
227
+ const prior = this.foldHeaderStmt.get(modelId);
228
+ this.upsertFoldHeaderStmt.run(modelId, foldVersion);
229
+ if (prior === undefined || prior.version === foldVersion)
230
+ return { rebuilt: false };
231
+ this.dropFoldStatesStmt.run(modelId);
232
+ return { rebuilt: true };
233
+ })
234
+ .immediate();
235
+ }
236
+ async applyFolded(a) {
237
+ // IMMEDIATE for the same reason: the miss path reads the header after the write attempt.
238
+ return this.db
239
+ .transaction(() => {
240
+ const hit = this.applyFoldedStmt.get(a.modelId, a.executionId, a.key ?? null, a.scope ?? null, a.upToSequence, a.state === null || a.state === undefined ? null : JSON.stringify(a.state), a.modelId, a.foldVersion);
241
+ if (hit !== undefined)
242
+ return hit.erased === 1 ? "applied-erased" : "applied";
243
+ const hdr = this.foldHeaderStmt.get(a.modelId);
244
+ return hdr?.version === a.foldVersion ? "stale" : "version-mismatch";
245
+ })
246
+ .immediate();
247
+ }
248
+ async getFolded(modelId, executionIds) {
249
+ (0, plugin_read_model_1.assertManyKeys)(executionIds, "getFolded");
250
+ const ids = [...new Set(executionIds)];
251
+ if (ids.length === 0)
252
+ return [];
253
+ const rows = this.db
254
+ .prepare(`SELECT execution_id, app_key, scope, sequence, state, erased FROM read_model_state
255
+ WHERE model_id = ? AND execution_id IN (${ids.map(() => "?").join(",")})`)
256
+ .all(modelId, ...ids);
257
+ const byId = new Map(rows.map((r) => [r.execution_id, toFolded(r)]));
258
+ return ids.flatMap((id) => {
259
+ const r = byId.get(id);
260
+ return r === undefined ? [] : [r];
261
+ });
262
+ }
263
+ async getFoldedByKey(modelId, keys) {
264
+ (0, plugin_read_model_1.assertManyKeys)(keys, "getFoldedByKey");
265
+ const uniq = [...new Set(keys)];
266
+ if (uniq.length === 0)
267
+ return [];
268
+ // BINARY collation = memcmp over UTF-8 = code-point order (ADR 0132), explicit so a
269
+ // future column collation can never change it.
270
+ const rows = this.db
271
+ .prepare(`SELECT execution_id, app_key, scope, sequence, state, erased FROM read_model_state
272
+ WHERE model_id = ? AND app_key IN (${uniq.map(() => "?").join(",")})
273
+ ORDER BY execution_id COLLATE BINARY`)
274
+ .all(modelId, ...uniq);
275
+ return rows.map(toFolded);
276
+ }
277
+ async purgeFolded(modelId, selector) {
278
+ (0, plugin_read_model_1.assertFoldedSelector)(selector);
279
+ const [col, val] = (0, plugin_read_model_1.foldedSelectorColumn)(selector);
280
+ // Tombstone, never delete: the row IS the watermark (ADR 0132 D12). `col` is one of
281
+ // three literals from foldedSelectorColumn, never caller input.
282
+ return this.db
283
+ .prepare(`UPDATE read_model_state SET state = NULL, erased = 1
284
+ WHERE model_id = ? AND ${col} = ? AND erased = 0`)
285
+ .run(modelId, val).changes;
286
+ }
287
+ async close() {
288
+ this.db.close();
289
+ }
290
+ }
291
+ exports.SqliteReadModelStore = SqliteReadModelStore;
292
+ //# sourceMappingURL=SqliteReadModelStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SqliteReadModelStore.js","sourceRoot":"","sources":["../src/SqliteReadModelStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type Snapshot, type SnapshotCodecName, type SnapshotStore } from "@loomwork/snapshot-runtime";
2
+ export interface SqliteSnapshotStoreOptions {
3
+ /**
4
+ * ADR 0141 - how NEW snapshots are written. `"none"` (default): JSON text, byte-identical to before.
5
+ * `"zstd"`: a BLOB frame (format byte + zstd-compressed JSON) in the same `state` column. Reads accept
6
+ * BOTH whatever this is set to, and nothing is rewritten. Throws at construction on a Node without zstd
7
+ * (>= 22.15). No schema change: SQLite's column affinity keeps a BLOB a BLOB.
8
+ */
9
+ codec?: SnapshotCodecName;
10
+ }
11
+ export declare class SqliteSnapshotStore implements SnapshotStore {
12
+ private readonly db;
13
+ /** ADR 0141 - the codec new snapshots are written with (structural; `ProfilingSnapshotStore` reads it). */
14
+ readonly snapshotCodec: SnapshotCodecName;
15
+ constructor(dbPath: string, options?: SqliteSnapshotStoreOptions);
16
+ save(snapshot: Snapshot): Promise<void>;
17
+ loadLatest(executionId: string): Promise<Snapshot | null>;
18
+ loadAt(executionId: string, sequence: number): Promise<Snapshot | null>;
19
+ loadLatestBefore(executionId: string, sequence: number): Promise<Snapshot | null>;
20
+ deleteExecution(executionId: string): Promise<void>;
21
+ listSequences(executionId: string, range?: {
22
+ readonly after?: number;
23
+ readonly before?: number;
24
+ }): Promise<number[]>;
25
+ pruneSnapshots(executionId: string, range: {
26
+ readonly after: number;
27
+ readonly before: number;
28
+ }, keep?: readonly number[]): Promise<number>;
29
+ close(): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=SqliteSnapshotStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SqliteSnapshotStore.d.ts","sourceRoot":"","sources":["../src/SqliteSnapshotStore.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAKnB,MAAM,4BAA4B,CAAC;AAepC,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAeD,qBAAa,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,2GAA2G;IAC3G,QAAQ,CAAC,aAAa,EAAE,iBAAiB,CAAC;gBAE9B,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,0BAA+B;IAc9D,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IASzD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IASvE,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IASjF,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,KAAK,GAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAChE,OAAO,CAAC,MAAM,EAAE,CAAC;IAoBd,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC1D,IAAI,GAAE,SAAS,MAAM,EAAO,GAC3B,OAAO,CAAC,MAAM,CAAC;IAWZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SqliteSnapshotStore = void 0;
7
+ const snapshot_runtime_1 = require("@loomwork/snapshot-runtime");
8
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
9
+ const snapshotSchema_1 = require("./snapshotSchema");
10
+ function rowToSnapshot(row) {
11
+ return {
12
+ id: row.id,
13
+ executionId: row.execution_id,
14
+ workflowId: row.workflow_id,
15
+ sequence: row.sequence,
16
+ state: (typeof row.state === "string"
17
+ ? JSON.parse(row.state)
18
+ : (0, snapshot_runtime_1.decodeState)(row.state)),
19
+ createdAt: row.created_at,
20
+ };
21
+ }
22
+ class SqliteSnapshotStore {
23
+ db;
24
+ /** ADR 0141 - the codec new snapshots are written with (structural; `ProfilingSnapshotStore` reads it). */
25
+ snapshotCodec;
26
+ constructor(dbPath, options = {}) {
27
+ this.snapshotCodec = (0, snapshot_runtime_1.parseSnapshotCodec)(options.codec);
28
+ (0, snapshot_runtime_1.assertSnapshotCodecAvailable)(this.snapshotCodec);
29
+ this.db = new better_sqlite3_1.default(dbPath);
30
+ try {
31
+ this.db.pragma("journal_mode = WAL");
32
+ this.db.pragma("synchronous = NORMAL");
33
+ (0, snapshotSchema_1.migrate)(this.db);
34
+ }
35
+ catch (err) {
36
+ this.db.close(); // don't leak the file handle if setup fails on a bad/locked file
37
+ throw err;
38
+ }
39
+ }
40
+ async save(snapshot) {
41
+ this.db
42
+ .prepare(`
43
+ INSERT OR REPLACE INTO snapshots (id, execution_id, workflow_id, sequence, state, created_at)
44
+ VALUES (?, ?, ?, ?, ?, ?)
45
+ `)
46
+ .run(snapshot.id, snapshot.executionId, snapshot.workflowId, snapshot.sequence, this.snapshotCodec === "none"
47
+ ? JSON.stringify(snapshot.state)
48
+ : (0, snapshot_runtime_1.encodeState)(snapshot.state, this.snapshotCodec), snapshot.createdAt);
49
+ }
50
+ async loadLatest(executionId) {
51
+ const row = this.db
52
+ .prepare("SELECT * FROM snapshots WHERE execution_id = ? ORDER BY sequence DESC LIMIT 1")
53
+ .get(executionId);
54
+ return row ? rowToSnapshot(row) : null;
55
+ }
56
+ async loadAt(executionId, sequence) {
57
+ const row = this.db
58
+ .prepare("SELECT * FROM snapshots WHERE execution_id = ? AND sequence = ?")
59
+ .get(executionId, sequence);
60
+ return row ? rowToSnapshot(row) : null;
61
+ }
62
+ async loadLatestBefore(executionId, sequence) {
63
+ const row = this.db
64
+ .prepare("SELECT * FROM snapshots WHERE execution_id = ? AND sequence <= ? ORDER BY sequence DESC LIMIT 1")
65
+ .get(executionId, sequence);
66
+ return row ? rowToSnapshot(row) : null;
67
+ }
68
+ async deleteExecution(executionId) {
69
+ this.db.prepare("DELETE FROM snapshots WHERE execution_id = ?").run(executionId);
70
+ }
71
+ async listSequences(executionId, range = {}) {
72
+ // A bound the caller omitted is simply absent from the WHERE (a sentinel would drop a row at it).
73
+ const where = ["execution_id = ?"];
74
+ const args = [executionId];
75
+ if (range.after !== undefined) {
76
+ where.push("sequence > ?");
77
+ args.push(range.after);
78
+ }
79
+ if (range.before !== undefined) {
80
+ where.push("sequence < ?");
81
+ args.push(range.before);
82
+ }
83
+ const rows = this.db
84
+ .prepare(`SELECT sequence FROM snapshots WHERE ${where.join(" AND ")} ORDER BY sequence`)
85
+ .all(...args);
86
+ return rows.map((r) => r.sequence);
87
+ }
88
+ async pruneSnapshots(executionId, range, keep = []) {
89
+ // One statement. The keep-set is a bound JSON array, not an interpolated list.
90
+ const info = this.db
91
+ .prepare(`DELETE FROM snapshots WHERE execution_id = ? AND sequence > ? AND sequence < ?
92
+ AND sequence NOT IN (SELECT value FROM json_each(?))`)
93
+ .run(executionId, range.after, range.before, JSON.stringify(keep));
94
+ return info.changes;
95
+ }
96
+ async close() {
97
+ this.db.close();
98
+ }
99
+ }
100
+ exports.SqliteSnapshotStore = SqliteSnapshotStore;
101
+ //# sourceMappingURL=SqliteSnapshotStore.js.map