@delali/sirannon-db 0.1.7 → 0.2.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 (72) hide show
  1. package/README.md +117 -969
  2. package/dist/backup-scheduler/index.d.ts +1 -1
  3. package/dist/backup-scheduler/index.mjs +2 -2
  4. package/dist/baseline-Br77Fnhb.d.ts +6 -0
  5. package/dist/{change-tracker-CbmaMO-N.d.ts → change-tracker-DKRVUC3l.d.ts} +5 -8
  6. package/dist/{chunk-DJLX6CAE.mjs → chunk-4ISB7XMA.mjs} +2 -1
  7. package/dist/chunk-5NOIGN5Y.mjs +18 -0
  8. package/dist/chunk-67M7KAH6.mjs +90 -0
  9. package/dist/{chunk-GEZUUIKV.mjs → chunk-D7LAYTKN.mjs} +5 -2
  10. package/dist/{chunk-4IGMIJQK.mjs → chunk-EBJXPQQO.mjs} +59 -20
  11. package/dist/{chunk-BNUTBHHH.mjs → chunk-FHWTZFI4.mjs} +1 -1
  12. package/dist/chunk-GVCNMPOS.mjs +130 -0
  13. package/dist/chunk-H237TXZW.mjs +1407 -0
  14. package/dist/{chunk-HHRMRFFR.mjs → chunk-H6PIVVDN.mjs} +353 -142
  15. package/dist/chunk-HR5CWTLC.mjs +661 -0
  16. package/dist/chunk-JU64Y7HM.mjs +299 -0
  17. package/dist/{chunk-CW6S3WL5.mjs → chunk-JZGINXTN.mjs} +7 -5
  18. package/dist/{chunk-CJLYFDP5.mjs → chunk-LFZ37BSX.mjs} +1 -1
  19. package/dist/chunk-LNY2VVHE.mjs +18 -0
  20. package/dist/chunk-NVQS53NT.mjs +665 -0
  21. package/dist/{chunk-H5AB6NIR.mjs → chunk-O7SLN3GI.mjs} +2 -15
  22. package/dist/chunk-OQVZBEBY.mjs +283 -0
  23. package/dist/{chunk-VLTICJOD.mjs → chunk-PIKHN33N.mjs} +1 -1
  24. package/dist/chunk-TJF5GZSV.mjs +187 -0
  25. package/dist/chunk-TUD5CJ76.mjs +126 -0
  26. package/dist/{chunk-YPYVQJ4C.mjs → chunk-UC3SCMIN.mjs} +9 -1
  27. package/dist/client/index.d.ts +216 -321
  28. package/dist/client/index.mjs +1192 -1342
  29. package/dist/client/topology.d.ts +61 -0
  30. package/dist/client/topology.mjs +682 -0
  31. package/dist/client-base-CLWmH5Ln.d.ts +234 -0
  32. package/dist/codegen/cli.d.ts +2 -0
  33. package/dist/codegen/cli.mjs +11 -0
  34. package/dist/codegen/index.d.ts +30 -0
  35. package/dist/codegen/index.mjs +5 -0
  36. package/dist/core/index.d.ts +37 -16
  37. package/dist/core/index.mjs +2322 -298
  38. package/dist/core/writer-worker.mjs +28 -6
  39. package/dist/{database-DuGp0Rtr.d.ts → database-BY0L5Q2n.d.ts} +59 -14
  40. package/dist/driver/better-sqlite3.d.ts +1 -1
  41. package/dist/driver/better-sqlite3.mjs +6 -6
  42. package/dist/driver/node.d.ts +1 -1
  43. package/dist/driver/node.mjs +6 -6
  44. package/dist/driver/wa-sqlite.d.ts +1 -136
  45. package/dist/{errors-5Nf5ZAEC.d.ts → errors-Bw5MdNCu.d.ts} +5 -1
  46. package/dist/file-migrations/index.d.ts +7 -5
  47. package/dist/file-migrations/index.mjs +42 -37
  48. package/dist/operation-registry-9DcvxcE5.d.ts +45 -0
  49. package/dist/primary-wins-DPAm2AKG.d.ts +61 -0
  50. package/dist/protocol-6KrSq2Hy.d.ts +66 -0
  51. package/dist/react/index.d.ts +20 -0
  52. package/dist/react/index.mjs +137 -0
  53. package/dist/replication/coordinator/etcd.d.ts +5 -3
  54. package/dist/replication/coordinator/etcd.mjs +554 -462
  55. package/dist/replication/index.d.ts +51 -105
  56. package/dist/replication/index.mjs +1415 -2061
  57. package/dist/server/index.d.ts +111 -87
  58. package/dist/server/index.mjs +1917 -489
  59. package/dist/server-options-1JHu8pid.d.ts +191 -0
  60. package/dist/{sirannon-4SspRvP5.d.ts → sirannon-DaQSyhbJ.d.ts} +7 -2
  61. package/dist/transport/grpc.d.ts +7 -4
  62. package/dist/transport/grpc.mjs +191 -97
  63. package/dist/transport/memory.d.ts +5 -134
  64. package/dist/transport/memory.mjs +3 -1
  65. package/dist/{types-BsjobKbl.d.ts → types-B7gmEsZW.d.ts} +8 -61
  66. package/dist/{types-BEu1I_9_.d.ts → types-BsVabqSI.d.ts} +1 -1
  67. package/dist/types-CL6piSnD.d.ts +43 -0
  68. package/dist/types-C_D8IhpO.d.ts +60 -0
  69. package/dist/{types-D_hQW1hr.d.ts → types-zhnRXrsb.d.ts} +55 -165
  70. package/package.json +44 -6
  71. package/dist/chunk-TKGHYWQ6.mjs +0 -35
  72. package/dist/types-D4p4UyDK.d.ts +0 -26
@@ -0,0 +1,665 @@
1
+ import { META_TABLE, CHANGES_TABLE, APPLIED_CHANGES_TABLE, MIGRATIONS_TABLE, COLUMN_VERSIONS_TABLE, DEVICE_CURSORS_TABLE, PEER_STATE_TABLE, SYNC_STATE_TABLE, STAGED_CHANGES_TABLE } from './chunk-D7LAYTKN.mjs';
2
+ import { sha256Hex } from './chunk-GVCNMPOS.mjs';
3
+ import { SirannonError } from './chunk-UC3SCMIN.mjs';
4
+
5
+ // src/core/system-catalog/applied-changes-table.ts
6
+ function toSeq(value) {
7
+ if (value === null || value === void 0) return 0n;
8
+ return typeof value === "bigint" ? value : BigInt(String(value));
9
+ }
10
+ async function ensureAppliedChangesTable(conn) {
11
+ await conn.exec(`
12
+ CREATE TABLE IF NOT EXISTS "${APPLIED_CHANGES_TABLE}" (
13
+ source_node_id TEXT NOT NULL,
14
+ source_seq INTEGER NOT NULL,
15
+ applied_at REAL NOT NULL,
16
+ PRIMARY KEY (source_node_id, source_seq)
17
+ )`);
18
+ }
19
+ function prepareInsertAppliedChange(conn) {
20
+ return conn.prepare(
21
+ `INSERT OR IGNORE INTO ${APPLIED_CHANGES_TABLE} (source_node_id, source_seq, applied_at) VALUES (?, ?, ?)`
22
+ );
23
+ }
24
+ async function selectMaxAppliedSourceSeq(conn, sourceNodeId) {
25
+ const stmt = await conn.prepare(
26
+ `SELECT MAX(source_seq) AS max_seq FROM ${APPLIED_CHANGES_TABLE} WHERE source_node_id = ?`
27
+ );
28
+ const row = await stmt.get(sourceNodeId);
29
+ return toSeq(row?.max_seq);
30
+ }
31
+ async function selectMaxAppliedSourceSeqByNode(conn) {
32
+ const stmt = await conn.prepare(
33
+ `SELECT source_node_id, MAX(source_seq) AS max_seq FROM ${APPLIED_CHANGES_TABLE} GROUP BY source_node_id`
34
+ );
35
+ const rows = await stmt.all();
36
+ const byNode = /* @__PURE__ */ new Map();
37
+ for (const row of rows) {
38
+ if (row.max_seq === null || row.max_seq === void 0) continue;
39
+ byNode.set(row.source_node_id, toSeq(row.max_seq));
40
+ }
41
+ return byNode;
42
+ }
43
+ async function selectAppliedSourceSeqsInRange(conn, sourceNodeId, fromSeq, toSeqBound) {
44
+ const stmt = await conn.prepare(
45
+ `SELECT source_seq FROM ${APPLIED_CHANGES_TABLE} WHERE source_node_id = ? AND source_seq >= ? AND source_seq <= ?`
46
+ );
47
+ const rows = await stmt.all(sourceNodeId, fromSeq.toString(), toSeqBound.toString());
48
+ const seqs = /* @__PURE__ */ new Set();
49
+ for (const row of rows) {
50
+ seqs.add(String(row.source_seq));
51
+ }
52
+ return seqs;
53
+ }
54
+
55
+ // src/core/system-catalog/sqlite-catalog.ts
56
+ async function selectTableExists(conn, table) {
57
+ const stmt = await conn.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?");
58
+ return Boolean(await stmt.get(table));
59
+ }
60
+ async function tableInfoRows(conn, table) {
61
+ const stmt = await conn.prepare(`PRAGMA table_info("${table}")`);
62
+ return await stmt.all();
63
+ }
64
+ async function tableColumnNames(conn, table) {
65
+ return (await tableInfoRows(conn, table)).map((col) => col.name);
66
+ }
67
+ async function tablePkColumns(conn, table) {
68
+ return (await tableInfoRows(conn, table)).filter((col) => col.pk > 0).sort((a, b) => a.pk - b.pk).map((col) => col.name);
69
+ }
70
+ async function selectTableSql(conn, table) {
71
+ const stmt = await conn.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?");
72
+ const row = await stmt.get(table);
73
+ return row?.sql ?? null;
74
+ }
75
+ async function selectCountTableRows(conn, table) {
76
+ const stmt = await conn.prepare(`SELECT COUNT(*) AS cnt FROM "${table}"`);
77
+ const row = await stmt.get();
78
+ return Number(row?.cnt ?? 0);
79
+ }
80
+ async function referencedTables(conn, table) {
81
+ const stmt = await conn.prepare(`PRAGMA foreign_key_list("${table}")`);
82
+ const rows = await stmt.all();
83
+ return rows.map((row) => row.table);
84
+ }
85
+ async function setForeignKeysEnabled(conn, enabled) {
86
+ await conn.exec(enabled ? "PRAGMA foreign_keys = ON" : "PRAGMA foreign_keys = OFF");
87
+ }
88
+ async function selectUserSchemaObjects(conn, excludePrefix) {
89
+ const stmt = await conn.prepare(
90
+ `SELECT type, name, tbl_name, sql FROM sqlite_master
91
+ WHERE type IN ('table', 'index') AND name NOT LIKE ? AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL`
92
+ );
93
+ return await stmt.all(`${excludePrefix}%`);
94
+ }
95
+ async function selectUserTableNames(conn, excludePrefix) {
96
+ const stmt = await conn.prepare(
97
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE ? AND name NOT LIKE 'sqlite_%'"
98
+ );
99
+ const rows = await stmt.all(`${excludePrefix}%`);
100
+ return rows.map((row) => row.name);
101
+ }
102
+
103
+ // src/core/system-catalog/columns.ts
104
+ var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
105
+ var SAFE_COLUMN_TYPE = /^[A-Za-z]+$/;
106
+ var SAFE_DEFAULT_TEXT = /^[A-Za-z0-9_]*$/;
107
+ function assertSafeIdentifier(value) {
108
+ if (!SAFE_IDENTIFIER.test(value)) {
109
+ throw new SirannonError(`Unsafe SQL identifier for internal schema: ${value}`, "INTERNAL_SCHEMA_ERROR");
110
+ }
111
+ }
112
+ async function tableColumns(conn, table) {
113
+ assertSafeIdentifier(table);
114
+ return new Set(await tableColumnNames(conn, table));
115
+ }
116
+ async function ensureColumn(conn, table, column, columnType, notNullDefaultText) {
117
+ assertSafeIdentifier(table);
118
+ assertSafeIdentifier(column);
119
+ if (!SAFE_COLUMN_TYPE.test(columnType)) {
120
+ throw new SirannonError(`Unsafe column type for internal schema: ${columnType}`, "INTERNAL_SCHEMA_ERROR");
121
+ }
122
+ if (notNullDefaultText !== void 0 && !SAFE_DEFAULT_TEXT.test(notNullDefaultText)) {
123
+ throw new SirannonError(`Unsafe column default for internal schema: ${notNullDefaultText}`, "INTERNAL_SCHEMA_ERROR");
124
+ }
125
+ if ((await tableColumns(conn, table)).has(column)) {
126
+ return;
127
+ }
128
+ const constraint = notNullDefaultText === void 0 ? "" : ` NOT NULL DEFAULT '${notNullDefaultText}'`;
129
+ try {
130
+ await conn.exec(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${columnType}${constraint}`);
131
+ } catch (err) {
132
+ if ((await tableColumns(conn, table)).has(column)) {
133
+ return;
134
+ }
135
+ throw err;
136
+ }
137
+ }
138
+
139
+ // src/core/system-catalog/meta-table.ts
140
+ async function ensureMetaTable(conn) {
141
+ await conn.exec(`CREATE TABLE IF NOT EXISTS "${META_TABLE}" (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
142
+ }
143
+ async function selectMetaValue(conn, key) {
144
+ const stmt = await conn.prepare(`SELECT value FROM "${META_TABLE}" WHERE key = ?`);
145
+ const row = await stmt.get(key);
146
+ const value = row?.value;
147
+ return typeof value === "string" ? value : null;
148
+ }
149
+ async function insertMetaValueIfAbsent(conn, key, value) {
150
+ const stmt = await conn.prepare(`INSERT OR IGNORE INTO "${META_TABLE}" (key, value) VALUES (?, ?)`);
151
+ await stmt.run(key, value);
152
+ }
153
+ async function deleteMetaValue(conn, key) {
154
+ const stmt = await conn.prepare(`DELETE FROM "${META_TABLE}" WHERE key = ?`);
155
+ await stmt.run(key);
156
+ }
157
+ var UPSERT_META_VALUE_SQL = `INSERT INTO "${META_TABLE}" (key, value) VALUES (?, ?)
158
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`;
159
+ async function upsertMetaValue(conn, key, value) {
160
+ const stmt = await conn.prepare(UPSERT_META_VALUE_SQL);
161
+ await stmt.run(key, value);
162
+ }
163
+
164
+ // src/core/system-catalog/changes-table.ts
165
+ var CHANGE_LOG_COLUMNS = "seq, table_name, operation, row_id, changed_at, old_data, new_data, node_id, tx_id, hlc";
166
+ var DDL_CHANGE_ROW_TABLE = "__ddl__";
167
+ function selectChangesAfterSeqSql(tableName) {
168
+ return `SELECT ${CHANGE_LOG_COLUMNS} FROM "${tableName}" WHERE seq > ? ORDER BY seq ASC LIMIT ?`;
169
+ }
170
+ function selectTableChangesInRangeSql(tableName) {
171
+ return `SELECT ${CHANGE_LOG_COLUMNS} FROM "${tableName}"
172
+ WHERE table_name = ? AND seq > ? AND seq <= ?
173
+ ORDER BY seq ASC LIMIT ?`;
174
+ }
175
+ function selectTablesChangesInRangeSql(tableName, tableCount) {
176
+ const placeholders = Array.from({ length: tableCount }, () => "?").join(", ");
177
+ return `SELECT ${CHANGE_LOG_COLUMNS} FROM "${tableName}"
178
+ WHERE table_name IN (${placeholders}) AND seq > ? AND seq <= ?
179
+ ORDER BY seq ASC LIMIT ?`;
180
+ }
181
+ function selectNodeChangesAfterSeqSql(tableName) {
182
+ return `SELECT ${CHANGE_LOG_COLUMNS} FROM "${tableName}"
183
+ WHERE seq > ? AND node_id = ?
184
+ ORDER BY seq ASC`;
185
+ }
186
+ function selectOutboundChangesSql(tableName, excludeDdl) {
187
+ const ddlFilter = excludeDdl ? "AND operation != 'DDL'" : "";
188
+ return `SELECT ${CHANGE_LOG_COLUMNS} FROM "${tableName}"
189
+ WHERE seq > ? AND node_id = ? ${ddlFilter}
190
+ ORDER BY seq ASC LIMIT ?`;
191
+ }
192
+ async function selectCountOutboundChanges(conn, tableName, afterSeq, nodeId) {
193
+ const stmt = await conn.prepare(
194
+ `SELECT COUNT(*) AS pending FROM "${tableName}" WHERE seq > ? AND node_id = ? AND operation != 'DDL'`
195
+ );
196
+ const row = await stmt.get(afterSeq.toString(), nodeId);
197
+ return Number(row.pending);
198
+ }
199
+ async function insertDdlChange(conn, tableName, entry) {
200
+ const stmt = await conn.prepare(
201
+ `INSERT INTO "${tableName}" (table_name, operation, row_id, new_data, node_id, tx_id, hlc)
202
+ VALUES ('${DDL_CHANGE_ROW_TABLE}', 'DDL', '', ?, ?, ?, ?)`
203
+ );
204
+ await stmt.run(JSON.stringify({ ddlStatement: entry.ddlStatement }), entry.nodeId, entry.txId, entry.hlc);
205
+ }
206
+ function updateChangeStampsAfterSeqSql(tableName) {
207
+ return `UPDATE "${tableName}" SET node_id = ?, tx_id = ?, hlc = ? WHERE seq > ? AND node_id = ''`;
208
+ }
209
+ function updateUnstampedChangeStampsSql(tableName) {
210
+ return `UPDATE "${tableName}" SET node_id = ?, tx_id = ?, hlc = ? WHERE node_id = ''`;
211
+ }
212
+ function selectMinChangeSeqSql(tableName) {
213
+ return `SELECT MIN(seq) AS seq FROM "${tableName}"`;
214
+ }
215
+ function selectMinForeignChangeSeqSql(tableName) {
216
+ return `SELECT MIN(seq) AS seq FROM "${tableName}" WHERE seq > ? AND node_id != ?`;
217
+ }
218
+ function deleteChangesBeforeSql(tableName) {
219
+ return `DELETE FROM "${tableName}" WHERE changed_at < ?`;
220
+ }
221
+ function deleteChangesBeforeUpToSeqSql(tableName) {
222
+ return `DELETE FROM "${tableName}" WHERE changed_at < ? AND seq <= ?`;
223
+ }
224
+ async function selectMaxRowChangeHlc(conn, tableName, rowTable, rowId) {
225
+ const stmt = await conn.prepare(
226
+ `SELECT MAX(hlc) AS max_hlc FROM "${tableName}" WHERE table_name = ? AND row_id = ? AND hlc != ''`
227
+ );
228
+ const row = await stmt.get(rowTable, rowId);
229
+ return row?.max_hlc ?? null;
230
+ }
231
+ async function selectMaxChangeSeq(conn, tableName = CHANGES_TABLE) {
232
+ const stmt = await conn.prepare(`SELECT COALESCE(MAX(seq), 0) AS seq FROM "${tableName}"`);
233
+ const row = await stmt.get();
234
+ const seq = row?.seq;
235
+ if (seq === void 0 || seq === null) return 0n;
236
+ return typeof seq === "bigint" ? seq : BigInt(String(seq));
237
+ }
238
+ async function selectMaxChangeHlc(conn, tableName = CHANGES_TABLE) {
239
+ const stmt = await conn.prepare(`SELECT MAX(hlc) AS hlc FROM "${tableName}" WHERE hlc != ''`);
240
+ const row = await stmt.get();
241
+ return typeof row?.hlc === "string" && row.hlc.length > 0 ? row.hlc : null;
242
+ }
243
+ async function ensureChangesTable(conn, tableName) {
244
+ assertSafeIdentifier(tableName);
245
+ await conn.exec(`
246
+ CREATE TABLE IF NOT EXISTS "${tableName}" (
247
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
248
+ table_name TEXT NOT NULL,
249
+ operation TEXT NOT NULL,
250
+ row_id TEXT NOT NULL,
251
+ changed_at REAL NOT NULL DEFAULT (unixepoch('subsec')),
252
+ old_data TEXT,
253
+ new_data TEXT,
254
+ node_id TEXT NOT NULL DEFAULT '',
255
+ tx_id TEXT NOT NULL DEFAULT '',
256
+ hlc TEXT NOT NULL DEFAULT ''
257
+ )`);
258
+ await ensureColumn(conn, tableName, "node_id", "TEXT", "");
259
+ await ensureColumn(conn, tableName, "tx_id", "TEXT", "");
260
+ await ensureColumn(conn, tableName, "hlc", "TEXT", "");
261
+ await conn.exec(`CREATE INDEX IF NOT EXISTS "idx_${tableName}_changed_at" ON "${tableName}" (changed_at)`);
262
+ await conn.exec(`CREATE INDEX IF NOT EXISTS "idx_${tableName}_node_id" ON "${tableName}" (node_id)`);
263
+ await conn.exec(`CREATE INDEX IF NOT EXISTS "idx_${tableName}_hlc" ON "${tableName}" (hlc)`);
264
+ await ensureMetaTable(conn);
265
+ }
266
+
267
+ // src/core/system-catalog/migrations-table.ts
268
+ var CREATE_MIGRATIONS_TABLE = `
269
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
270
+ version INTEGER PRIMARY KEY,
271
+ name TEXT NOT NULL,
272
+ applied_at REAL NOT NULL DEFAULT (unixepoch('subsec')),
273
+ checksum TEXT
274
+ )
275
+ `;
276
+ async function ensureMigrationsTable(conn) {
277
+ await conn.exec(CREATE_MIGRATIONS_TABLE);
278
+ await ensureColumn(conn, MIGRATIONS_TABLE, "checksum", "TEXT");
279
+ }
280
+ async function selectAppliedMigrations(conn) {
281
+ const stmt = await conn.prepare(`SELECT version, name, checksum FROM ${MIGRATIONS_TABLE} ORDER BY version`);
282
+ return await stmt.all();
283
+ }
284
+ async function replaceMigrationHistory(conn, rows) {
285
+ await ensureMigrationsTable(conn);
286
+ await conn.transaction(async (tx) => {
287
+ await tx.exec(`DELETE FROM ${MIGRATIONS_TABLE}`);
288
+ const insert = await tx.prepare(`INSERT INTO ${MIGRATIONS_TABLE} (version, name, checksum) VALUES (?, ?, ?)`);
289
+ for (const row of rows) {
290
+ await insert.run(row.version, row.name, row.checksum);
291
+ }
292
+ });
293
+ }
294
+ async function selectAppliedMigrationsNewestFirst(conn) {
295
+ const stmt = await conn.prepare(`SELECT version, name FROM ${MIGRATIONS_TABLE} ORDER BY version DESC`);
296
+ return await stmt.all();
297
+ }
298
+ function prepareInsertMigration(conn) {
299
+ return conn.prepare(`INSERT INTO ${MIGRATIONS_TABLE} (version, name, checksum) VALUES (?, ?, ?)`);
300
+ }
301
+ function prepareUpdateMigrationChecksum(conn) {
302
+ return conn.prepare(`UPDATE ${MIGRATIONS_TABLE} SET checksum = ? WHERE version = ?`);
303
+ }
304
+ function prepareDeleteMigration(conn) {
305
+ return conn.prepare(`DELETE FROM ${MIGRATIONS_TABLE} WHERE version = ?`);
306
+ }
307
+ function highestMigrationVersion(rows) {
308
+ let highest = 0;
309
+ for (const row of rows) {
310
+ if (row.version > highest) highest = row.version;
311
+ }
312
+ return highest;
313
+ }
314
+
315
+ // src/core/system-catalog/column-versions-table.ts
316
+ var UPSERT_COLUMN_VERSION = `INSERT INTO ${COLUMN_VERSIONS_TABLE} (table_name, row_id, column_name, hlc, node_id)
317
+ VALUES (?, ?, ?, ?, ?)
318
+ ON CONFLICT(table_name, row_id, column_name)
319
+ DO UPDATE SET hlc = excluded.hlc, node_id = excluded.node_id`;
320
+ async function ensureColumnVersionsTable(conn) {
321
+ await conn.exec(`
322
+ CREATE TABLE IF NOT EXISTS "${COLUMN_VERSIONS_TABLE}" (
323
+ table_name TEXT NOT NULL,
324
+ row_id TEXT NOT NULL,
325
+ column_name TEXT NOT NULL,
326
+ hlc TEXT NOT NULL,
327
+ node_id TEXT NOT NULL,
328
+ PRIMARY KEY (table_name, row_id, column_name)
329
+ )`);
330
+ }
331
+ async function selectMaxColumnVersionHlc(conn) {
332
+ const stmt = await conn.prepare(`SELECT MAX(hlc) AS max_hlc FROM ${COLUMN_VERSIONS_TABLE} WHERE hlc != ''`);
333
+ const row = await stmt.get();
334
+ return row?.max_hlc ?? null;
335
+ }
336
+ async function selectMaxColumnVersionHlcForRow(conn, tableName, rowId) {
337
+ const stmt = await conn.prepare(
338
+ `SELECT MAX(hlc) AS max_hlc FROM ${COLUMN_VERSIONS_TABLE} WHERE table_name = ? AND row_id = ?`
339
+ );
340
+ const row = await stmt.get(tableName, rowId);
341
+ return row?.max_hlc ?? null;
342
+ }
343
+ function prepareUpsertColumnVersion(conn) {
344
+ return conn.prepare(UPSERT_COLUMN_VERSION);
345
+ }
346
+ function prepareUpsertNewerColumnVersion(conn) {
347
+ return conn.prepare(`${UPSERT_COLUMN_VERSION}
348
+ WHERE excluded.hlc > ${COLUMN_VERSIONS_TABLE}.hlc`);
349
+ }
350
+ function prepareDeleteRowColumnVersions(conn) {
351
+ return conn.prepare(`DELETE FROM ${COLUMN_VERSIONS_TABLE} WHERE table_name = ? AND row_id = ?`);
352
+ }
353
+ function prepareDeleteRowColumnVersionsUpToHlc(conn) {
354
+ return conn.prepare(`DELETE FROM ${COLUMN_VERSIONS_TABLE} WHERE table_name = ? AND row_id = ? AND hlc <= ?`);
355
+ }
356
+
357
+ // src/core/system-catalog/device-cursors-table.ts
358
+ async function ensureDeviceCursorsTable(conn) {
359
+ await conn.exec(`
360
+ CREATE TABLE IF NOT EXISTS "${DEVICE_CURSORS_TABLE}" (
361
+ device_id TEXT PRIMARY KEY,
362
+ acked_seq INTEGER NOT NULL DEFAULT 0,
363
+ updated_at REAL NOT NULL
364
+ )`);
365
+ }
366
+ async function upsertDeviceCursor(conn, deviceId, ackedSeq, updatedAt) {
367
+ const stmt = await conn.prepare(
368
+ `INSERT INTO "${DEVICE_CURSORS_TABLE}" (device_id, acked_seq, updated_at)
369
+ VALUES (?, ?, ?)
370
+ ON CONFLICT(device_id) DO UPDATE SET
371
+ acked_seq = max("${DEVICE_CURSORS_TABLE}".acked_seq, excluded.acked_seq),
372
+ updated_at = excluded.updated_at`
373
+ );
374
+ await stmt.run(deviceId, ackedSeq.toString(), updatedAt);
375
+ }
376
+ async function deleteDeviceCursorsUpdatedBefore(conn, cutoff) {
377
+ const stmt = await conn.prepare(`DELETE FROM "${DEVICE_CURSORS_TABLE}" WHERE updated_at < ?`);
378
+ const result = await stmt.run(cutoff);
379
+ return result.changes;
380
+ }
381
+ async function selectDeviceCursors(conn) {
382
+ const stmt = await conn.prepare(`SELECT device_id, acked_seq FROM "${DEVICE_CURSORS_TABLE}"`);
383
+ const rows = await stmt.all();
384
+ return rows.map((row) => ({ deviceId: row.device_id, ackedSeq: BigInt(row.acked_seq) }));
385
+ }
386
+
387
+ // src/core/system-catalog/peer-state-table.ts
388
+ async function ensurePeerStateTable(conn) {
389
+ await conn.exec(`
390
+ CREATE TABLE IF NOT EXISTS "${PEER_STATE_TABLE}" (
391
+ peer_node_id TEXT PRIMARY KEY,
392
+ last_acked_seq INTEGER NOT NULL DEFAULT 0,
393
+ last_received_hlc TEXT NOT NULL DEFAULT '',
394
+ updated_at REAL NOT NULL
395
+ )`);
396
+ }
397
+ async function upsertPeerAckedSeq(conn, peerNodeId, seq, updatedAt) {
398
+ const stmt = await conn.prepare(
399
+ `INSERT INTO ${PEER_STATE_TABLE} (peer_node_id, last_acked_seq, updated_at)
400
+ VALUES (?, ?, ?)
401
+ ON CONFLICT(peer_node_id)
402
+ DO UPDATE SET
403
+ last_acked_seq = max(${PEER_STATE_TABLE}.last_acked_seq, excluded.last_acked_seq),
404
+ updated_at = CASE
405
+ WHEN excluded.last_acked_seq >= ${PEER_STATE_TABLE}.last_acked_seq THEN excluded.updated_at
406
+ ELSE ${PEER_STATE_TABLE}.updated_at
407
+ END`
408
+ );
409
+ await stmt.run(peerNodeId, seq.toString(), updatedAt);
410
+ }
411
+ async function selectPeerAckedSeq(conn, peerNodeId) {
412
+ const stmt = await conn.prepare(`SELECT last_acked_seq FROM ${PEER_STATE_TABLE} WHERE peer_node_id = ?`);
413
+ const row = await stmt.get(peerNodeId);
414
+ const seq = row?.last_acked_seq;
415
+ if (seq === null || seq === void 0) return 0n;
416
+ return typeof seq === "bigint" ? seq : BigInt(String(seq));
417
+ }
418
+ async function selectMinPeerAckedSeq(conn) {
419
+ const stmt = await conn.prepare(`SELECT MIN(last_acked_seq) AS min_seq, COUNT(*) AS cnt FROM ${PEER_STATE_TABLE}`);
420
+ const row = await stmt.get();
421
+ if (row === void 0 || Number(row.cnt ?? 0) === 0) return null;
422
+ const min = row.min_seq;
423
+ if (min === null || min === void 0) return 0n;
424
+ return typeof min === "bigint" ? min : BigInt(String(min));
425
+ }
426
+
427
+ // src/core/system-catalog/sync-state-table.ts
428
+ var SYNC_META_ROW = "__sync_meta__";
429
+ async function ensureSyncStateTable(conn) {
430
+ await conn.exec(`
431
+ CREATE TABLE IF NOT EXISTS "${SYNC_STATE_TABLE}" (
432
+ table_name TEXT PRIMARY KEY,
433
+ status TEXT NOT NULL DEFAULT 'pending',
434
+ row_count INTEGER NOT NULL DEFAULT 0,
435
+ pk_hash TEXT NOT NULL DEFAULT '',
436
+ snapshot_seq INTEGER,
437
+ source_peer_id TEXT,
438
+ started_at REAL,
439
+ completed_at REAL,
440
+ request_id TEXT
441
+ )`);
442
+ }
443
+ async function upsertSyncTableStatus(conn, entry) {
444
+ const stmt = await conn.prepare(
445
+ `INSERT INTO ${SYNC_STATE_TABLE} (table_name, status, row_count, pk_hash, completed_at)
446
+ VALUES (?, ?, ?, ?, ?)
447
+ ON CONFLICT(table_name) DO UPDATE SET
448
+ status = excluded.status,
449
+ row_count = COALESCE(excluded.row_count, row_count),
450
+ pk_hash = COALESCE(excluded.pk_hash, pk_hash),
451
+ completed_at = excluded.completed_at`
452
+ );
453
+ await stmt.run(entry.tableName, entry.status, entry.rowCount, entry.pkHash, entry.completedAt);
454
+ }
455
+ async function upsertSyncMeta(conn, entry) {
456
+ const stmt = await conn.prepare(
457
+ `INSERT INTO ${SYNC_STATE_TABLE} (table_name, status, snapshot_seq, source_peer_id, started_at, request_id)
458
+ VALUES ('${SYNC_META_ROW}', ?, ?, ?, ?, ?)
459
+ ON CONFLICT(table_name) DO UPDATE SET
460
+ status = excluded.status,
461
+ snapshot_seq = COALESCE(excluded.snapshot_seq, snapshot_seq),
462
+ source_peer_id = COALESCE(excluded.source_peer_id, source_peer_id),
463
+ started_at = COALESCE(excluded.started_at, started_at),
464
+ request_id = COALESCE(excluded.request_id, request_id)`
465
+ );
466
+ await stmt.run(
467
+ entry.status,
468
+ entry.snapshotSeq !== void 0 ? entry.snapshotSeq.toString() : null,
469
+ entry.sourcePeerId ?? null,
470
+ entry.startedAt,
471
+ entry.requestId ?? null
472
+ );
473
+ }
474
+ async function selectSyncMetaRow(conn) {
475
+ const stmt = await conn.prepare(
476
+ `SELECT status, snapshot_seq, source_peer_id FROM ${SYNC_STATE_TABLE} WHERE table_name = '${SYNC_META_ROW}'`
477
+ );
478
+ const row = await stmt.get();
479
+ if (!row) return null;
480
+ const seq = row.snapshot_seq;
481
+ return {
482
+ status: row.status,
483
+ snapshotSeq: seq === null || seq === void 0 ? null : typeof seq === "bigint" ? seq : BigInt(String(seq)),
484
+ sourcePeerId: row.source_peer_id
485
+ };
486
+ }
487
+ async function selectCompletedSyncTableNames(conn) {
488
+ const stmt = await conn.prepare(
489
+ `SELECT table_name FROM ${SYNC_STATE_TABLE} WHERE table_name != '${SYNC_META_ROW}' AND status = 'completed'`
490
+ );
491
+ const rows = await stmt.all();
492
+ return rows.map((row) => row.table_name);
493
+ }
494
+ async function deleteSyncTableStates(conn) {
495
+ await conn.exec(`DELETE FROM ${SYNC_STATE_TABLE} WHERE table_name != '${SYNC_META_ROW}'`);
496
+ }
497
+
498
+ // src/core/system-catalog/replication-tables.ts
499
+ async function ensureBatchApplyTables(conn) {
500
+ await ensureAppliedChangesTable(conn);
501
+ await ensureColumnVersionsTable(conn);
502
+ }
503
+ async function ensureReplicationStateTables(conn) {
504
+ await ensurePeerStateTable(conn);
505
+ await ensureBatchApplyTables(conn);
506
+ await ensureSyncStateTable(conn);
507
+ }
508
+
509
+ // src/core/system-catalog/staged-changes-table.ts
510
+ function toSeq2(value) {
511
+ if (value === null || value === void 0) return null;
512
+ return typeof value === "bigint" ? value : BigInt(String(value));
513
+ }
514
+ var STAGED_COLUMNS = "seq, table_name, operation, row_id, changed_at, old_data, new_data, node_id, tx_id, hlc, tx_end";
515
+ async function ensureStagedChangesTable(conn) {
516
+ await conn.exec(`
517
+ CREATE TABLE IF NOT EXISTS "${STAGED_CHANGES_TABLE}" (
518
+ seq INTEGER PRIMARY KEY,
519
+ table_name TEXT NOT NULL,
520
+ operation TEXT NOT NULL,
521
+ row_id TEXT NOT NULL,
522
+ changed_at REAL NOT NULL,
523
+ old_data TEXT,
524
+ new_data TEXT,
525
+ node_id TEXT NOT NULL DEFAULT '',
526
+ tx_id TEXT NOT NULL DEFAULT '',
527
+ hlc TEXT NOT NULL DEFAULT '',
528
+ tx_end INTEGER NOT NULL DEFAULT 0
529
+ )`);
530
+ await conn.exec(
531
+ `CREATE INDEX IF NOT EXISTS "idx_${STAGED_CHANGES_TABLE}_tx_end" ON "${STAGED_CHANGES_TABLE}" (tx_end, seq)`
532
+ );
533
+ }
534
+ function upsertStagedChangeSql() {
535
+ return `INSERT INTO "${STAGED_CHANGES_TABLE}" (${STAGED_COLUMNS})
536
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
537
+ ON CONFLICT(seq) DO UPDATE SET
538
+ table_name = excluded.table_name,
539
+ operation = excluded.operation,
540
+ row_id = excluded.row_id,
541
+ changed_at = excluded.changed_at,
542
+ old_data = excluded.old_data,
543
+ new_data = excluded.new_data,
544
+ node_id = excluded.node_id,
545
+ tx_id = excluded.tx_id,
546
+ hlc = excluded.hlc,
547
+ tx_end = excluded.tx_end`;
548
+ }
549
+ async function selectMaxStagedSeq(conn) {
550
+ const stmt = await conn.prepare(`SELECT MAX(seq) AS seq FROM "${STAGED_CHANGES_TABLE}"`);
551
+ const row = await stmt.get();
552
+ return toSeq2(row?.seq);
553
+ }
554
+ async function selectFirstStagedTransactionEnd(conn) {
555
+ const stmt = await conn.prepare(
556
+ `SELECT seq, tx_id FROM "${STAGED_CHANGES_TABLE}" WHERE tx_end = 1 ORDER BY seq ASC LIMIT 1`
557
+ );
558
+ const row = await stmt.get();
559
+ const seq = toSeq2(row?.seq);
560
+ if (seq === null) return null;
561
+ return { seq, txId: row?.tx_id ?? "" };
562
+ }
563
+ async function selectStagedChangesInRange(conn, afterSeq, upToSeq, limit) {
564
+ const stmt = await conn.prepare(
565
+ `SELECT ${STAGED_COLUMNS} FROM "${STAGED_CHANGES_TABLE}"
566
+ WHERE seq > ? AND seq <= ? ORDER BY seq ASC LIMIT ?`
567
+ );
568
+ return await stmt.all(afterSeq.toString(), upToSeq.toString(), limit);
569
+ }
570
+ async function deleteStagedChangesUpToSeq(conn, upToSeq) {
571
+ const stmt = await conn.prepare(`DELETE FROM "${STAGED_CHANGES_TABLE}" WHERE seq <= ?`);
572
+ await stmt.run(upToSeq.toString());
573
+ }
574
+ async function deleteAllStagedChanges(conn) {
575
+ await conn.exec(`DELETE FROM "${STAGED_CHANGES_TABLE}"`);
576
+ }
577
+
578
+ // src/core/sync/canonicalise.ts
579
+ function canonicaliseForChecksum(value) {
580
+ return canonicaliseValue(value);
581
+ }
582
+ function canonicaliseValue(v) {
583
+ if (v === null || v === void 0) return "null";
584
+ if (typeof v === "boolean") return v ? "true" : "false";
585
+ if (typeof v === "string") return JSON.stringify(v);
586
+ if (typeof v === "bigint") return `{"__sint":"${v.toString()}"}`;
587
+ if (typeof v === "number") {
588
+ if (Number.isInteger(v)) return `{"__sint":"${v.toString()}"}`;
589
+ if (Number.isFinite(v)) return JSON.stringify(v);
590
+ return "null";
591
+ }
592
+ if (typeof v === "object") {
593
+ if (v instanceof Uint8Array) {
594
+ const buf = Buffer.isBuffer(v) ? v : Buffer.from(v);
595
+ return `{"__blob":"${buf.toString("base64")}"}`;
596
+ }
597
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(v)) {
598
+ return `{"__blob":"${v.toString("base64")}"}`;
599
+ }
600
+ if (Array.isArray(v)) {
601
+ return `[${v.map(canonicaliseValue).join(",")}]`;
602
+ }
603
+ const obj = v;
604
+ const parts = [];
605
+ for (const key of Object.keys(obj).sort()) {
606
+ const val = obj[key];
607
+ if (val === void 0) continue;
608
+ parts.push(`${JSON.stringify(key)}:${canonicaliseValue(val)}`);
609
+ }
610
+ return `{${parts.join(",")}}`;
611
+ }
612
+ return "null";
613
+ }
614
+
615
+ // src/core/sync/pk.ts
616
+ var PkResolver = class {
617
+ constructor(conn) {
618
+ this.conn = conn;
619
+ }
620
+ cache = /* @__PURE__ */ new Map();
621
+ async forTable(table) {
622
+ const cached = this.cache.get(table);
623
+ if (cached) return cached;
624
+ const pkCols = await this.forTableOnConnection(this.conn, table);
625
+ this.cache.set(table, pkCols);
626
+ return pkCols;
627
+ }
628
+ async forTableOnConnection(conn, table) {
629
+ const pkCols = await tablePkColumns(conn, table);
630
+ if (pkCols.length === 0) {
631
+ pkCols.push("rowid");
632
+ }
633
+ return pkCols;
634
+ }
635
+ };
636
+
637
+ // src/core/sync/checksum.ts
638
+ function computeChecksum(changes) {
639
+ return sha256Hex(canonicaliseForChecksum(changes));
640
+ }
641
+
642
+ // src/core/sync/validators.ts
643
+ var IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
644
+ var SEQ_STRING_RE = /^\d{1,19}$/;
645
+ var SAFE_DDL_RE = /^\s*(CREATE\s+TABLE|ALTER\s+TABLE\s+\S+\s+ADD\s+COLUMN|DROP\s+TABLE|CREATE\s+INDEX|DROP\s+INDEX)\b/i;
646
+ var DDL_DENY_RE = /\b(load_extension|ATTACH|randomblob|zeroblob|writefile|readfile|fts3_tokenizer)\b/i;
647
+ function validateIdentifier(name) {
648
+ return IDENTIFIER_RE.test(name);
649
+ }
650
+ function validateDdlSafety(sql) {
651
+ if (!SAFE_DDL_RE.test(sql)) return false;
652
+ if (sql.includes(";")) return false;
653
+ if (/\bAS\s+SELECT\b/i.test(sql)) return false;
654
+ if (DDL_DENY_RE.test(sql)) return false;
655
+ const body = sql.replace(SAFE_DDL_RE, "");
656
+ if (/\bSELECT\b/i.test(body)) return false;
657
+ return true;
658
+ }
659
+ var DROP_TABLE_RE = /^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"?([A-Za-z_][A-Za-z0-9_]*)"?\s*;?\s*$/i;
660
+ function extractDroppedTable(sql) {
661
+ const m = DROP_TABLE_RE.exec(sql);
662
+ return m?.[1] ?? null;
663
+ }
664
+
665
+ export { IDENTIFIER_RE, PkResolver, SEQ_STRING_RE, UPSERT_META_VALUE_SQL, assertSafeIdentifier, canonicaliseForChecksum, computeChecksum, deleteAllStagedChanges, deleteChangesBeforeSql, deleteChangesBeforeUpToSeqSql, deleteDeviceCursorsUpdatedBefore, deleteMetaValue, deleteStagedChangesUpToSeq, deleteSyncTableStates, ensureBatchApplyTables, ensureChangesTable, ensureDeviceCursorsTable, ensureMetaTable, ensureMigrationsTable, ensureReplicationStateTables, ensureStagedChangesTable, extractDroppedTable, highestMigrationVersion, insertDdlChange, insertMetaValueIfAbsent, prepareDeleteMigration, prepareDeleteRowColumnVersions, prepareDeleteRowColumnVersionsUpToHlc, prepareInsertAppliedChange, prepareInsertMigration, prepareUpdateMigrationChecksum, prepareUpsertColumnVersion, prepareUpsertNewerColumnVersion, referencedTables, replaceMigrationHistory, selectAppliedMigrations, selectAppliedMigrationsNewestFirst, selectAppliedSourceSeqsInRange, selectChangesAfterSeqSql, selectCompletedSyncTableNames, selectCountOutboundChanges, selectCountTableRows, selectDeviceCursors, selectFirstStagedTransactionEnd, selectMaxAppliedSourceSeq, selectMaxAppliedSourceSeqByNode, selectMaxChangeHlc, selectMaxChangeSeq, selectMaxColumnVersionHlc, selectMaxColumnVersionHlcForRow, selectMaxRowChangeHlc, selectMaxStagedSeq, selectMetaValue, selectMinChangeSeqSql, selectMinForeignChangeSeqSql, selectMinPeerAckedSeq, selectNodeChangesAfterSeqSql, selectOutboundChangesSql, selectPeerAckedSeq, selectStagedChangesInRange, selectSyncMetaRow, selectTableChangesInRangeSql, selectTableExists, selectTableSql, selectTablesChangesInRangeSql, selectUserSchemaObjects, selectUserTableNames, setForeignKeysEnabled, tableColumnNames, tableInfoRows, tablePkColumns, updateChangeStampsAfterSeqSql, updateUnstampedChangeStampsSql, upsertDeviceCursor, upsertMetaValue, upsertPeerAckedSeq, upsertStagedChangeSql, upsertSyncMeta, upsertSyncTableStatus, validateDdlSafety, validateIdentifier };