@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
@@ -1,15 +1,22 @@
1
- import { runBulkLoad, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling } from '../chunk-HHRMRFFR.mjs';
2
- export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-HHRMRFFR.mjs';
3
- export { defineDriver } from '../chunk-BNUTBHHH.mjs';
4
- import { DEFAULT_SYNCHRONOUS } from '../chunk-CJLYFDP5.mjs';
5
- export { DEFAULT_SYNCHRONOUS, isSynchronousLevel, synchronousPragmaValue } from '../chunk-CJLYFDP5.mjs';
6
- import { Transaction } from '../chunk-TKGHYWQ6.mjs';
7
- export { Transaction } from '../chunk-TKGHYWQ6.mjs';
8
- import { query, queryForWire, queryOne, executeBatch, executeBatchSummary, execute, bindParams, executeGroup } from '../chunk-CW6S3WL5.mjs';
9
- export { execute, executeBatch, query, queryOne } from '../chunk-CW6S3WL5.mjs';
10
- import { MIGRATIONS_TABLE } from '../chunk-GEZUUIKV.mjs';
11
- import { SirannonError, ConnectionPoolError, MigrationError, ReadOnlyError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, WriteOverloadError, ExtensionError } from '../chunk-YPYVQJ4C.mjs';
12
- export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError, WriteOverloadError } from '../chunk-YPYVQJ4C.mjs';
1
+ import { Transaction, LWWResolver, REMOTE_ORIGIN_NODE_ID, BatchReader, BatchApplier, recordLocalColumnVersions, HLC, loadPersistedHlc, isWellFormedHlc, HLC_CLOCK_META_KEY } from '../chunk-HR5CWTLC.mjs';
2
+ export { Transaction } from '../chunk-HR5CWTLC.mjs';
3
+ import { executeBatch, executeBatchSummary, execute, bindParams, executeGroup, query, queryForWire, queryOne } from '../chunk-JZGINXTN.mjs';
4
+ export { execute, executeBatch, query, queryOne } from '../chunk-JZGINXTN.mjs';
5
+ import { tokenizeSql, unsupported, findClauses, readSource, readSortTerms, readWindow, readSelectItems, parameterName } from '../chunk-JU64Y7HM.mjs';
6
+ import { reconcileMigrationChecksums, migrationContentChecksum, runBulkLoad, ensureCdcEpoch, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling, randomHex, TransactionGrouper, PrimedSubscription } from '../chunk-H6PIVVDN.mjs';
7
+ export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-H6PIVVDN.mjs';
8
+ import { ensureMigrationsTable, prepareUpdateMigrationChecksum, prepareInsertMigration, selectAppliedMigrationsNewestFirst, prepareDeleteMigration, selectAppliedMigrations, tableInfoRows, selectTableSql, selectTableExists, selectMetaValue, upsertMetaValue, SEQ_STRING_RE, replaceMigrationHistory, highestMigrationVersion, selectCountOutboundChanges, ensureMetaTable, ensureChangesTable, ensureBatchApplyTables, PkResolver, selectMaxChangeSeq, selectMaxAppliedSourceSeq, insertMetaValueIfAbsent, selectMaxChangeHlc, updateUnstampedChangeStampsSql, UPSERT_META_VALUE_SQL, setForeignKeysEnabled, IDENTIFIER_RE, validateDdlSafety, ensureStagedChangesTable, upsertStagedChangeSql, selectFirstStagedTransactionEnd, deleteStagedChangesUpToSeq, selectMaxStagedSeq, deleteAllStagedChanges, deleteMetaValue, assertSafeIdentifier, selectStagedChangesInRange } from '../chunk-NVQS53NT.mjs';
9
+ import { ReplicationError } from '../chunk-5NOIGN5Y.mjs';
10
+ import { MIGRATIONS_TABLE, LIVE_PROBE_TABLE_PREFIX, META_TABLE, CHANGES_TABLE, assertSqlAllowed, INTERNAL_TABLE_PREFIX, encodeTaggedValues, decodeTaggedValues } from '../chunk-D7LAYTKN.mjs';
11
+ import '../chunk-GVCNMPOS.mjs';
12
+ export { defineDriver } from '../chunk-FHWTZFI4.mjs';
13
+ import { DEFAULT_SYNCHRONOUS } from '../chunk-LFZ37BSX.mjs';
14
+ export { DEFAULT_SYNCHRONOUS, isSynchronousLevel, synchronousPragmaValue } from '../chunk-LFZ37BSX.mjs';
15
+ import { LAZY_DOWN_SQL, parseMigrationFilename } from '../chunk-LNY2VVHE.mjs';
16
+ export { MIGRATION_FILENAME_PATTERN, parseMigrationFilename } from '../chunk-LNY2VVHE.mjs';
17
+ import { resolveEffectiveBaseline, planPendingMigrations, SQLITE_USER_VERSION_MAX, applyBaselineOption } from '../chunk-67M7KAH6.mjs';
18
+ import { SirannonError, ConnectionPoolError, MigrationError, ReadOnlyError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, CDCError, ExtensionError, WriteOverloadError } from '../chunk-UC3SCMIN.mjs';
19
+ export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, RequestDeniedError, SirannonError, TransactionError, WriteOverloadError } from '../chunk-UC3SCMIN.mjs';
13
20
 
14
21
  // src/core/connection-pool.ts
15
22
  async function closeAllSilently(connections) {
@@ -202,25 +209,170 @@ var CdcAwareTransaction = class extends Transaction {
202
209
  }
203
210
  };
204
211
 
212
+ // src/core/cdc/read-position.ts
213
+ var TOKEN_VERSION = "1";
214
+ var HEX_RE = /^[0-9a-f]+$/;
215
+ function encodeReadPosition(position) {
216
+ if (!HEX_RE.test(position.epoch)) {
217
+ throw new CDCError(`Cannot issue a read position for epoch '${position.epoch}': an epoch is lower-case hex`);
218
+ }
219
+ if (position.seq < 0n) {
220
+ throw new CDCError("Cannot issue a read position for a negative change-log sequence");
221
+ }
222
+ return toHex(`${TOKEN_VERSION}:${position.epoch}:${position.seq.toString()}`);
223
+ }
224
+ async function readAtPosition(conn, epoch, read) {
225
+ return conn.transaction(async (txConn) => {
226
+ const value = await read(txConn);
227
+ const seq = await selectTableExists(txConn, CHANGES_TABLE) ? await selectMaxChangeSeq(txConn) : 0n;
228
+ return { value, position: encodeReadPosition({ epoch, seq }), seq };
229
+ });
230
+ }
231
+ function toHex(value) {
232
+ let hex = "";
233
+ for (let i = 0; i < value.length; i++) {
234
+ hex += value.charCodeAt(i).toString(16).padStart(2, "0");
235
+ }
236
+ return hex;
237
+ }
238
+
239
+ // src/core/sync/stamper.ts
240
+ var NODE_ID_META_KEY = "node_id";
241
+ var SyncStamper = class _SyncStamper {
242
+ constructor(nodeId, hlc, changesTable) {
243
+ this.nodeId = nodeId;
244
+ this.hlc = hlc;
245
+ this.changesTable = changesTable;
246
+ }
247
+ static async init(conn, changesTable = CHANGES_TABLE) {
248
+ await ensureMetaTable(conn);
249
+ await insertMetaValueIfAbsent(conn, NODE_ID_META_KEY, randomHex(16));
250
+ const nodeId = await selectMetaValue(conn, NODE_ID_META_KEY);
251
+ if (nodeId === null || nodeId.length === 0) {
252
+ throw new CDCError("Failed to read the persisted node identity");
253
+ }
254
+ const hlc = new HLC(nodeId);
255
+ const persisted = await loadPersistedHlc(conn);
256
+ if (persisted !== null) {
257
+ hlc.receive(persisted);
258
+ }
259
+ const persistedMax = await selectMaxChangeHlc(conn, changesTable);
260
+ if (persistedMax !== null && isWellFormedHlc(persistedMax)) {
261
+ hlc.receive(persistedMax);
262
+ }
263
+ return new _SyncStamper(nodeId, hlc, changesTable);
264
+ }
265
+ stampStatements(options) {
266
+ const hlcValue = this.hlc.now();
267
+ const txId = randomHex(16);
268
+ const statements = [
269
+ {
270
+ sql: updateUnstampedChangeStampsSql(this.changesTable),
271
+ params: [this.nodeId, txId, hlcValue],
272
+ trusted: true
273
+ }
274
+ ];
275
+ if (options?.persistClock !== false) {
276
+ statements.push({
277
+ sql: UPSERT_META_VALUE_SQL,
278
+ params: [HLC_CLOCK_META_KEY, hlcValue],
279
+ trusted: true
280
+ });
281
+ }
282
+ return statements;
283
+ }
284
+ async applyStamps(txConn) {
285
+ for (const statement of this.stampStatements()) {
286
+ const stmt = await txConn.prepare(statement.sql);
287
+ await stmt.run(...statement.params);
288
+ }
289
+ }
290
+ };
291
+
205
292
  // src/core/database-cdc.ts
206
293
  var DatabaseCdcController = class {
207
- constructor(runExclusive, acquireWriter, pollInterval, retention) {
294
+ constructor(runExclusive, acquireWriter, pollInterval, retention, openSnapshotConnection) {
208
295
  this.runExclusive = runExclusive;
209
296
  this.acquireWriter = acquireWriter;
210
297
  this.pollInterval = pollInterval;
211
298
  this.retention = retention;
299
+ this.openSnapshotConnection = openSnapshotConnection;
212
300
  }
213
301
  tracker = null;
214
- subscriptions = null;
302
+ subscriptionManager = null;
303
+ liveConnRequest = null;
215
304
  stopPolling = null;
305
+ stamper = null;
306
+ epochRequest = null;
307
+ ensureEpoch() {
308
+ this.epochRequest ??= this.runExclusive(() => ensureCdcEpoch(this.acquireWriter())).catch((err) => {
309
+ this.epochRequest = null;
310
+ throw err;
311
+ });
312
+ return this.epochRequest;
313
+ }
314
+ async readAtPositionWith(read) {
315
+ const epoch = await this.ensureEpoch();
316
+ const open = this.openSnapshotConnection;
317
+ if (open === null) {
318
+ return this.runExclusive(() => readAtPosition(this.acquireWriter(), epoch, read));
319
+ }
320
+ const conn = await open();
321
+ try {
322
+ return await readAtPosition(conn, epoch, read);
323
+ } finally {
324
+ await conn.close().catch(() => {
325
+ });
326
+ }
327
+ }
328
+ async liveConnection() {
329
+ const open = this.openSnapshotConnection;
330
+ if (open === null) {
331
+ return { conn: this.acquireWriter(), run: this.runExclusive };
332
+ }
333
+ this.liveConnRequest ??= open().catch((err) => {
334
+ this.liveConnRequest = null;
335
+ throw err;
336
+ });
337
+ return { conn: await this.liveConnRequest, run: (op) => op() };
338
+ }
339
+ async closeLiveConnection() {
340
+ const request = this.liveConnRequest;
341
+ if (request === null) return;
342
+ this.liveConnRequest = null;
343
+ await request.then((conn) => conn.close()).catch(() => {
344
+ });
345
+ }
346
+ get subscriptions() {
347
+ return this.subscriptionManager;
348
+ }
216
349
  get changeTracker() {
217
350
  return this.tracker;
218
351
  }
219
352
  async watch(table) {
220
353
  const { tracker } = this.ensure();
221
- await this.runExclusive(() => tracker.watch(this.acquireWriter(), table));
354
+ await this.runExclusive(async () => {
355
+ await tracker.watch(this.acquireWriter(), table);
356
+ await this.ensureStamper();
357
+ });
222
358
  this.ensurePolling();
223
359
  }
360
+ async ensureStamper() {
361
+ this.stamper ??= await SyncStamper.init(this.acquireWriter());
362
+ return this.stamper;
363
+ }
364
+ async ensureStamping() {
365
+ if (this.stamper) return;
366
+ await this.runExclusive(() => this.ensureStamper());
367
+ }
368
+ stampStatements(options) {
369
+ return this.stamper ? this.stamper.stampStatements(options) : null;
370
+ }
371
+ async applyStamps(txConn) {
372
+ if (this.stamper) {
373
+ await this.stamper.applyStamps(txConn);
374
+ }
375
+ }
224
376
  async unwatch(table) {
225
377
  const tracker = this.tracker;
226
378
  if (!tracker) return;
@@ -239,7 +391,11 @@ var DatabaseCdcController = class {
239
391
  return Transaction.run(writer, fn);
240
392
  }
241
393
  const state = { sawDdl: false, droppedTables: [] };
242
- const result = await writer.transaction((txConn) => fn(new CdcAwareTransaction(txConn, tracker, state)));
394
+ const result = await writer.transaction(async (txConn) => {
395
+ const value = await fn(new CdcAwareTransaction(txConn, tracker, state));
396
+ await this.applyStamps(txConn);
397
+ return value;
398
+ });
243
399
  if (state.sawDdl && state.droppedTables.length > 0) {
244
400
  await tracker.pruneDroppedTables(writer, state.droppedTables);
245
401
  }
@@ -253,18 +409,18 @@ var DatabaseCdcController = class {
253
409
  }
254
410
  ensure() {
255
411
  const tracker = this.tracker ?? new ChangeTracker({ retention: this.retention });
256
- const subscriptions = this.subscriptions ?? new SubscriptionManager();
412
+ const subscriptions = this.subscriptionManager ?? new SubscriptionManager();
257
413
  this.tracker = tracker;
258
- this.subscriptions = subscriptions;
414
+ this.subscriptionManager = subscriptions;
259
415
  return { tracker, subscriptions };
260
416
  }
261
417
  ensurePolling() {
262
418
  if (this.stopPolling) return;
263
- if (!this.tracker || !this.subscriptions) return;
419
+ if (!this.tracker || !this.subscriptionManager) return;
264
420
  this.stopPolling = startPolling(
265
421
  this.acquireWriter(),
266
422
  this.tracker,
267
- this.subscriptions,
423
+ this.subscriptionManager,
268
424
  this.pollInterval,
269
425
  void 0,
270
426
  this.runExclusive
@@ -310,7 +466,6 @@ var DatabaseObserver = class {
310
466
  this.parentHooks = parentHooks;
311
467
  this.metrics = metrics;
312
468
  }
313
- /** False lets a caller skip the timing calls on a path where nothing would read them. */
314
469
  get observesQueries() {
315
470
  return this.metrics !== null || this.localHooks.has("beforeQuery") || this.localHooks.has("afterQuery") || (this.parentHooks?.has("beforeQuery") ?? false) || (this.parentHooks?.has("afterQuery") ?? false);
316
471
  }
@@ -327,12 +482,6 @@ var DatabaseObserver = class {
327
482
  if (!this.metrics) return op();
328
483
  return this.metrics.trackQuery(op, { databaseId: this.databaseId, sql });
329
484
  }
330
- /**
331
- * Reports each statement separately, since a hook filtering on SQL has no other
332
- * way to see them. They share the transaction's duration because the group runs
333
- * them back to back under one commit and no statement has a duration of its own,
334
- * which is already what a grouped {@link Database.execute} reports.
335
- */
336
485
  async withTransactionHooks(statements, op) {
337
486
  for (const statement of statements) this.fireBefore(statement.sql, statement.params);
338
487
  const start = performance.now();
@@ -362,32 +511,497 @@ var DatabaseObserver = class {
362
511
  }
363
512
  };
364
513
 
365
- // src/core/extension-loader.ts
366
- async function loadExtension(driver, writer, extensionPath) {
367
- if (!driver.capabilities.extensions || !driver.resolveExtensionPath) {
368
- throw new ExtensionError(extensionPath, "Extensions are not supported by the current driver");
514
+ // src/core/migrations/schema-version.ts
515
+ async function readSchemaVersion(conn) {
516
+ const stmt = await conn.prepare("PRAGMA user_version");
517
+ const row = await stmt.get();
518
+ return row === void 0 ? 0 : Number(row.user_version);
519
+ }
520
+ async function writeSchemaVersion(conn, version) {
521
+ if (!Number.isSafeInteger(version) || version < 0 || version > SQLITE_USER_VERSION_MAX) {
522
+ throw new SirannonError(
523
+ `Schema version is outside the PRAGMA user_version range: ${version}`,
524
+ "INTERNAL_SCHEMA_ERROR"
525
+ );
369
526
  }
370
- if (!extensionPath || extensionPath.includes("\0")) {
371
- throw new ExtensionError(extensionPath || "", "Extension path is empty or contains null bytes");
527
+ await conn.exec(`PRAGMA user_version = ${version}`);
528
+ }
529
+ async function mirrorSchemaVersion(conn, version) {
530
+ if (version > SQLITE_USER_VERSION_MAX) return;
531
+ await writeSchemaVersion(conn, version);
532
+ }
533
+ async function syncSchemaVersion(conn, expected) {
534
+ if (expected > SQLITE_USER_VERSION_MAX) return;
535
+ const current = await readSchemaVersion(conn);
536
+ if (current !== expected) {
537
+ await writeSchemaVersion(conn, expected);
372
538
  }
373
- for (let i = 0; i < extensionPath.length; i++) {
374
- if (extensionPath.charCodeAt(i) <= 31) {
375
- throw new ExtensionError(extensionPath, "Extension path contains control characters");
539
+ }
540
+
541
+ // src/core/sync/snapshot-apply.ts
542
+ var SNAPSHOT_STATE_META_KEY = "device_sync_snapshot_state";
543
+ var SNAPSHOT_STATE_LOADING = "loading";
544
+ function assertTableNames(tables) {
545
+ for (const table of tables) {
546
+ if (!IDENTIFIER_RE.test(table)) {
547
+ throw new ReplicationError(`Invalid table name: ${table}`);
376
548
  }
377
549
  }
378
- const segments = extensionPath.split(/[/\\]/);
379
- if (segments.includes("..")) {
380
- throw new ExtensionError(extensionPath, "Extension path must not contain directory traversal segments");
550
+ }
551
+ async function snapshotLoadPending(conn) {
552
+ if (!await selectTableExists(conn, META_TABLE)) return false;
553
+ return await selectMetaValue(conn, SNAPSHOT_STATE_META_KEY) !== null;
554
+ }
555
+ async function beginSnapshotLoad(conn, tables, tracker) {
556
+ assertTableNames(tables);
557
+ await ensureMetaTable(conn);
558
+ await upsertMetaValue(conn, SNAPSHOT_STATE_META_KEY, SNAPSHOT_STATE_LOADING);
559
+ await setForeignKeysEnabled(conn, false);
560
+ await ensureStagedChangesTable(conn);
561
+ await conn.transaction(async (tx) => {
562
+ await deleteAllStagedChanges(tx);
563
+ for (const table of tables) {
564
+ await tracker?.unwatch(tx, table);
565
+ }
566
+ const reversed = [...tables].reverse();
567
+ for (const table of reversed) {
568
+ await tx.exec(`DROP TABLE IF EXISTS "${table}"`);
569
+ }
570
+ });
571
+ }
572
+ async function applySnapshotSchema(conn, schema) {
573
+ for (const ddl of schema) {
574
+ if (!validateDdlSafety(ddl)) {
575
+ throw new ReplicationError(`Snapshot schema statement failed safety validation: ${ddl.slice(0, 120)}`);
576
+ }
381
577
  }
382
- const resolved = driver.resolveExtensionPath(extensionPath);
578
+ for (const ddl of schema) {
579
+ const rewritten = ddl.replace(/^\s*CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/i, "CREATE TABLE IF NOT EXISTS ").replace(/^\s*CREATE\s+(UNIQUE\s+)?INDEX\s+(?!IF\s+NOT\s+EXISTS)/i, "CREATE $1INDEX IF NOT EXISTS ");
580
+ await conn.exec(rewritten);
581
+ }
582
+ }
583
+ async function loadSnapshotPage(conn, table, rows) {
584
+ if (!IDENTIFIER_RE.test(table)) {
585
+ throw new ReplicationError(`Invalid table name: ${table}`);
586
+ }
587
+ if (rows.length === 0) return;
588
+ const columns = Object.keys(rows[0]);
589
+ if (columns.length === 0) {
590
+ throw new ReplicationError(`Snapshot page for table '${table}' carries rows without columns`);
591
+ }
592
+ for (const column of columns) {
593
+ if (!IDENTIFIER_RE.test(column) && column !== "rowid") {
594
+ throw new ReplicationError(`Invalid column name in snapshot page for table '${table}': ${column}`);
595
+ }
596
+ }
597
+ const columnSet = new Set(columns);
598
+ for (const row of rows) {
599
+ const keys = Object.keys(row);
600
+ if (keys.length !== columns.length || keys.some((key) => !columnSet.has(key))) {
601
+ throw new ReplicationError(`Snapshot page rows for table '${table}' do not share one column set`);
602
+ }
603
+ }
604
+ const columnList = columns.map((column) => `"${column}"`).join(", ");
605
+ const placeholders = columns.map(() => "?").join(", ");
606
+ await conn.transaction(async (tx) => {
607
+ const insert = await tx.prepare(`INSERT INTO "${table}" (${columnList}) VALUES (${placeholders})`);
608
+ for (const row of rows) {
609
+ await insert.run(...columns.map((column) => row[column]));
610
+ }
611
+ });
612
+ }
613
+ async function endSnapshotLoad(conn, tables, tracker) {
614
+ assertTableNames(tables);
615
+ for (const table of tables) {
616
+ await tracker?.watch(conn, table);
617
+ }
618
+ await setForeignKeysEnabled(conn, true);
619
+ await deleteMetaValue(conn, SNAPSHOT_STATE_META_KEY);
620
+ }
621
+ async function abortSnapshotLoad(conn) {
622
+ await setForeignKeysEnabled(conn, true);
623
+ }
624
+
625
+ // src/core/sync/staged-pull.ts
626
+ var STAGED_APPLY_BATCH_ROWS = 500;
627
+ async function stagePulledChanges(conn, events) {
628
+ if (events.length === 0) return null;
629
+ await ensureStagedChangesTable(conn);
630
+ let maxSeq2 = null;
631
+ await conn.transaction(async (tx) => {
632
+ const stmt = await tx.prepare(upsertStagedChangeSql());
633
+ for (const event of events) {
634
+ await stmt.run(
635
+ event.seq.toString(),
636
+ event.table,
637
+ event.type,
638
+ event.rowId ?? "",
639
+ event.timestamp,
640
+ event.oldRow === void 0 ? null : JSON.stringify(encodeTaggedValues(event.oldRow)),
641
+ event.type === "delete" ? null : JSON.stringify(encodeTaggedValues(event.row)),
642
+ event.origin ?? "",
643
+ event.txId ?? "",
644
+ event.hlc ?? "",
645
+ event.txEnd === true ? 1 : 0
646
+ );
647
+ if (maxSeq2 === null || event.seq > maxSeq2) {
648
+ maxSeq2 = event.seq;
649
+ }
650
+ }
651
+ });
652
+ return maxSeq2;
653
+ }
654
+ async function applyStagedTransactions(conn, applier, options) {
655
+ await ensureStagedChangesTable(conn);
656
+ let appliedThrough = null;
657
+ while (true) {
658
+ const head = await selectFirstStagedTransactionEnd(conn);
659
+ if (head === null) break;
660
+ const endSeq = head.seq;
661
+ const result = await applier.applyStagedGroup({
662
+ source: {
663
+ nextBatch: (afterSeq) => readStagedEntries(conn, afterSeq, endSeq)
664
+ },
665
+ resolver: options.resolver,
666
+ withinTx: options.withinTx === void 0 ? void 0 : (tx) => options.withinTx?.(tx, endSeq) ?? Promise.resolve()
667
+ });
668
+ if (result.maxHlc !== "") {
669
+ await applier.mergeHlc(result.maxHlc);
670
+ }
671
+ if (options.onChange) {
672
+ await emitStagedEvents(conn, endSeq, options.onChange);
673
+ }
674
+ await deleteStagedChangesUpToSeq(conn, endSeq);
675
+ appliedThrough = endSeq;
676
+ }
677
+ return appliedThrough;
678
+ }
679
+ async function recoverStagedPull(conn, applier, appliedFloor, options) {
680
+ await ensureStagedChangesTable(conn);
681
+ if (appliedFloor !== null) {
682
+ await deleteStagedChangesUpToSeq(conn, appliedFloor);
683
+ }
684
+ let appliedThrough = null;
685
+ let applyError = null;
383
686
  try {
384
- const escaped = resolved.replace(/'/g, "''");
385
- await writer.exec(`SELECT load_extension('${escaped}')`);
687
+ appliedThrough = await applyStagedTransactions(conn, applier, options);
386
688
  } catch (err) {
387
- throw new ExtensionError(extensionPath, err instanceof Error ? err.message : String(err));
689
+ applyError = err;
690
+ }
691
+ const stagedMax = await selectMaxStagedSeq(conn);
692
+ const appliedSeq = maxSeq(appliedFloor, appliedThrough);
693
+ return { resumeSeq: maxSeq(appliedSeq, stagedMax), appliedSeq, applyError };
694
+ }
695
+ function maxSeq(a, b) {
696
+ if (a === null) return b;
697
+ if (b === null) return a;
698
+ return a > b ? a : b;
699
+ }
700
+ async function readStagedEntries(conn, afterSeq, upToSeq) {
701
+ const rows = await selectStagedChangesInRange(conn, afterSeq, upToSeq, STAGED_APPLY_BATCH_ROWS);
702
+ return rows.map((row) => ({ seq: BigInt(row.seq), change: rowToReplicationChange(row) }));
703
+ }
704
+ function rowToReplicationChange(row) {
705
+ return {
706
+ table: row.table_name,
707
+ operation: row.operation,
708
+ rowId: row.row_id,
709
+ primaryKey: {},
710
+ hlc: row.hlc,
711
+ txId: row.tx_id,
712
+ nodeId: row.node_id,
713
+ newData: row.new_data === null ? null : decodeTaggedValues(JSON.parse(row.new_data)),
714
+ oldData: row.old_data === null ? null : decodeTaggedValues(JSON.parse(row.old_data))
715
+ };
716
+ }
717
+ function rowToChangeEvent(row) {
718
+ return {
719
+ type: row.operation,
720
+ table: row.table_name,
721
+ row: row.new_data === null ? {} : decodeTaggedValues(JSON.parse(row.new_data)),
722
+ oldRow: row.old_data === null ? void 0 : decodeTaggedValues(JSON.parse(row.old_data)),
723
+ seq: BigInt(row.seq),
724
+ timestamp: row.changed_at,
725
+ rowId: row.row_id,
726
+ ...row.node_id !== "" ? { origin: row.node_id } : {},
727
+ ...row.hlc !== "" ? { hlc: row.hlc } : {},
728
+ ...row.tx_id !== "" ? { txId: row.tx_id } : {},
729
+ ...row.tx_end === 1 ? { txEnd: true } : {}
730
+ };
731
+ }
732
+ async function emitStagedEvents(conn, upToSeq, onChange) {
733
+ let cursor = 0n;
734
+ while (true) {
735
+ const rows = await selectStagedChangesInRange(conn, cursor, upToSeq, STAGED_APPLY_BATCH_ROWS);
736
+ if (rows.length === 0) return;
737
+ for (const row of rows) {
738
+ cursor = BigInt(row.seq);
739
+ try {
740
+ onChange(rowToChangeEvent(row));
741
+ } catch {
742
+ }
743
+ }
388
744
  }
389
745
  }
390
746
 
747
+ // src/core/database-sync.ts
748
+ var PUSHED_SEQ_META_KEY = "device_sync_pushed_seq";
749
+ var PULL_SEQ_META_KEY = "device_sync_pull_seq";
750
+ var PULL_EPOCH_META_KEY = "device_sync_pull_epoch";
751
+ var RESYNC_REQUIRED_META_KEY = "device_sync_resync_required";
752
+ var COLUMN_VERSIONS_SEQ_META_KEY = "device_sync_column_versions_seq";
753
+ var DatabaseSyncController = class {
754
+ constructor(runExclusive, acquireWriter, cdc) {
755
+ this.runExclusive = runExclusive;
756
+ this.acquireWriter = acquireWriter;
757
+ this.cdc = cdc;
758
+ }
759
+ pkResolver = null;
760
+ tablesReady = false;
761
+ metaReady = false;
762
+ outboxReader = null;
763
+ localPruneBoundary = null;
764
+ snapshotGate = false;
765
+ defaultResolver = new LWWResolver();
766
+ applyChanges(batch, resolver) {
767
+ return this.runExclusive(async () => {
768
+ const applier = await this.ensureApplier();
769
+ const result = await applier.applyBatch(batch, resolver ?? this.defaultResolver);
770
+ if (result.droppedTables.length > 0) {
771
+ await this.cdc.changeTracker?.pruneDroppedTables(this.acquireWriter(), result.droppedTables);
772
+ }
773
+ return result;
774
+ });
775
+ }
776
+ devicePort() {
777
+ return {
778
+ identity: () => this.runExclusive(async () => ({ nodeId: (await this.cdc.ensureStamper()).nodeId })),
779
+ applyPulledTransaction: (changes, pullSeq, resolver) => this.applyPulledTransaction(changes, pullSeq, resolver),
780
+ stagePulledChanges: (events) => this.stagePulled(events),
781
+ applyStagedPull: (resolver, onChange) => this.applyStagedPull(resolver, onChange),
782
+ recoverStagedPull: (resolver, onChange) => this.recoverStagedPullState(resolver, onChange),
783
+ readOutboxBatch: (afterSeq, limit) => this.readOutboxBatch(afterSeq, limit),
784
+ countOutboxPending: (afterSeq) => this.countOutboxPending(afterSeq),
785
+ getPushCursor: () => this.getMetaSeq(PUSHED_SEQ_META_KEY).then((seq) => seq ?? 0n),
786
+ setPushCursor: (seq) => this.setMetaSeq(PUSHED_SEQ_META_KEY, seq),
787
+ getPullState: () => this.getPullState(),
788
+ getResyncRequired: () => this.getResyncRequired(),
789
+ setResyncRequired: (required) => this.setResyncRequired(required),
790
+ setPullState: (seq, epoch) => this.setPullState(seq, epoch),
791
+ protectUnpushedChanges: (pushedSeq) => {
792
+ this.localPruneBoundary = pushedSeq;
793
+ this.applyLocalPruneBoundary();
794
+ },
795
+ snapshotLoadPending: () => this.runExclusive(() => snapshotLoadPending(this.acquireWriter())),
796
+ beginSnapshotLoad: (tables) => this.beginSnapshotLoad(tables),
797
+ applySnapshotSchema: (schema) => this.runExclusive(() => applySnapshotSchema(this.acquireWriter(), schema)),
798
+ loadSnapshotPage: (table, rows) => this.runExclusive(() => loadSnapshotPage(this.acquireWriter(), table, rows)),
799
+ replaceMigrationHistory: (rows) => this.replaceMigrationHistory(rows),
800
+ endSnapshotLoad: (tables) => this.endSnapshotLoad(tables),
801
+ abortSnapshotLoad: () => this.runExclusive(() => abortSnapshotLoad(this.acquireWriter()))
802
+ };
803
+ }
804
+ get snapshotLoadBlocked() {
805
+ return this.snapshotGate;
806
+ }
807
+ seedSnapshotGate() {
808
+ this.snapshotGate = true;
809
+ }
810
+ applyPulledTransaction(changes, pullSeq, resolver) {
811
+ return this.runExclusive(async () => {
812
+ if (changes.length === 0) {
813
+ return { applied: 0, skipped: 0, conflicts: 0, droppedTables: [] };
814
+ }
815
+ await this.ensureMeta();
816
+ const applier = await this.ensureApplier();
817
+ let maxHlc = "";
818
+ let sourceNodeId = REMOTE_ORIGIN_NODE_ID;
819
+ for (const change of changes) {
820
+ if (change.hlc > maxHlc) maxHlc = change.hlc;
821
+ if (sourceNodeId === REMOTE_ORIGIN_NODE_ID && change.nodeId !== "") sourceNodeId = change.nodeId;
822
+ }
823
+ const result = await applier.applyGroup({
824
+ sourceNodeId,
825
+ txId: changes[0].txId,
826
+ changes,
827
+ resolver: resolver ?? this.defaultResolver,
828
+ withinTx: (tx) => upsertMetaValue(tx, PULL_SEQ_META_KEY, pullSeq.toString())
829
+ });
830
+ if (result.droppedTables.length > 0) {
831
+ await this.cdc.changeTracker?.pruneDroppedTables(this.acquireWriter(), result.droppedTables);
832
+ }
833
+ if (maxHlc !== "") {
834
+ await applier.mergeHlc(maxHlc);
835
+ }
836
+ return result;
837
+ });
838
+ }
839
+ stagePulled(events) {
840
+ return this.runExclusive(async () => {
841
+ await this.ensureMeta();
842
+ return stagePulledChanges(this.acquireWriter(), events);
843
+ });
844
+ }
845
+ applyStagedPull(resolver, onChange) {
846
+ return this.runExclusive(async () => {
847
+ await this.ensureMeta();
848
+ const applier = await this.ensureApplier();
849
+ return applyStagedTransactions(this.acquireWriter(), applier, this.stagedApplyOptions(resolver, onChange));
850
+ });
851
+ }
852
+ recoverStagedPullState(resolver, onChange) {
853
+ return this.runExclusive(async () => {
854
+ const writer = await this.ensureMeta();
855
+ const applier = await this.ensureApplier();
856
+ const recorded = await selectMetaValue(writer, PULL_SEQ_META_KEY);
857
+ const appliedFloor = recorded !== null && SEQ_STRING_RE.test(recorded) ? BigInt(recorded) : null;
858
+ return recoverStagedPull(writer, applier, appliedFloor, this.stagedApplyOptions(resolver, onChange));
859
+ });
860
+ }
861
+ stagedApplyOptions(resolver, onChange) {
862
+ return {
863
+ resolver: resolver ?? this.defaultResolver,
864
+ withinTx: (tx, appliedThroughSeq) => upsertMetaValue(tx, PULL_SEQ_META_KEY, appliedThroughSeq.toString()),
865
+ onChange
866
+ };
867
+ }
868
+ replaceMigrationHistory(rows) {
869
+ return this.runExclusive(async () => {
870
+ const writer = this.acquireWriter();
871
+ await replaceMigrationHistory(writer, rows);
872
+ await mirrorSchemaVersion(writer, highestMigrationVersion(rows));
873
+ });
874
+ }
875
+ async beginSnapshotLoad(tables) {
876
+ this.snapshotGate = true;
877
+ try {
878
+ await this.runExclusive(() => beginSnapshotLoad(this.acquireWriter(), tables, this.cdc.changeTracker));
879
+ } catch (err) {
880
+ this.snapshotGate = await this.runExclusive(() => snapshotLoadPending(this.acquireWriter()));
881
+ throw err;
882
+ }
883
+ }
884
+ async endSnapshotLoad(tables) {
885
+ await this.runExclusive(() => endSnapshotLoad(this.acquireWriter(), tables, this.cdc.changeTracker));
886
+ this.snapshotGate = false;
887
+ }
888
+ applyLocalPruneBoundary() {
889
+ if (this.localPruneBoundary !== null) {
890
+ this.cdc.changeTracker?.setPruneBoundary(this.localPruneBoundary);
891
+ }
892
+ }
893
+ readOutboxBatch(afterSeq, limit) {
894
+ return this.runExclusive(async () => {
895
+ this.applyLocalPruneBoundary();
896
+ const reader = await this.ensureOutboxReader();
897
+ return reader.readBatch(afterSeq, limit);
898
+ });
899
+ }
900
+ countOutboxPending(afterSeq) {
901
+ return this.runExclusive(async () => {
902
+ this.applyLocalPruneBoundary();
903
+ const writer = this.acquireWriter();
904
+ await this.ensureSyncTables(writer);
905
+ const stamper = await this.cdc.ensureStamper();
906
+ return selectCountOutboundChanges(writer, CHANGES_TABLE, afterSeq, stamper.nodeId);
907
+ });
908
+ }
909
+ getResyncRequired() {
910
+ return this.runExclusive(async () => {
911
+ const writer = await this.ensureMeta();
912
+ return await selectMetaValue(writer, RESYNC_REQUIRED_META_KEY) === "1";
913
+ });
914
+ }
915
+ setResyncRequired(required) {
916
+ return this.runExclusive(async () => {
917
+ const writer = await this.ensureMeta();
918
+ await upsertMetaValue(writer, RESYNC_REQUIRED_META_KEY, required ? "1" : "0");
919
+ });
920
+ }
921
+ getPullState() {
922
+ return this.runExclusive(async () => {
923
+ const writer = await this.ensureMeta();
924
+ const seq = await selectMetaValue(writer, PULL_SEQ_META_KEY);
925
+ if (seq === null || !SEQ_STRING_RE.test(seq)) return null;
926
+ const epoch = await selectMetaValue(writer, PULL_EPOCH_META_KEY);
927
+ return { seq: BigInt(seq), epoch: epoch ?? void 0 };
928
+ });
929
+ }
930
+ setPullState(seq, epoch) {
931
+ return this.runExclusive(async () => {
932
+ const writer = await this.ensureMeta();
933
+ await upsertMetaValue(writer, PULL_SEQ_META_KEY, seq.toString());
934
+ if (epoch !== void 0) {
935
+ await upsertMetaValue(writer, PULL_EPOCH_META_KEY, epoch);
936
+ }
937
+ });
938
+ }
939
+ getMetaSeq(key) {
940
+ return this.runExclusive(async () => {
941
+ const writer = await this.ensureMeta();
942
+ const value = await selectMetaValue(writer, key);
943
+ if (value === null || !SEQ_STRING_RE.test(value)) return null;
944
+ return BigInt(value);
945
+ });
946
+ }
947
+ setMetaSeq(key, seq) {
948
+ return this.runExclusive(async () => {
949
+ const writer = await this.ensureMeta();
950
+ await upsertMetaValue(writer, key, seq.toString());
951
+ });
952
+ }
953
+ async ensureMeta() {
954
+ const writer = this.acquireWriter();
955
+ if (!this.metaReady) {
956
+ await ensureMetaTable(writer);
957
+ this.metaReady = true;
958
+ }
959
+ return writer;
960
+ }
961
+ async ensureSyncTables(writer) {
962
+ if (!this.tablesReady) {
963
+ await ensureChangesTable(writer, CHANGES_TABLE);
964
+ await ensureBatchApplyTables(writer);
965
+ this.tablesReady = true;
966
+ }
967
+ }
968
+ async ensureOutboxReader() {
969
+ const writer = this.acquireWriter();
970
+ await this.ensureSyncTables(writer);
971
+ const stamper = await this.cdc.ensureStamper();
972
+ this.pkResolver ??= new PkResolver(writer);
973
+ this.outboxReader ??= new BatchReader(writer, stamper.nodeId, CHANGES_TABLE, this.pkResolver, true);
974
+ return this.outboxReader;
975
+ }
976
+ async ensureApplier() {
977
+ const writer = this.acquireWriter();
978
+ await this.ensureSyncTables(writer);
979
+ const stamper = await this.cdc.ensureStamper();
980
+ this.pkResolver ??= new PkResolver(writer);
981
+ return new BatchApplier(
982
+ writer,
983
+ stamper.nodeId,
984
+ stamper.hlc,
985
+ this.pkResolver,
986
+ (fromNodeId) => this.lastAppliedSeq(fromNodeId),
987
+ this.cdc.changeTracker ?? void 0,
988
+ CHANGES_TABLE,
989
+ (tx) => this.catchUpLocalColumnVersions(tx, stamper.nodeId)
990
+ );
991
+ }
992
+ async catchUpLocalColumnVersions(tx, localNodeId) {
993
+ const recorded = await selectMetaValue(tx, COLUMN_VERSIONS_SEQ_META_KEY);
994
+ const afterSeq = recorded !== null && SEQ_STRING_RE.test(recorded) ? BigInt(recorded) : 0n;
995
+ const latestSeq = await selectMaxChangeSeq(tx, CHANGES_TABLE);
996
+ if (latestSeq <= afterSeq) return;
997
+ await recordLocalColumnVersions(tx, CHANGES_TABLE, localNodeId, afterSeq);
998
+ await upsertMetaValue(tx, COLUMN_VERSIONS_SEQ_META_KEY, latestSeq.toString());
999
+ }
1000
+ lastAppliedSeq(fromNodeId) {
1001
+ return selectMaxAppliedSourceSeq(this.acquireWriter(), fromNodeId);
1002
+ }
1003
+ };
1004
+
391
1005
  // src/core/group-committer.ts
392
1006
  var DEFAULT_MAX_GROUP_SIZE = 1e3;
393
1007
  var DML_PREFIX_RE = /^\s*(?:INSERT|UPDATE|DELETE|REPLACE)\b/i;
@@ -472,7 +1086,7 @@ var GroupCommitter = class {
472
1086
  }
473
1087
  async flush(batch) {
474
1088
  const lone = batch[0];
475
- if (batch.length === 1 && lone.statements.length === 1) {
1089
+ if (batch.length === 1 && lone.statements.length === 1 && !this.needsStamp(lone.statements[0].sql)) {
476
1090
  try {
477
1091
  lone.resolve([await this.writerLock.run(() => this.runSingle(lone.statements[0]))]);
478
1092
  } catch (err) {
@@ -489,21 +1103,52 @@ var GroupCommitter = class {
489
1103
  }
490
1104
  for (let i = 0; i < batch.length; i++) {
491
1105
  const outcome = outcomes[i];
492
- if (outcome.ok) batch[i].resolve(outcome.values);
1106
+ if (outcome.ok) batch[i].resolve(outcome.values.slice(0, batch[i].statements.length));
493
1107
  else batch[i].reject(outcome.error);
494
1108
  }
495
1109
  }
1110
+ needsStamp(sql) {
1111
+ if (!isGroupable(sql)) return false;
1112
+ const stamps = this.hooks.stampStatements();
1113
+ return stamps !== null && stamps.length > 0;
1114
+ }
496
1115
  async runSingle(statement) {
497
1116
  const writer = this.hooks.acquireWriter();
498
- const result = await execute(writer, statement.sql, statement.params);
1117
+ const stamps = isGroupable(statement.sql) ? this.hooks.stampStatements() : null;
1118
+ const result = stamps === null || stamps.length === 0 ? await execute(writer, statement.sql, statement.params) : await this.runStampedAlone(writer, statement, stamps);
499
1119
  await this.hooks.afterCommit(writer, statement.sql);
500
1120
  return result;
501
1121
  }
1122
+ async runStampedAlone(writer, statement, stamps) {
1123
+ await writer.exec("SAVEPOINT sirannon_stamp");
1124
+ try {
1125
+ const result = await execute(writer, statement.sql, statement.params);
1126
+ for (const stamp of stamps) {
1127
+ await execute(writer, stamp.sql, stamp.params, stamp.trusted === true);
1128
+ }
1129
+ await writer.exec("RELEASE sirannon_stamp");
1130
+ return result;
1131
+ } catch (err) {
1132
+ try {
1133
+ await writer.exec("ROLLBACK TO sirannon_stamp");
1134
+ await writer.exec("RELEASE sirannon_stamp");
1135
+ } catch {
1136
+ }
1137
+ throw err;
1138
+ }
1139
+ }
502
1140
  async runGroup(batch) {
503
1141
  const writer = this.hooks.acquireWriter();
504
- const units = batch.map((job) => ({
505
- statements: job.statements.map((statement) => ({ sql: statement.sql, params: bindParams(statement.params) }))
506
- }));
1142
+ const units = batch.map((job, index) => {
1143
+ const stamps = this.hooks.stampStatements({ persistClock: index === batch.length - 1 }) ?? [];
1144
+ return {
1145
+ statements: [...job.statements, ...stamps].map((statement) => ({
1146
+ sql: statement.sql,
1147
+ params: bindParams(statement.params),
1148
+ ...statement.trusted === true ? { trusted: true } : {}
1149
+ }))
1150
+ };
1151
+ });
507
1152
  if (writer.runGroup) {
508
1153
  const raw = await writer.runGroup(units);
509
1154
  return raw.map(
@@ -589,38 +1234,1331 @@ var HookRegistry = class {
589
1234
  this.hooks.set(event, [hook]);
590
1235
  }
591
1236
  }
592
- loadConfig(config) {
593
- for (const [configKey, event] of Object.entries(HOOK_CONFIG_MAP)) {
594
- const value = config[configKey];
595
- if (!value) continue;
596
- const hooks = Array.isArray(value) ? value : [value];
597
- for (const hook of hooks) {
598
- this.addHook(event, hook);
599
- }
1237
+ loadConfig(config) {
1238
+ for (const [configKey, event] of Object.entries(HOOK_CONFIG_MAP)) {
1239
+ const value = config[configKey];
1240
+ if (!value) continue;
1241
+ const hooks = Array.isArray(value) ? value : [value];
1242
+ for (const hook of hooks) {
1243
+ this.addHook(event, hook);
1244
+ }
1245
+ }
1246
+ }
1247
+ };
1248
+
1249
+ // src/core/worker/config.ts
1250
+ var DEFAULT_MAX_PENDING_WRITES = 1024;
1251
+ var DEFAULT_RETRY_AFTER_MS = 1e3;
1252
+ function requireCount(value, name, min) {
1253
+ if (!Number.isInteger(value) || value < min) {
1254
+ throw new SirannonError(`writerWorker.${name} must be an integer >= ${min}`, "INVALID_WRITER_WORKER");
1255
+ }
1256
+ return value;
1257
+ }
1258
+ function resolveWriterWorkerConfig(option) {
1259
+ if (!option) {
1260
+ return { enabled: false, maxPendingWrites: 0, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host: {} };
1261
+ }
1262
+ const opts = option === true ? {} : option;
1263
+ const host = {};
1264
+ if (opts.writeTimeoutMs !== void 0) host.writeTimeoutMs = requireCount(opts.writeTimeoutMs, "writeTimeoutMs", 0);
1265
+ if (opts.maxRestarts !== void 0) host.maxRestarts = requireCount(opts.maxRestarts, "maxRestarts", 0);
1266
+ const maxPendingWrites = opts.maxPendingWrites === void 0 ? DEFAULT_MAX_PENDING_WRITES : requireCount(opts.maxPendingWrites, "maxPendingWrites", 1);
1267
+ return { enabled: true, maxPendingWrites, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host };
1268
+ }
1269
+
1270
+ // src/core/worker/gate.ts
1271
+ var WriteGate = class {
1272
+ constructor(limit, retryAfterMs) {
1273
+ this.limit = limit;
1274
+ this.retryAfterMs = retryAfterMs;
1275
+ }
1276
+ inFlight = 0;
1277
+ run(op) {
1278
+ if (this.limit > 0 && this.inFlight >= this.limit) {
1279
+ return Promise.reject(new WriteOverloadError(this.limit, this.retryAfterMs));
1280
+ }
1281
+ this.inFlight++;
1282
+ return op().finally(() => {
1283
+ this.inFlight--;
1284
+ });
1285
+ }
1286
+ };
1287
+
1288
+ // src/core/writer-lock.ts
1289
+ var UNTRACKED = {
1290
+ run: (operation) => operation(),
1291
+ isActive: () => false,
1292
+ exit: (operation) => operation()
1293
+ };
1294
+ var WriterLock = class {
1295
+ tail = Promise.resolve();
1296
+ context;
1297
+ constructor(context) {
1298
+ this.context = context ?? UNTRACKED;
1299
+ }
1300
+ run(operation) {
1301
+ if (this.context.isActive()) {
1302
+ return operation();
1303
+ }
1304
+ const enter = () => this.context.run(operation);
1305
+ const ticket = this.tail.then(enter, enter);
1306
+ this.tail = ticket.then(swallow, swallow);
1307
+ return ticket;
1308
+ }
1309
+ isHeld() {
1310
+ return this.context.isActive();
1311
+ }
1312
+ async settle() {
1313
+ await this.run(() => Promise.resolve());
1314
+ }
1315
+ detached(operation) {
1316
+ return this.context.exit(operation);
1317
+ }
1318
+ };
1319
+ function swallow() {
1320
+ }
1321
+
1322
+ // src/core/database-create.ts
1323
+ async function createDatabaseRuntime(id, path, driver, options, internals) {
1324
+ const writerWorker = resolveWriterWorkerConfig(options?.writerWorker);
1325
+ const readOnly = options?.readOnly ?? false;
1326
+ if (writerWorker.enabled && !readOnly && !driver.startWriterHost) {
1327
+ throw new SirannonError(
1328
+ `writerWorker is enabled for database '${id}' but the driver does not carry a worker entry; use a driver that supports worker offload or disable writerWorker`,
1329
+ "WRITER_WORKER_UNSUPPORTED"
1330
+ );
1331
+ }
1332
+ const pool = await ConnectionPool.create({
1333
+ driver,
1334
+ path,
1335
+ readOnly: options?.readOnly,
1336
+ readPoolSize: options?.readPoolSize ?? 4,
1337
+ walMode: options?.walMode ?? true,
1338
+ synchronous: options?.synchronous,
1339
+ useWriterWorker: writerWorker.enabled && !readOnly,
1340
+ workerHostOptions: writerWorker.host
1341
+ });
1342
+ const writeGate = new WriteGate(writerWorker.enabled ? writerWorker.maxPendingWrites : 0, writerWorker.retryAfterMs);
1343
+ const writerLock = new WriterLock(driver.createWriterContext?.());
1344
+ const hookRegistry = new HookRegistry();
1345
+ const observer = new DatabaseObserver(id, hookRegistry, internals?.parentHooks ?? null, internals?.metrics ?? null);
1346
+ const backups = new DatabaseBackupController(
1347
+ (op) => writerLock.run(op),
1348
+ () => pool.acquireWriter(),
1349
+ driver.createBackupEngine?.()
1350
+ );
1351
+ const canOpenSnapshotConnection = driver.capabilities.multipleConnections && path !== ":memory:";
1352
+ const cdc = new DatabaseCdcController(
1353
+ (op) => writerLock.run(op),
1354
+ () => pool.acquireWriter(),
1355
+ options?.cdcPollInterval ?? 50,
1356
+ options?.cdcRetention ?? 36e5,
1357
+ canOpenSnapshotConnection ? () => driver.open(path, { readonly: true, walMode: false }) : null
1358
+ );
1359
+ const sync = new DatabaseSyncController(
1360
+ (op) => writeGate.run(() => writerLock.run(op)),
1361
+ () => pool.acquireWriter(),
1362
+ cdc
1363
+ );
1364
+ if (!readOnly && await snapshotLoadPending(pool.acquireWriter())) {
1365
+ sync.seedSnapshotGate();
1366
+ }
1367
+ const groupCommitter = new GroupCommitter(writerLock, {
1368
+ acquireWriter: () => pool.acquireWriter(),
1369
+ afterCommit: (writer, sql) => applyDdlSideEffectsIfRelevant(cdc.changeTracker, writer, sql),
1370
+ stampStatements: (stampOptions) => cdc.stampStatements(stampOptions)
1371
+ });
1372
+ return { pool, writeGate, writerLock, hookRegistry, observer, backups, cdc, sync, groupCommitter };
1373
+ }
1374
+
1375
+ // src/core/database-reads.ts
1376
+ function readRows(deps, sql, params, options) {
1377
+ return deps.observer.withQueryHooks(
1378
+ sql,
1379
+ params,
1380
+ options,
1381
+ () => onReadConnection(deps, sql, (conn) => query(conn, sql, params))
1382
+ );
1383
+ }
1384
+ function readWireRows(deps, sql, params, options) {
1385
+ return deps.observer.withQueryHooks(
1386
+ sql,
1387
+ params,
1388
+ options,
1389
+ () => onReadConnection(deps, sql, (conn) => queryForWire(conn, sql, params))
1390
+ );
1391
+ }
1392
+ function readOneRow(deps, sql, params, options) {
1393
+ return deps.observer.withQueryHooks(
1394
+ sql,
1395
+ params,
1396
+ options,
1397
+ () => onReadConnection(deps, sql, (conn) => queryOne(conn, sql, params))
1398
+ );
1399
+ }
1400
+ function onReadConnection(deps, sql, op) {
1401
+ if (deps.pool.readerCount === 0) {
1402
+ return deps.writerLock.run(() => deps.observer.track(sql, () => op(deps.pool.acquireWriter())));
1403
+ }
1404
+ return deps.observer.track(sql, () => op(deps.pool.acquireReader()));
1405
+ }
1406
+
1407
+ // src/core/extension-loader.ts
1408
+ async function loadExtension(driver, writer, extensionPath) {
1409
+ if (!driver.capabilities.extensions || !driver.resolveExtensionPath) {
1410
+ throw new ExtensionError(extensionPath, "Extensions are not supported by the current driver");
1411
+ }
1412
+ if (!extensionPath || extensionPath.includes("\0")) {
1413
+ throw new ExtensionError(extensionPath || "", "Extension path is empty or contains null bytes");
1414
+ }
1415
+ for (let i = 0; i < extensionPath.length; i++) {
1416
+ if (extensionPath.charCodeAt(i) <= 31) {
1417
+ throw new ExtensionError(extensionPath, "Extension path contains control characters");
1418
+ }
1419
+ }
1420
+ const segments = extensionPath.split(/[/\\]/);
1421
+ if (segments.includes("..")) {
1422
+ throw new ExtensionError(extensionPath, "Extension path must not contain directory traversal segments");
1423
+ }
1424
+ const resolved = driver.resolveExtensionPath(extensionPath);
1425
+ try {
1426
+ const escaped = resolved.replace(/'/g, "''");
1427
+ await writer.exec(`SELECT load_extension('${escaped}')`);
1428
+ } catch (err) {
1429
+ throw new ExtensionError(extensionPath, err instanceof Error ? err.message : String(err));
1430
+ }
1431
+ }
1432
+
1433
+ // src/core/live/column-collations.ts
1434
+ var TABLE_CONSTRAINT_STARTS = /* @__PURE__ */ new Set(["constraint", "primary", "unique", "check", "foreign"]);
1435
+ function parseColumnCollations(ddl) {
1436
+ const collations = /* @__PURE__ */ new Map();
1437
+ const tokens = tokenizeSql(ddl);
1438
+ const open = tokens.findIndex((token) => token.kind === "punct" && token.value === "(" && token.depth === 0);
1439
+ if (open === -1) return collations;
1440
+ let definition = [];
1441
+ for (let i = open + 1; i < tokens.length; i++) {
1442
+ const token = tokens[i];
1443
+ if (token.depth === 0) break;
1444
+ if (token.depth === 1 && token.kind === "punct" && token.value === ",") {
1445
+ readDefinition(definition, collations);
1446
+ definition = [];
1447
+ continue;
1448
+ }
1449
+ definition.push(token);
1450
+ }
1451
+ readDefinition(definition, collations);
1452
+ return collations;
1453
+ }
1454
+ function readDefinition(definition, collations) {
1455
+ const name = definition[0];
1456
+ if (name === void 0 || name.kind !== "word") return;
1457
+ if (!name.quoted && TABLE_CONSTRAINT_STARTS.has(name.lower)) return;
1458
+ for (let i = 1; i < definition.length - 1; i++) {
1459
+ const token = definition[i];
1460
+ if (token.kind === "word" && !token.quoted && token.lower === "collate") {
1461
+ collations.set(name.value, definition[i + 1].value);
1462
+ return;
1463
+ }
1464
+ }
1465
+ }
1466
+
1467
+ // src/core/live/sqlite-order.ts
1468
+ var TYPE_NULL = 0;
1469
+ var TYPE_NUMBER = 1;
1470
+ var TYPE_TEXT = 2;
1471
+ var TYPE_BLOB = 3;
1472
+ var encoder = new TextEncoder();
1473
+ function resolveCollation(name) {
1474
+ if (name === null) return "binary";
1475
+ const lower = name.toLowerCase();
1476
+ if (lower === "binary" || lower === "nocase" || lower === "rtrim") return lower;
1477
+ throw new CDCError(
1478
+ `Cannot keep a live query ordered by the '${name}' collation: order is maintained for BINARY, NOCASE, and RTRIM`
1479
+ );
1480
+ }
1481
+ function toSortValue(value, collation) {
1482
+ if (value === null || value === void 0) return { type: TYPE_NULL };
1483
+ if (typeof value === "number" || typeof value === "bigint") return { type: TYPE_NUMBER, numeric: value };
1484
+ if (typeof value === "boolean") return { type: TYPE_NUMBER, numeric: value ? 1 : 0 };
1485
+ if (typeof value === "string") return { type: TYPE_TEXT, bytes: encoder.encode(applyCollation(value, collation)) };
1486
+ if (value instanceof Uint8Array) return { type: TYPE_BLOB, bytes: value };
1487
+ if (ArrayBuffer.isView(value)) {
1488
+ const view = value;
1489
+ return { type: TYPE_BLOB, bytes: new Uint8Array(view.buffer, view.byteOffset, view.byteLength) };
1490
+ }
1491
+ return { type: TYPE_TEXT, bytes: encoder.encode(applyCollation(String(value), collation)) };
1492
+ }
1493
+ function applyCollation(text, collation) {
1494
+ if (collation === "nocase") return upperAscii(text);
1495
+ if (collation === "rtrim") return trimTrailingSpaces(text);
1496
+ return text;
1497
+ }
1498
+ function upperAscii(text) {
1499
+ let result = "";
1500
+ for (let i = 0; i < text.length; i++) {
1501
+ const code = text.charCodeAt(i);
1502
+ result += code >= 97 && code <= 122 ? String.fromCharCode(code - 32) : text[i];
1503
+ }
1504
+ return result;
1505
+ }
1506
+ function trimTrailingSpaces(text) {
1507
+ let end = text.length;
1508
+ while (end > 0 && text.charCodeAt(end - 1) === 32) end--;
1509
+ return text.slice(0, end);
1510
+ }
1511
+ function compareSortValues(left, right) {
1512
+ if (left.type !== right.type) return left.type < right.type ? -1 : 1;
1513
+ if (left.type === TYPE_NULL) return 0;
1514
+ if (left.type === TYPE_NUMBER) return compareNumeric(left.numeric, right.numeric);
1515
+ return compareBytes(left.bytes, right.bytes);
1516
+ }
1517
+ function compareNumeric(left, right) {
1518
+ if (typeof left === "number" && typeof right === "number") {
1519
+ return left < right ? -1 : left > right ? 1 : 0;
1520
+ }
1521
+ if (typeof left === "bigint" && typeof right === "bigint") {
1522
+ return left < right ? -1 : left > right ? 1 : 0;
1523
+ }
1524
+ const big = typeof left === "bigint" ? left : right;
1525
+ const real = typeof left === "bigint" ? right : left;
1526
+ const sign = typeof left === "bigint" ? 1 : -1;
1527
+ if (!Number.isFinite(real)) return real > 0 ? -sign : sign;
1528
+ const whole = Math.floor(real);
1529
+ const wholeAsBig = BigInt(whole);
1530
+ if (big !== wholeAsBig) return big < wholeAsBig ? -sign : sign;
1531
+ return real > whole ? -sign : 0;
1532
+ }
1533
+ function compareBytes(left, right) {
1534
+ const shared = Math.min(left.length, right.length);
1535
+ for (let i = 0; i < shared; i++) {
1536
+ if (left[i] !== right[i]) return left[i] < right[i] ? -1 : 1;
1537
+ }
1538
+ return left.length === right.length ? 0 : left.length < right.length ? -1 : 1;
1539
+ }
1540
+ function buildSortKeyPlan(terms) {
1541
+ const collations = terms.map((term) => resolveCollation(term.collation));
1542
+ const descending = terms.map((term) => term.direction === "desc");
1543
+ const nullsLast = terms.map((term) => term.nulls === "last");
1544
+ return {
1545
+ collations,
1546
+ compare(left, right) {
1547
+ for (let i = 0; i < terms.length; i++) {
1548
+ const a = left[i];
1549
+ const b = right[i];
1550
+ const aNull = a.type === TYPE_NULL;
1551
+ const bNull = b.type === TYPE_NULL;
1552
+ if (aNull !== bNull) return (aNull ? 1 : -1) * (nullsLast[i] ? 1 : -1);
1553
+ if (aNull) continue;
1554
+ const order = compareSortValues(a, b);
1555
+ if (order !== 0) return descending[i] ? -order : order;
1556
+ }
1557
+ return 0;
1558
+ }
1559
+ };
1560
+ }
1561
+ function lowerBoundIndex(rows, key, plan) {
1562
+ let low = 0;
1563
+ let high = rows.length;
1564
+ while (low < high) {
1565
+ const middle = low + high >>> 1;
1566
+ if (plan.compare(rows[middle].sort, key) < 0) low = middle + 1;
1567
+ else high = middle;
1568
+ }
1569
+ return low;
1570
+ }
1571
+ function placementIndex(rows, key, plan) {
1572
+ let low = 0;
1573
+ let high = rows.length;
1574
+ while (low < high) {
1575
+ const middle = low + high >>> 1;
1576
+ if (plan.compare(rows[middle].sort, key) <= 0) low = middle + 1;
1577
+ else high = middle;
1578
+ }
1579
+ return low;
1580
+ }
1581
+
1582
+ // src/core/live/live-result.ts
1583
+ var LiveResult = class {
1584
+ constructor(plan) {
1585
+ this.plan = plan;
1586
+ }
1587
+ rows = [];
1588
+ index = /* @__PURE__ */ new Map();
1589
+ windowTruncated = false;
1590
+ get size() {
1591
+ return this.rows.length;
1592
+ }
1593
+ snapshot() {
1594
+ return this.rows.map((held) => held.row);
1595
+ }
1596
+ reset(rows) {
1597
+ this.rows = rows;
1598
+ this.index = new Map(rows.map((held) => [held.key, held]));
1599
+ this.windowTruncated = this.plan.limit !== null && rows.length >= this.plan.limit;
1600
+ }
1601
+ apply(changes) {
1602
+ const draft = new Draft(this.plan, this.rows.slice(), new Map(this.index), this.windowTruncated);
1603
+ for (const change of changes) {
1604
+ if (!draft.apply(change)) return null;
1605
+ }
1606
+ if (!draft.windowIsWhole()) return null;
1607
+ this.rows = draft.rows;
1608
+ this.index = draft.index;
1609
+ this.windowTruncated = draft.truncated;
1610
+ return draft.ops;
1611
+ }
1612
+ };
1613
+ var Draft = class {
1614
+ constructor(plan, rows, index, truncated) {
1615
+ this.plan = plan;
1616
+ this.rows = rows;
1617
+ this.index = index;
1618
+ this.truncated = truncated;
1619
+ }
1620
+ ops = [];
1621
+ apply(change) {
1622
+ const held = this.index.get(change.key);
1623
+ if (change.after === null) {
1624
+ if (change.before === null) return true;
1625
+ return held === void 0 ? this.plan.offset === 0 : this.remove(held);
1626
+ }
1627
+ const entering = { key: change.key, row: change.after.row, sort: change.after.sort };
1628
+ if (held !== void 0) {
1629
+ if (this.plan.sortPlan.compare(held.sort, entering.sort) === 0) return this.replace(held, entering);
1630
+ if (!this.remove(held)) return false;
1631
+ } else if (change.before !== null && this.plan.offset > 0) {
1632
+ return false;
1633
+ }
1634
+ return this.insert(entering);
1635
+ }
1636
+ windowIsWhole() {
1637
+ if (this.plan.limit === null || !this.truncated) return true;
1638
+ return this.rows.length >= this.plan.limit;
1639
+ }
1640
+ replace(held, replacement) {
1641
+ const at = this.locate(held);
1642
+ if (at === -1) return false;
1643
+ this.rows[at] = replacement;
1644
+ this.index.set(replacement.key, replacement);
1645
+ this.ops.push({ op: "update", index: at, row: replacement.row });
1646
+ return true;
1647
+ }
1648
+ remove(held) {
1649
+ if (this.plan.offset > 0) return false;
1650
+ const at = this.locate(held);
1651
+ if (at === -1) return false;
1652
+ this.rows.splice(at, 1);
1653
+ this.index.delete(held.key);
1654
+ this.ops.push({ op: "delete", index: at });
1655
+ return true;
1656
+ }
1657
+ insert(held) {
1658
+ if (this.plan.offset > 0) {
1659
+ if (this.rows.length === 0) return false;
1660
+ if (this.plan.sortPlan.compare(held.sort, this.rows[0].sort) <= 0) return false;
1661
+ }
1662
+ const at = placementIndex(this.rows, held.sort, this.plan.sortPlan);
1663
+ if (this.plan.limit !== null && at >= this.plan.limit) {
1664
+ this.truncated = true;
1665
+ return true;
1666
+ }
1667
+ this.rows.splice(at, 0, held);
1668
+ this.index.set(held.key, held);
1669
+ this.ops.push({ op: "insert", index: at, row: held.row });
1670
+ if (this.plan.limit !== null && this.rows.length > this.plan.limit) {
1671
+ const dropped = this.rows.pop();
1672
+ if (dropped !== void 0) this.index.delete(dropped.key);
1673
+ this.ops.push({ op: "delete", index: this.rows.length });
1674
+ this.truncated = true;
1675
+ }
1676
+ return true;
1677
+ }
1678
+ locate(held) {
1679
+ let at = lowerBoundIndex(this.rows, held.sort, this.plan.sortPlan);
1680
+ while (at < this.rows.length && this.rows[at].key !== held.key) {
1681
+ if (this.plan.sortPlan.compare(this.rows[at].sort, held.sort) !== 0) return -1;
1682
+ at++;
1683
+ }
1684
+ return at < this.rows.length ? at : -1;
1685
+ }
1686
+ };
1687
+
1688
+ // src/core/live/row-keys.ts
1689
+ var HEX = "0123456789abcdef";
1690
+ function rowidKey(value) {
1691
+ return `rowid:${String(value)}`;
1692
+ }
1693
+ function encodeRowKey(values) {
1694
+ let key = "";
1695
+ for (const value of values) {
1696
+ const part = encodePart(value);
1697
+ key += `${part.length}:${part}`;
1698
+ }
1699
+ return key;
1700
+ }
1701
+ function encodePart(value) {
1702
+ if (value === null || value === void 0) return "z";
1703
+ if (typeof value === "bigint") return `n${value.toString()}`;
1704
+ if (typeof value === "number") return `n${Number.isInteger(value) ? value.toFixed(0) : value.toString()}`;
1705
+ if (typeof value === "boolean") return `n${value ? "1" : "0"}`;
1706
+ if (typeof value === "string") return `s${value}`;
1707
+ if (value instanceof Uint8Array) return `b${toHex2(value)}`;
1708
+ if (ArrayBuffer.isView(value)) {
1709
+ const view = value;
1710
+ return `b${toHex2(new Uint8Array(view.buffer, view.byteOffset, view.byteLength))}`;
1711
+ }
1712
+ return `s${String(value)}`;
1713
+ }
1714
+ function toHex2(bytes) {
1715
+ let hex = "";
1716
+ for (const byte of bytes) {
1717
+ hex += HEX[byte >> 4] + HEX[byte & 15];
1718
+ }
1719
+ return hex;
1720
+ }
1721
+
1722
+ // src/core/live/live-query.ts
1723
+ var MaintainedLiveQuery = class _MaintainedLiveQuery {
1724
+ constructor(source) {
1725
+ this.source = source;
1726
+ this.result = new LiveResult(source.plan);
1727
+ }
1728
+ state = { status: "pending" };
1729
+ listeners = /* @__PURE__ */ new Set();
1730
+ result;
1731
+ batches = [];
1732
+ pending = [];
1733
+ pendingBytes = 0;
1734
+ pendingOverflowed = false;
1735
+ stopDelivery = null;
1736
+ appliedThroughSeq = -1n;
1737
+ reading = false;
1738
+ readAgain = false;
1739
+ draining = false;
1740
+ closed = false;
1741
+ static async open(source) {
1742
+ const query2 = new _MaintainedLiveQuery(source);
1743
+ try {
1744
+ await query2.refresh(false);
1745
+ } catch (err) {
1746
+ await query2.close();
1747
+ throw err;
1748
+ }
1749
+ if (query2.state.status === "error") {
1750
+ const failure = query2.state.error;
1751
+ await query2.close();
1752
+ throw failure;
1753
+ }
1754
+ return query2;
1755
+ }
1756
+ getState() {
1757
+ return this.state;
1758
+ }
1759
+ subscribe(listener) {
1760
+ this.listeners.add(listener);
1761
+ return () => {
1762
+ this.listeners.delete(listener);
1763
+ };
1764
+ }
1765
+ async close() {
1766
+ if (this.closed) return;
1767
+ this.closed = true;
1768
+ this.stopDelivery?.();
1769
+ this.stopDelivery = null;
1770
+ this.listeners.clear();
1771
+ this.batches.length = 0;
1772
+ this.pending = [];
1773
+ await this.source.release();
1774
+ }
1775
+ receive(event) {
1776
+ if (this.closed) return;
1777
+ if (event.seq <= this.appliedThroughSeq) return;
1778
+ if (!this.pendingOverflowed) {
1779
+ this.pending.push(event);
1780
+ this.pendingBytes += approximateBytes(event);
1781
+ if (this.pending.length > this.source.maxTransactionChanges || this.pendingBytes > this.source.maxTransactionBytes) {
1782
+ this.pendingOverflowed = true;
1783
+ this.pending = [];
1784
+ }
1785
+ }
1786
+ if (event.txEnd !== true) return;
1787
+ const overflowed = this.pendingOverflowed;
1788
+ const batch = this.pending;
1789
+ this.pending = [];
1790
+ this.pendingBytes = 0;
1791
+ this.pendingOverflowed = false;
1792
+ this.batches.push(overflowed ? [] : batch);
1793
+ void this.drain();
1794
+ }
1795
+ deliveryLost() {
1796
+ if (this.closed) return;
1797
+ this.stopDelivery = null;
1798
+ this.batches.length = 0;
1799
+ this.pending = [];
1800
+ this.pendingBytes = 0;
1801
+ this.pendingOverflowed = false;
1802
+ void this.reread();
1803
+ }
1804
+ async drain() {
1805
+ if (this.draining || this.reading || this.closed) return;
1806
+ this.draining = true;
1807
+ try {
1808
+ while (this.batches.length > 0 && !this.closed && !this.reading) {
1809
+ const batch = this.batches.shift();
1810
+ if (batch === void 0) break;
1811
+ await this.applyBatch(batch);
1812
+ }
1813
+ } finally {
1814
+ this.draining = false;
1815
+ }
1816
+ }
1817
+ async applyBatch(batch) {
1818
+ if (batch.length === 0) {
1819
+ await this.reread();
1820
+ return;
1821
+ }
1822
+ const fresh = batch.filter((event) => event.seq > this.appliedThroughSeq);
1823
+ if (fresh.length === 0) return;
1824
+ let changes;
1825
+ try {
1826
+ changes = await this.evaluate(fresh);
1827
+ } catch (err) {
1828
+ this.fail(err);
1829
+ return;
1830
+ }
1831
+ if (this.closed) return;
1832
+ const highest = fresh[fresh.length - 1].seq;
1833
+ if (changes.length === 0) {
1834
+ this.appliedThroughSeq = highest;
1835
+ return;
1836
+ }
1837
+ const ops = changes.length > this.result.size ? null : this.result.apply(changes);
1838
+ if (ops === null) {
1839
+ await this.reread();
1840
+ return;
1841
+ }
1842
+ this.appliedThroughSeq = highest;
1843
+ this.publish({ status: "ready", rows: this.result.snapshot(), revalidating: false }, { kind: "ops", ops });
1844
+ }
1845
+ async evaluate(events) {
1846
+ const candidates = [];
1847
+ for (let i = 0; i < events.length; i++) {
1848
+ const event = events[i];
1849
+ if (event.type !== "insert" && event.oldRow !== void 0) {
1850
+ candidates.push({ slot: i * 2, payload: event.oldRow });
1851
+ }
1852
+ if (event.type !== "delete") {
1853
+ candidates.push({ slot: i * 2 + 1, payload: event.row });
1854
+ }
1855
+ }
1856
+ const matches = await this.source.probe.evaluate(candidates);
1857
+ const changes = [];
1858
+ for (let i = 0; i < events.length; i++) {
1859
+ const event = events[i];
1860
+ const before = matches.get(i * 2) ?? null;
1861
+ const after = matches.get(i * 2 + 1) ?? null;
1862
+ if (before === null && after === null) continue;
1863
+ const beforeKey = event.oldRow === void 0 ? null : this.keyOf(event, event.oldRow);
1864
+ const afterKey = event.type === "delete" ? null : this.keyOf(event, event.row);
1865
+ if (before !== null && after !== null && beforeKey !== null && afterKey !== null && beforeKey !== afterKey) {
1866
+ changes.push({ key: beforeKey, before, after: null });
1867
+ changes.push({ key: afterKey, before: null, after });
1868
+ continue;
1869
+ }
1870
+ const key = after !== null ? afterKey ?? beforeKey : beforeKey;
1871
+ if (key === null) continue;
1872
+ changes.push({ key, before, after });
1873
+ }
1874
+ return changes;
1875
+ }
1876
+ keyOf(event, payload) {
1877
+ if (this.source.plan.usesRowid) return event.rowId === void 0 ? null : rowidKey(event.rowId);
1878
+ return encodeRowKey(this.source.plan.keyColumns.map((column) => payload[column]));
1879
+ }
1880
+ async reread() {
1881
+ if (this.reading) {
1882
+ this.readAgain = true;
1883
+ return;
1884
+ }
1885
+ if (this.state.status === "ready" && !this.state.revalidating) {
1886
+ this.publish({ status: "ready", rows: this.state.rows, revalidating: true }, { kind: "revalidating" });
1887
+ }
1888
+ await this.refresh(true);
1889
+ }
1890
+ async refresh(spread) {
1891
+ this.reading = true;
1892
+ try {
1893
+ do {
1894
+ this.readAgain = false;
1895
+ if (spread && this.source.rereadJitterMs > 0) {
1896
+ await this.source.wait(Math.floor(Math.random() * this.source.rereadJitterMs));
1897
+ }
1898
+ if (this.closed) return;
1899
+ const refreshed = await this.source.read();
1900
+ if (this.closed) return;
1901
+ this.result.reset(refreshed.rows);
1902
+ this.appliedThroughSeq = refreshed.seq;
1903
+ if (this.stopDelivery === null) {
1904
+ const stop = await this.source.start(
1905
+ { onEvent: (event) => this.receive(event), onLost: () => this.deliveryLost() },
1906
+ refreshed.seq
1907
+ );
1908
+ if (this.closed) {
1909
+ stop();
1910
+ return;
1911
+ }
1912
+ this.stopDelivery = stop;
1913
+ }
1914
+ } while (this.readAgain && !this.closed);
1915
+ this.publish({ status: "ready", rows: this.result.snapshot(), revalidating: false }, { kind: "rows" });
1916
+ } catch (err) {
1917
+ this.fail(err);
1918
+ } finally {
1919
+ this.reading = false;
1920
+ }
1921
+ if (!this.closed && this.batches.length > 0) void this.drain();
1922
+ }
1923
+ fail(err) {
1924
+ if (this.closed) return;
1925
+ this.publish({ status: "error", error: err instanceof Error ? err : new Error(String(err)) }, { kind: "error" });
1926
+ }
1927
+ publish(next, update) {
1928
+ this.state = next;
1929
+ for (const listener of [...this.listeners]) {
1930
+ try {
1931
+ listener(update);
1932
+ } catch {
1933
+ }
1934
+ }
1935
+ }
1936
+ };
1937
+ function approximateBytes(event) {
1938
+ return approximateRowBytes(event.row) + (event.oldRow === void 0 ? 0 : approximateRowBytes(event.oldRow));
1939
+ }
1940
+ function approximateRowBytes(row) {
1941
+ let bytes = 0;
1942
+ for (const key of Object.keys(row)) {
1943
+ const value = row[key];
1944
+ bytes += key.length + 8;
1945
+ if (typeof value === "string") bytes += value.length;
1946
+ else if (value instanceof Uint8Array) bytes += value.byteLength;
1947
+ }
1948
+ return bytes;
1949
+ }
1950
+
1951
+ // src/core/live/primed-delivery.ts
1952
+ var PrimedLiveDelivery = class {
1953
+ constructor(cdc, conn, run, table) {
1954
+ this.cdc = cdc;
1955
+ this.conn = conn;
1956
+ this.run = run;
1957
+ this.table = table;
1958
+ }
1959
+ async start(handlers, sinceSeq) {
1960
+ const tracker = this.cdc.changeTracker;
1961
+ const manager = this.cdc.subscriptions;
1962
+ if (tracker === null || manager === null) {
1963
+ throw new CDCError(`Cannot deliver changes for '${this.table}': the table is not watched`);
1964
+ }
1965
+ const grouper = new TransactionGrouper((event) => {
1966
+ handlers.onEvent(event);
1967
+ return true;
1968
+ });
1969
+ let stopped = false;
1970
+ let removeBatchEnd = () => {
1971
+ };
1972
+ let subscription = null;
1973
+ const stop = () => {
1974
+ if (stopped) return;
1975
+ stopped = true;
1976
+ removeBatchEnd();
1977
+ subscription?.unsubscribe();
1978
+ };
1979
+ const primed = new PrimedSubscription(
1980
+ sinceSeq,
1981
+ (event) => {
1982
+ grouper.receive(event);
1983
+ return true;
1984
+ },
1985
+ () => {
1986
+ stop();
1987
+ handlers.onLost();
1988
+ }
1989
+ );
1990
+ const boundary = tracker.cursor;
1991
+ const boundaryEndsTransaction = tracker.pollEndedAtTxBoundary;
1992
+ subscription = manager.subscribe(this.table, void 0, (event) => primed.onLiveEvent(event));
1993
+ try {
1994
+ await this.run(() => primed.replayTables(tracker, this.conn, [this.table], void 0, boundary));
1995
+ } catch (err) {
1996
+ stop();
1997
+ throw err;
1998
+ }
1999
+ if (stopped) return stop;
2000
+ grouper.flush(boundaryEndsTransaction);
2001
+ primed.goLive();
2002
+ removeBatchEnd = manager.addBatchEndListener((atTxBoundary) => {
2003
+ grouper.flush(atTxBoundary);
2004
+ });
2005
+ return stop;
2006
+ }
2007
+ };
2008
+
2009
+ // src/core/system-catalog/live-probe-table.ts
2010
+ var SAFE_DECLARED_TYPE = /^[A-Za-z0-9_ ()',.+-]*$/;
2011
+ async function ensureLiveProbeTable(conn, probeTable, columns) {
2012
+ assertSafeIdentifier(probeTable);
2013
+ if (columns.length === 0) {
2014
+ throw new SirannonError(`Cannot stage rows for '${probeTable}': the table declares no columns`, "CDC_ERROR");
2015
+ }
2016
+ const definitions = columns.map((column) => {
2017
+ assertSafeIdentifier(column.name);
2018
+ if (!SAFE_DECLARED_TYPE.test(column.type)) {
2019
+ throw new SirannonError(`Unsafe column type for internal schema: ${column.type}`, "INTERNAL_SCHEMA_ERROR");
2020
+ }
2021
+ if (column.collation !== null) assertSafeIdentifier(column.collation);
2022
+ const type = column.type.trim().length === 0 ? "" : ` ${column.type.trim()}`;
2023
+ const collate = column.collation === null ? "" : ` COLLATE "${column.collation}"`;
2024
+ return `"${column.name}"${type}${collate}`;
2025
+ });
2026
+ await conn.exec(`CREATE TEMP TABLE IF NOT EXISTS "${probeTable}" (${definitions.join(", ")})`);
2027
+ }
2028
+ async function dropLiveProbeTable(conn, probeTable) {
2029
+ assertSafeIdentifier(probeTable);
2030
+ await conn.exec(`DROP TABLE IF EXISTS temp."${probeTable}"`);
2031
+ }
2032
+ async function prepareInsertLiveProbeRow(conn, probeTable, columns) {
2033
+ assertSafeIdentifier(probeTable);
2034
+ const names = columns.map((column) => {
2035
+ assertSafeIdentifier(column.name);
2036
+ return `"${column.name}"`;
2037
+ });
2038
+ const placeholders = new Array(columns.length + 1).fill("?").join(", ");
2039
+ return conn.prepare(`INSERT INTO temp."${probeTable}" (rowid, ${names.join(", ")}) VALUES (${placeholders})`);
2040
+ }
2041
+ async function prepareDeleteLiveProbeRows(conn, probeTable) {
2042
+ assertSafeIdentifier(probeTable);
2043
+ return conn.prepare(`DELETE FROM temp."${probeTable}"`);
2044
+ }
2045
+ function selectLiveProbeMatchesSql(probeTable, keyColumn, projection, where, sourceName) {
2046
+ assertSafeIdentifier(probeTable);
2047
+ assertSafeIdentifier(keyColumn);
2048
+ const filter = where === null ? "" : `
2049
+ WHERE (${where}
2050
+ )`;
2051
+ return `SELECT rowid AS "${keyColumn}"
2052
+ , ${projection}
2053
+ FROM temp."${probeTable}" AS "${sourceName}"${filter}`;
2054
+ }
2055
+
2056
+ // src/core/live/query-plan.ts
2057
+ var KEY_COLUMN = "_sirannon_k";
2058
+ var SORT_COLUMN_PREFIX = "_sirannon_s";
2059
+ var IDENTIFIER_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
2060
+ function buildLivePlan(shape, table, params) {
2061
+ const sourceName = shape.alias ?? shape.table;
2062
+ assertIdentifier(shape, sourceName, "the table or its alias");
2063
+ for (const column of table.columns) {
2064
+ if (column.name.toLowerCase().startsWith(INTERNAL_TABLE_PREFIX)) {
2065
+ throw new CDCError(
2066
+ `Cannot open a live query for '${shape.sql}': column '${column.name}' uses the '${INTERNAL_TABLE_PREFIX}' prefix Sirannon reserves`
2067
+ );
2068
+ }
2069
+ }
2070
+ const usesRowid = table.pkColumns.length === 0;
2071
+ const keyColumns = usesRowid ? ["rowid"] : table.pkColumns;
2072
+ const keyColumnAliases = keyColumns.map((_, index) => `${KEY_COLUMN}${index}`);
2073
+ const sortColumns = shape.sortTerms.map((_, index) => `${SORT_COLUMN_PREFIX}${index}`);
2074
+ const sortExpressions = shape.sortTerms.map((term) => resolveSortExpression(shape, term));
2075
+ const collations = shape.sortTerms.map(
2076
+ (term, index) => effectiveCollation(term, sortExpressions[index], sourceName, table.collations)
2077
+ );
2078
+ const sortPlan = buildSortKeyPlan(shape.sortTerms.map((term, index) => ({ ...term, collation: collations[index] })));
2079
+ const projection = [
2080
+ shape.selectList,
2081
+ ...sortExpressions.map((expression, index) => `${expression} AS "${sortColumns[index]}"`)
2082
+ ].join("\n, ");
2083
+ const keyProjection = keyColumns.map((column, index) => `"${sourceName}"."${column}" AS "${keyColumnAliases[index]}"`).join(", ");
2084
+ const from = shape.alias === null ? `"${shape.table}"` : `"${shape.table}" AS "${shape.alias}"`;
2085
+ const where = shape.where === null ? "" : `
2086
+ WHERE (${shape.where}
2087
+ )`;
2088
+ const tail = shape.tail.length === 0 ? "" : `
2089
+ ${shape.tail}`;
2090
+ return {
2091
+ shape,
2092
+ sourceName,
2093
+ probeColumns: table.columns.map((column) => ({
2094
+ name: column.name,
2095
+ type: column.type,
2096
+ collation: table.collations.get(column.name) ?? null
2097
+ })),
2098
+ keyColumns,
2099
+ usesRowid,
2100
+ sortPlan,
2101
+ sortColumns,
2102
+ keyColumnAliases,
2103
+ readSql: `SELECT ${projection}
2104
+ , ${keyProjection}
2105
+ FROM ${from}${where}${tail}`,
2106
+ probeSql: (probeTable) => selectLiveProbeMatchesSql(probeTable, KEY_COLUMN, projection, shape.where, sourceName),
2107
+ probeParams: (bound) => sliceProbeParams(shape, bound),
2108
+ limit: shape.limit === null ? null : resolveBound(shape, shape.limit, params, "LIMIT"),
2109
+ offset: shape.offset === null ? 0 : resolveBound(shape, shape.offset, params, "OFFSET")
2110
+ };
2111
+ }
2112
+ function sliceProbeParams(shape, params) {
2113
+ if (params === void 0) return [];
2114
+ if (!Array.isArray(params)) return [params];
2115
+ return params.slice(0, shape.selectParameters + shape.whereParameters);
2116
+ }
2117
+ function probeNamedParams(probeSql, params) {
2118
+ if (params === void 0 || Array.isArray(params)) return params;
2119
+ const used = /* @__PURE__ */ new Set();
2120
+ for (const token of tokenizeSql(probeSql)) {
2121
+ const name = parameterName(token);
2122
+ if (name !== null) used.add(name);
2123
+ }
2124
+ const picked = {};
2125
+ for (const [key, value] of Object.entries(params)) {
2126
+ if (used.has(key)) picked[key] = value;
2127
+ }
2128
+ return picked;
2129
+ }
2130
+ function resolveSortExpression(shape, term) {
2131
+ const tokens = tokenizeSql(term.expression);
2132
+ if (tokens.length === 1 && tokens[0].kind === "number") {
2133
+ return resolveOrdinal(shape, Number(tokens[0].value));
2134
+ }
2135
+ if (tokens.length === 1 && tokens[0].kind === "word") {
2136
+ const item = shape.selectItems.find((candidate) => candidate.alias === tokens[0].value);
2137
+ if (item !== void 0) return item.expression;
2138
+ }
2139
+ return term.expression;
2140
+ }
2141
+ function resolveOrdinal(shape, ordinal) {
2142
+ if (shape.selectItems.some((item2) => item2.star)) {
2143
+ throw new CDCError(
2144
+ `Cannot open a live query for '${shape.sql}': ORDER BY ${ordinal} cannot be resolved through '*', so name the column instead`
2145
+ );
2146
+ }
2147
+ const item = shape.selectItems[ordinal - 1];
2148
+ if (item === void 0) {
2149
+ throw new CDCError(
2150
+ `Cannot open a live query for '${shape.sql}': ORDER BY ${ordinal} names no column of the select list`
2151
+ );
2152
+ }
2153
+ return item.expression;
2154
+ }
2155
+ function effectiveCollation(term, expression, sourceName, collations) {
2156
+ if (term.collation !== null) return term.collation;
2157
+ const column = bareColumnReference(expression, sourceName);
2158
+ return column === null ? null : collations.get(column) ?? null;
2159
+ }
2160
+ function bareColumnReference(expression, sourceName) {
2161
+ const tokens = tokenizeSql(expression);
2162
+ if (tokens.length === 1 && tokens[0].kind === "word") return tokens[0].value;
2163
+ if (tokens.length === 3 && tokens[0].kind === "word" && tokens[1].value === "." && tokens[2].kind === "word") {
2164
+ return tokens[0].value === sourceName ? tokens[2].value : null;
2165
+ }
2166
+ return null;
2167
+ }
2168
+ function resolveBound(shape, bound, params, label) {
2169
+ if (bound.kind === "literal") return bound.literal;
2170
+ const value = boundParameterValue(bound, params, shape);
2171
+ if (typeof value === "bigint") return assertWholeBound(shape, Number(value), label);
2172
+ if (typeof value === "number") return assertWholeBound(shape, value, label);
2173
+ throw new CDCError(`Cannot open a live query for '${shape.sql}': ${label} is bound to a value that is not a number`);
2174
+ }
2175
+ function boundParameterValue(bound, params, shape) {
2176
+ if (params === void 0) {
2177
+ throw new CDCError(`Cannot open a live query for '${shape.sql}': ${bound.parameter} has no bound value`);
2178
+ }
2179
+ if (!Array.isArray(params)) return params[bound.parameter.slice(1)];
2180
+ const numbered = bound.parameter.length > 1 ? Number(bound.parameter.slice(1)) : 0;
2181
+ const ordinal = numbered > 0 ? numbered : placeholderOrdinal(shape.sql, bound.position);
2182
+ return params[ordinal - 1];
2183
+ }
2184
+ function placeholderOrdinal(sql, position) {
2185
+ let ordinal = 0;
2186
+ for (const token of tokenizeSql(sql)) {
2187
+ if (token.kind !== "param") continue;
2188
+ ordinal++;
2189
+ if (token.start === position) return ordinal;
2190
+ }
2191
+ return ordinal;
2192
+ }
2193
+ function assertWholeBound(shape, value, label) {
2194
+ if (!Number.isInteger(value) || value < 0) {
2195
+ throw new CDCError(
2196
+ `Cannot open a live query for '${shape.sql}': ${label} is bound to ${value}, and it takes a non-negative whole number`
2197
+ );
2198
+ }
2199
+ return value;
2200
+ }
2201
+ function assertIdentifier(shape, name, label) {
2202
+ if (!IDENTIFIER_RE2.test(name)) {
2203
+ throw new CDCError(
2204
+ `Cannot open a live query for '${shape.sql}': ${label} must contain only letters, digits, and underscores, and start with a letter or underscore`
2205
+ );
2206
+ }
2207
+ }
2208
+
2209
+ // src/core/live/row-probe.ts
2210
+ var RowProbe = class _RowProbe {
2211
+ constructor(conn, probeTable, plan, run, insert, clear, match, matchParams) {
2212
+ this.conn = conn;
2213
+ this.probeTable = probeTable;
2214
+ this.plan = plan;
2215
+ this.run = run;
2216
+ this.insert = insert;
2217
+ this.clear = clear;
2218
+ this.match = match;
2219
+ this.matchParams = matchParams;
2220
+ }
2221
+ closed = false;
2222
+ static async open(conn, probeTable, plan, params, run) {
2223
+ const sql = plan.probeSql(probeTable);
2224
+ try {
2225
+ return await run(async () => {
2226
+ await ensureLiveProbeTable(conn, probeTable, plan.probeColumns);
2227
+ const insert = await prepareInsertLiveProbeRow(conn, probeTable, plan.probeColumns);
2228
+ const clear = await prepareDeleteLiveProbeRows(conn, probeTable);
2229
+ const match = await conn.prepare(sql);
2230
+ const named = probeNamedParams(sql, params);
2231
+ const matchParams = named === void 0 || Array.isArray(named) ? plan.probeParams(params) : [named];
2232
+ return new _RowProbe(conn, probeTable, plan, run, insert, clear, match, matchParams);
2233
+ });
2234
+ } catch (err) {
2235
+ await run(() => dropLiveProbeTable(conn, probeTable)).catch(() => {
2236
+ });
2237
+ throw err;
2238
+ }
2239
+ }
2240
+ async evaluate(candidates) {
2241
+ const matches = /* @__PURE__ */ new Map();
2242
+ if (candidates.length === 0 || this.closed) return matches;
2243
+ await this.run(async () => {
2244
+ try {
2245
+ for (const candidate of candidates) {
2246
+ const values = this.plan.probeColumns.map((column) => candidate.payload[column.name] ?? null);
2247
+ await this.insert.run(candidate.slot, ...values);
2248
+ }
2249
+ const rows = await this.match.all(...this.matchParams);
2250
+ for (const row of rows) {
2251
+ matches.set(Number(row[KEY_COLUMN]), this.take(row));
2252
+ }
2253
+ } finally {
2254
+ await this.clear.run();
2255
+ }
2256
+ });
2257
+ return matches;
2258
+ }
2259
+ take(row) {
2260
+ const sort = this.plan.sortColumns.map(
2261
+ (column, index) => toSortValue(row[column], this.plan.sortPlan.collations[index])
2262
+ );
2263
+ delete row[KEY_COLUMN];
2264
+ for (const column of this.plan.sortColumns) delete row[column];
2265
+ return { row, sort };
2266
+ }
2267
+ async close() {
2268
+ if (this.closed) return;
2269
+ this.closed = true;
2270
+ try {
2271
+ await this.run(() => dropLiveProbeTable(this.conn, this.probeTable));
2272
+ } catch {
2273
+ }
2274
+ }
2275
+ };
2276
+
2277
+ // src/core/live/statement-shape.ts
2278
+ var UNSUPPORTED_WORDS = /* @__PURE__ */ new Map([
2279
+ ["union", "a compound SELECT"],
2280
+ ["intersect", "a compound SELECT"],
2281
+ ["except", "a compound SELECT"],
2282
+ ["group", "GROUP BY"],
2283
+ ["having", "HAVING"],
2284
+ ["window", "a window clause"],
2285
+ ["join", "a join"],
2286
+ ["natural", "a join"],
2287
+ ["cross", "a join"],
2288
+ ["inner", "a join"],
2289
+ ["outer", "a join"],
2290
+ ["left", "a join"],
2291
+ ["right", "a join"],
2292
+ ["full", "a join"],
2293
+ ["with", "a common table expression"],
2294
+ ["values", "a VALUES clause"],
2295
+ ["distinct", "DISTINCT"]
2296
+ ]);
2297
+ var CLOCK_KEYWORDS = /* @__PURE__ */ new Set(["current_date", "current_time", "current_timestamp"]);
2298
+ var VOLATILE_FUNCTIONS = /* @__PURE__ */ new Set(["random", "randomblob", "changes", "last_insert_rowid", "total_changes"]);
2299
+ var CLOCK_FUNCTIONS = /* @__PURE__ */ new Set(["date", "time", "datetime", "julianday", "unixepoch", "strftime", "timediff"]);
2300
+ var CLOCK_MODIFIER = /\b(now|localtime)\b/i;
2301
+ var AGGREGATE_FUNCTIONS = /* @__PURE__ */ new Set([
2302
+ "avg",
2303
+ "count",
2304
+ "group_concat",
2305
+ "json_group_array",
2306
+ "json_group_object",
2307
+ "max",
2308
+ "min",
2309
+ "string_agg",
2310
+ "sum",
2311
+ "total"
2312
+ ]);
2313
+ function analyseStatement(sql) {
2314
+ assertSqlAllowed(sql);
2315
+ const tokens = tokenizeSql(sql);
2316
+ if (tokens.length === 0 || tokens[0].lower !== "select" || tokens[0].quoted) {
2317
+ throw unsupported(sql, "a live query reads with a single SELECT statement");
2318
+ }
2319
+ const body = withoutTrailingSemicolon(tokens);
2320
+ assertSupportedWords(sql, body);
2321
+ assertDeterministic(sql, body);
2322
+ const clauses = findClauses(sql, body);
2323
+ const source = readSource(sql, body, clauses);
2324
+ const sortTerms = readSortTerms(sql, body, clauses);
2325
+ const window = readWindow(sql, body, clauses);
2326
+ if (window.limit !== null && sortTerms.length === 0) {
2327
+ throw unsupported(sql, "a live query with LIMIT orders its rows, otherwise the window has no defined membership");
2328
+ }
2329
+ const usesPositionalParameters = body.some((token) => token.kind === "param" && token.value.startsWith("?"));
2330
+ assertSortTermsBindByName(sql, sortTerms, usesPositionalParameters);
2331
+ return {
2332
+ sql,
2333
+ table: source.table,
2334
+ alias: source.alias,
2335
+ selectList: sql.slice(clauses.selectStart, clauses.fromKeyword).trim(),
2336
+ selectItems: readSelectItems(sql, body, clauses),
2337
+ where: readWhere(sql, clauses),
2338
+ sortTerms,
2339
+ limit: window.limit,
2340
+ offset: window.offset,
2341
+ tail: sql.slice(clauses.tailStart, clauses.statementEnd).trim(),
2342
+ selectParameters: countParameters(body, clauses.selectStart, clauses.fromKeyword),
2343
+ whereParameters: clauses.whereStart === null || clauses.whereEnd === null ? 0 : countParameters(body, clauses.whereStart, clauses.whereEnd),
2344
+ usesPositionalParameters
2345
+ };
2346
+ }
2347
+ function countParameters(body, start, end) {
2348
+ return body.filter((token) => token.kind === "param" && token.start >= start && token.end <= end).length;
2349
+ }
2350
+ function readWhere(sql, clauses) {
2351
+ if (clauses.whereStart === null || clauses.whereEnd === null) return null;
2352
+ return sql.slice(clauses.whereStart, clauses.whereEnd).trim();
2353
+ }
2354
+ function assertSortTermsBindByName(sql, sortTerms, positional) {
2355
+ if (!positional) return;
2356
+ for (const term of sortTerms) {
2357
+ if (tokenizeSql(term.expression).some((token) => token.kind === "param")) {
2358
+ throw unsupported(
2359
+ sql,
2360
+ "a live query binds an ORDER BY parameter by name, because it evaluates the term a second time to place a changed row"
2361
+ );
2362
+ }
2363
+ }
2364
+ }
2365
+ function assertSupportedWords(sql, body) {
2366
+ for (let i = 0; i < body.length; i++) {
2367
+ const token = body[i];
2368
+ if (token.kind !== "word" || token.quoted) continue;
2369
+ if (i > 0 && token.lower === "select") {
2370
+ throw unsupported(sql, "a subquery in a live query is not maintainable");
2371
+ }
2372
+ if (token.lower === "over" && body[i + 1]?.value === "(") {
2373
+ throw unsupported(sql, "a window function in a live query is not maintainable");
2374
+ }
2375
+ if (AGGREGATE_FUNCTIONS.has(token.lower) && body[i + 1]?.value === "(") {
2376
+ throw unsupported(sql, `the aggregate ${token.value} in a live query is not maintainable`);
2377
+ }
2378
+ const reason = UNSUPPORTED_WORDS.get(token.lower);
2379
+ if (reason !== void 0 && token.depth === 0) {
2380
+ throw unsupported(sql, `${reason} in a live query is not maintainable`);
2381
+ }
2382
+ }
2383
+ if (body.some((token) => token.kind === "punct" && token.value === ";")) {
2384
+ throw unsupported(sql, "a live query reads with one statement");
2385
+ }
2386
+ }
2387
+ function assertDeterministic(sql, body) {
2388
+ for (let i = 0; i < body.length; i++) {
2389
+ const token = body[i];
2390
+ if (token.kind !== "word" || token.quoted) continue;
2391
+ if (CLOCK_KEYWORDS.has(token.lower)) {
2392
+ throw volatileRead(sql, token.value);
2393
+ }
2394
+ const open = body[i + 1];
2395
+ if (open === void 0 || open.kind !== "punct" || open.value !== "(") continue;
2396
+ if (VOLATILE_FUNCTIONS.has(token.lower)) {
2397
+ throw volatileRead(sql, `${token.value}()`);
2398
+ }
2399
+ if (CLOCK_FUNCTIONS.has(token.lower) && readsTheClock(token.lower, callArguments(body, i + 1))) {
2400
+ throw volatileRead(sql, `${token.value}()`);
600
2401
  }
601
2402
  }
602
- };
2403
+ }
2404
+ function callArguments(body, openIndex) {
2405
+ const outer = body[openIndex].depth;
2406
+ const args = [];
2407
+ let current = [];
2408
+ for (let i = openIndex + 1; i < body.length; i++) {
2409
+ const token = body[i];
2410
+ if (token.kind === "punct" && token.value === ")" && token.depth === outer) break;
2411
+ if (token.kind === "punct" && token.value === "," && token.depth === outer + 1) {
2412
+ args.push(current);
2413
+ current = [];
2414
+ continue;
2415
+ }
2416
+ current.push(token);
2417
+ }
2418
+ if (current.length > 0) args.push(current);
2419
+ return args;
2420
+ }
2421
+ function readsTheClock(name, args) {
2422
+ if (args.length === 0) return true;
2423
+ if (name === "strftime" && args.length === 1) return true;
2424
+ return args.some((argument) => argument.some((token) => token.kind === "string" && CLOCK_MODIFIER.test(token.value)));
2425
+ }
2426
+ function volatileRead(sql, reference) {
2427
+ return unsupported(
2428
+ sql,
2429
+ `${reference} returns a different value on each evaluation, and a live query only re-evaluates a row a change event touches, so its rows would go stale without any change to report`
2430
+ );
2431
+ }
2432
+ function withoutTrailingSemicolon(tokens) {
2433
+ const last = tokens[tokens.length - 1];
2434
+ if (last !== void 0 && last.kind === "punct" && last.value === ";") return tokens.slice(0, -1);
2435
+ return tokens;
2436
+ }
2437
+
2438
+ // src/core/live/database-live.ts
2439
+ var DEFAULT_REREAD_JITTER_MS = 25;
2440
+ var DEFAULT_MAX_TRANSACTION_CHANGES = 1e4;
2441
+ var MAX_TRANSACTION_BYTES = 16 * 1048576;
2442
+ var nextProbeTable = 0;
2443
+ async function openLiveQuery(deps, sql, params, options) {
2444
+ const shape = analyseStatement(sql);
2445
+ await deps.watch(shape.table);
2446
+ const { conn, run } = await deps.cdc.liveConnection();
2447
+ const info = await run(() => tableInfoRows(conn, shape.table));
2448
+ if (info.length === 0) {
2449
+ throw new CDCError(`Cannot open a live query for '${sql}': table '${shape.table}' has no columns`);
2450
+ }
2451
+ const ddl = await run(() => selectTableSql(conn, shape.table));
2452
+ const plan = buildLivePlan(
2453
+ shape,
2454
+ {
2455
+ columns: info.map((column) => ({ name: column.name, type: column.type })),
2456
+ collations: ddl === null ? /* @__PURE__ */ new Map() : parseColumnCollations(ddl),
2457
+ pkColumns: info.filter((column) => column.pk > 0).sort((left, right) => left.pk - right.pk).map((column) => column.name)
2458
+ },
2459
+ params
2460
+ );
2461
+ const probeTable = `${LIVE_PROBE_TABLE_PREFIX}${nextProbeTable++}`;
2462
+ const probe = await RowProbe.open(conn, probeTable, plan, params, run);
2463
+ try {
2464
+ return await MaintainedLiveQuery.open(buildSource(deps, conn, run, plan, probe, params, options));
2465
+ } catch (err) {
2466
+ await probe.close().catch(() => {
2467
+ });
2468
+ throw err;
2469
+ }
2470
+ }
2471
+ function buildSource(deps, conn, run, plan, probe, params, options) {
2472
+ const delivery = new PrimedLiveDelivery(deps.cdc, conn, run, plan.shape.table);
2473
+ return {
2474
+ plan,
2475
+ probe,
2476
+ rereadJitterMs: options?.rereadJitterMs ?? DEFAULT_REREAD_JITTER_MS,
2477
+ maxTransactionChanges: options?.maxTransactionChanges ?? DEFAULT_MAX_TRANSACTION_CHANGES,
2478
+ maxTransactionBytes: MAX_TRANSACTION_BYTES,
2479
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
2480
+ read: () => readAtPosition2(deps.cdc, plan, params),
2481
+ start: (handlers, sinceSeq) => delivery.start(handlers, sinceSeq),
2482
+ release: () => probe.close()
2483
+ };
2484
+ }
2485
+ async function readAtPosition2(cdc, plan, params) {
2486
+ const captured = await cdc.readAtPositionWith(async (conn) => {
2487
+ const stmt = await conn.prepare(plan.readSql);
2488
+ return stmt.all(...bindRead(params));
2489
+ });
2490
+ return { rows: captured.value.map((row) => toHeldRow(plan, row)), seq: captured.seq };
2491
+ }
2492
+ function bindRead(params) {
2493
+ if (params === void 0) return [];
2494
+ return Array.isArray(params) ? params : [params];
2495
+ }
2496
+ function toHeldRow(plan, row) {
2497
+ const key = plan.usesRowid ? rowidKey(row[plan.keyColumnAliases[0]]) : encodeRowKey(plan.keyColumnAliases.map((alias) => row[alias]));
2498
+ const sort = plan.sortColumns.map((column, index) => toSortValue(row[column], plan.sortPlan.collations[index]));
2499
+ for (const alias of plan.keyColumnAliases) delete row[alias];
2500
+ for (const column of plan.sortColumns) delete row[column];
2501
+ return { key, row, sort };
2502
+ }
2503
+
2504
+ // src/core/migrations/concurrency.ts
2505
+ var BUSY_CODES = /* @__PURE__ */ new Set(["SQLITE_BUSY", "SQLITE_BUSY_SNAPSHOT", "SQLITE_BUSY_RECOVERY", "SQLITE_BUSY_TIMEOUT"]);
2506
+ function isConcurrentMigrationConflict(err) {
2507
+ if (!(err instanceof Error)) return false;
2508
+ const code = err.code;
2509
+ if (typeof code === "string" && BUSY_CODES.has(code)) return true;
2510
+ const message = err.message;
2511
+ if (/SQLITE_BUSY|database is locked/i.test(message)) return true;
2512
+ return message.includes(`UNIQUE constraint failed: ${MIGRATIONS_TABLE}.version`);
2513
+ }
2514
+
2515
+ // src/core/migrations/types.ts
2516
+ var MIGRATION_NAME_RE = /^\w+$/;
603
2517
 
604
2518
  // src/core/migrations/runner.ts
605
- var CREATE_TRACKING_TABLE = `
606
- CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
607
- version INTEGER PRIMARY KEY,
608
- name TEXT NOT NULL,
609
- applied_at REAL NOT NULL DEFAULT (unixepoch('subsec'))
610
- )
611
- `;
612
2519
  var MigrationRunner = class _MigrationRunner {
613
2520
  static async run(conn, migrations) {
614
- await conn.exec(CREATE_TRACKING_TABLE);
615
2521
  const validated = _MigrationRunner.validateMigrations(migrations);
616
- const applied = await _MigrationRunner.getAppliedVersions(conn);
617
- const pending = validated.filter((m) => !applied.has(m.version));
618
- if (pending.length === 0) {
619
- return { applied: [], skipped: validated.length };
620
- }
621
- const appliedEntries = [];
622
- await conn.transaction(async (txConn) => {
623
- const insertStmt = await txConn.prepare(`INSERT INTO ${MIGRATIONS_TABLE} (version, name) VALUES (?, ?)`);
2522
+ const effectiveBaseline = resolveEffectiveBaseline(validated);
2523
+ return _MigrationRunner.retryOnConcurrentConflict(
2524
+ () => _MigrationRunner.applyPending(conn, validated, effectiveBaseline)
2525
+ );
2526
+ }
2527
+ static async retryOnConcurrentConflict(operation) {
2528
+ try {
2529
+ return await operation();
2530
+ } catch (err) {
2531
+ if (!isConcurrentMigrationConflict(err)) throw err;
2532
+ }
2533
+ try {
2534
+ return await operation();
2535
+ } catch (err) {
2536
+ if (!isConcurrentMigrationConflict(err)) throw err;
2537
+ throw new MigrationError(
2538
+ "Another process is writing to the migration history and the busy timeout elapsed; retry once it finishes",
2539
+ 0,
2540
+ "MIGRATION_CONCURRENT"
2541
+ );
2542
+ }
2543
+ }
2544
+ static async applyPending(conn, validated, effectiveBaseline) {
2545
+ return conn.transaction(async (txConn) => {
2546
+ await ensureMigrationsTable(txConn);
2547
+ const applied = await _MigrationRunner.getAppliedRows(txConn);
2548
+ const backfills = reconcileMigrationChecksums(validated, applied);
2549
+ if (backfills.length > 0) {
2550
+ const updateStmt = await prepareUpdateMigrationChecksum(txConn);
2551
+ for (const backfill of backfills) {
2552
+ await updateStmt.run(backfill.checksum, backfill.version);
2553
+ }
2554
+ }
2555
+ const pending = planPendingMigrations(validated, effectiveBaseline, new Set(applied.keys()));
2556
+ if (pending.length === 0) {
2557
+ await syncSchemaVersion(txConn, _MigrationRunner.highestVersion(applied.keys()));
2558
+ return { applied: [], skipped: validated.length };
2559
+ }
2560
+ const appliedEntries = [];
2561
+ const insertStmt = await prepareInsertMigration(txConn);
624
2562
  for (const migration of pending) {
625
2563
  try {
626
2564
  if (typeof migration.up === "string") {
@@ -631,19 +2569,27 @@ var MigrationRunner = class _MigrationRunner {
631
2569
  }
632
2570
  } catch (err) {
633
2571
  if (err instanceof MigrationError) throw err;
2572
+ if (isConcurrentMigrationConflict(err)) throw err;
634
2573
  throw new MigrationError(
635
2574
  `Migration ${migration.version}_${migration.name} failed: ${err instanceof Error ? err.message : String(err)}`,
636
2575
  migration.version
637
2576
  );
638
2577
  }
639
- await insertStmt.run(migration.version, migration.name);
2578
+ await insertStmt.run(migration.version, migration.name, migrationContentChecksum(migration));
640
2579
  appliedEntries.push({ version: migration.version, name: migration.name });
641
2580
  }
2581
+ await mirrorSchemaVersion(
2582
+ txConn,
2583
+ Math.max(
2584
+ _MigrationRunner.highestVersion(applied.keys()),
2585
+ _MigrationRunner.highestVersion(appliedEntries.map((e) => e.version))
2586
+ )
2587
+ );
2588
+ return {
2589
+ applied: appliedEntries,
2590
+ skipped: validated.length - pending.length
2591
+ };
642
2592
  });
643
- return {
644
- applied: appliedEntries,
645
- skipped: validated.length - pending.length
646
- };
647
2593
  }
648
2594
  static async rollback(conn, migrations, version) {
649
2595
  if (version !== void 0 && (!Number.isSafeInteger(version) || version < 0)) {
@@ -653,76 +2599,92 @@ var MigrationRunner = class _MigrationRunner {
653
2599
  "MIGRATION_VALIDATION_ERROR"
654
2600
  );
655
2601
  }
656
- await conn.exec(CREATE_TRACKING_TABLE);
657
- const selectStmt = await conn.prepare(`SELECT version, name FROM ${MIGRATIONS_TABLE} ORDER BY version DESC`);
658
- const appliedRows = await selectStmt.all();
659
- if (appliedRows.length === 0) {
660
- return { rolledBack: [] };
661
- }
662
- let rollbackSet;
663
- if (version === void 0) {
664
- rollbackSet = [appliedRows[0]];
665
- } else {
666
- rollbackSet = appliedRows.filter((row) => row.version > version);
667
- }
668
- if (rollbackSet.length === 0) {
669
- return { rolledBack: [] };
670
- }
671
2602
  _MigrationRunner.validateMigrations(migrations);
672
- const rollbackVersions = rollbackSet.map((r) => r.version);
673
2603
  const inputByVersion = new Map(migrations.map((m) => [m.version, m]));
674
- const downByVersion = /* @__PURE__ */ new Map();
675
- for (const v of rollbackVersions) {
676
- const m = inputByVersion.get(v);
677
- if (!m || m.down === void 0) {
678
- throw new MigrationError(`Migration version ${v} has no down migration`, v, "MIGRATION_NO_DOWN");
679
- }
680
- downByVersion.set(v, m);
681
- }
682
- const rolledBackEntries = [];
683
- await conn.transaction(async (txConn) => {
684
- const deleteStmt = await txConn.prepare(`DELETE FROM ${MIGRATIONS_TABLE} WHERE version = ?`);
685
- for (const entry of rollbackSet) {
686
- const migration = downByVersion.get(entry.version);
687
- if (!migration) {
688
- throw new MigrationError(
689
- `No down migration found for version ${entry.version}`,
2604
+ return _MigrationRunner.retryOnConcurrentConflict(
2605
+ () => conn.transaction(async (txConn) => {
2606
+ await ensureMigrationsTable(txConn);
2607
+ const appliedRows = await selectAppliedMigrationsNewestFirst(txConn);
2608
+ if (appliedRows.length === 0) {
2609
+ return { rolledBack: [] };
2610
+ }
2611
+ let rollbackSet;
2612
+ if (version === void 0) {
2613
+ rollbackSet = [appliedRows[0]];
2614
+ } else {
2615
+ rollbackSet = appliedRows.filter((row) => row.version > version);
2616
+ }
2617
+ if (rollbackSet.length === 0) {
2618
+ return { rolledBack: [] };
2619
+ }
2620
+ const downByVersion = /* @__PURE__ */ new Map();
2621
+ for (const entry of rollbackSet) {
2622
+ downByVersion.set(
690
2623
  entry.version,
691
- "MIGRATION_ROLLBACK_ERROR"
2624
+ _MigrationRunner.resolveDownMigration(inputByVersion.get(entry.version), entry.version)
692
2625
  );
693
2626
  }
694
- try {
695
- if (typeof migration.down === "string") {
696
- await txConn.exec(migration.down);
697
- } else {
698
- const result = migration.down?.(new Transaction(txConn));
699
- if (result instanceof Promise) await result;
2627
+ const rolledBackEntries = [];
2628
+ const deleteStmt = await prepareDeleteMigration(txConn);
2629
+ for (const entry of rollbackSet) {
2630
+ const migration = downByVersion.get(entry.version);
2631
+ if (!migration) {
2632
+ throw new MigrationError(
2633
+ `No down migration found for version ${entry.version}`,
2634
+ entry.version,
2635
+ "MIGRATION_ROLLBACK_ERROR"
2636
+ );
700
2637
  }
701
- } catch (err) {
702
- if (err instanceof MigrationError) throw err;
703
- throw new MigrationError(
704
- `Rollback of migration ${entry.version}_${entry.name} failed: ${err instanceof Error ? err.message : String(err)}`,
705
- entry.version,
706
- "MIGRATION_ROLLBACK_ERROR"
707
- );
2638
+ try {
2639
+ if (typeof migration.down === "string") {
2640
+ await txConn.exec(migration.down);
2641
+ } else {
2642
+ const result = migration.down?.(new Transaction(txConn));
2643
+ if (result instanceof Promise) await result;
2644
+ }
2645
+ } catch (err) {
2646
+ if (err instanceof MigrationError) throw err;
2647
+ if (isConcurrentMigrationConflict(err)) throw err;
2648
+ throw new MigrationError(
2649
+ `Rollback of migration ${entry.version}_${entry.name} failed: ${err instanceof Error ? err.message : String(err)}`,
2650
+ entry.version,
2651
+ "MIGRATION_ROLLBACK_ERROR"
2652
+ );
2653
+ }
2654
+ await deleteStmt.run(entry.version);
2655
+ rolledBackEntries.push({ version: entry.version, name: entry.name });
708
2656
  }
709
- await deleteStmt.run(entry.version);
710
- rolledBackEntries.push({ version: entry.version, name: entry.name });
711
- }
712
- });
713
- return { rolledBack: rolledBackEntries };
2657
+ const rolledBackVersions = new Set(rollbackSet.map((r) => r.version));
2658
+ const remaining = appliedRows.find((row) => !rolledBackVersions.has(row.version));
2659
+ await mirrorSchemaVersion(txConn, remaining === void 0 ? 0 : remaining.version);
2660
+ return { rolledBack: rolledBackEntries };
2661
+ })
2662
+ );
2663
+ }
2664
+ static resolveDownMigration(migration, version) {
2665
+ if (!migration) {
2666
+ throw new MigrationError(`Migration version ${version} has no down migration`, version, "MIGRATION_NO_DOWN");
2667
+ }
2668
+ if (migration.down !== void 0) {
2669
+ return migration;
2670
+ }
2671
+ const readDown = migration[LAZY_DOWN_SQL];
2672
+ if (readDown === void 0) {
2673
+ throw new MigrationError(`Migration version ${version} has no down migration`, version, "MIGRATION_NO_DOWN");
2674
+ }
2675
+ return { version: migration.version, name: migration.name, up: migration.up, down: readDown() };
714
2676
  }
715
2677
  static validateMigrations(migrations) {
716
2678
  const seenVersions = /* @__PURE__ */ new Map();
717
2679
  for (const m of migrations) {
718
- if (!Number.isSafeInteger(m.version) || m.version <= 0) {
2680
+ if (!Number.isSafeInteger(m.version) || m.version <= 0 || m.version > SQLITE_USER_VERSION_MAX) {
719
2681
  throw new MigrationError(
720
- `Invalid migration version: ${m.version}`,
2682
+ `Invalid migration version: ${m.version}. Versions must be integers from 1 to ${SQLITE_USER_VERSION_MAX} so they mirror to PRAGMA user_version`,
721
2683
  typeof m.version === "number" && Number.isFinite(m.version) ? m.version : 0,
722
2684
  "MIGRATION_VALIDATION_ERROR"
723
2685
  );
724
2686
  }
725
- if (!/^\w+$/.test(m.name)) {
2687
+ if (!MIGRATION_NAME_RE.test(m.name)) {
726
2688
  throw new MigrationError(`Invalid migration name: '${m.name}'`, m.version, "MIGRATION_VALIDATION_ERROR");
727
2689
  }
728
2690
  const existing = seenVersions.get(m.version);
@@ -751,89 +2713,37 @@ var MigrationRunner = class _MigrationRunner {
751
2713
  }
752
2714
  return [...migrations].sort((a, b) => a.version - b.version);
753
2715
  }
754
- static async getAppliedVersions(conn) {
755
- const stmt = await conn.prepare(`SELECT version FROM ${MIGRATIONS_TABLE} ORDER BY version`);
756
- const rows = await stmt.all();
757
- return new Set(rows.map((r) => r.version));
2716
+ static highestVersion(versions) {
2717
+ let highest = 0;
2718
+ for (const version of versions) {
2719
+ if (version > highest) highest = version;
2720
+ }
2721
+ return highest;
2722
+ }
2723
+ static async getAppliedRows(conn) {
2724
+ const rows = await selectAppliedMigrations(conn);
2725
+ return new Map(rows.map((r) => [r.version, { name: r.name, checksum: r.checksum }]));
758
2726
  }
759
2727
  };
760
2728
 
761
- // src/core/worker/config.ts
762
- var DEFAULT_MAX_PENDING_WRITES = 1024;
763
- var DEFAULT_RETRY_AFTER_MS = 1e3;
764
- function requireCount(value, name, min) {
765
- if (!Number.isInteger(value) || value < min) {
766
- throw new SirannonError(`writerWorker.${name} must be an integer >= ${min}`, "INVALID_WRITER_WORKER");
767
- }
768
- return value;
2729
+ // src/core/database-migrations.ts
2730
+ async function refreshTriggersAfterSchemaChange(tracker, conn) {
2731
+ if (!tracker || tracker.watchedTables.size === 0) return;
2732
+ await tracker.refreshAllTriggersUsingConnection(conn);
769
2733
  }
770
- function resolveWriterWorkerConfig(option) {
771
- if (!option) {
772
- return { enabled: false, maxPendingWrites: 0, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host: {} };
773
- }
774
- const opts = option === true ? {} : option;
775
- const host = {};
776
- if (opts.writeTimeoutMs !== void 0) host.writeTimeoutMs = requireCount(opts.writeTimeoutMs, "writeTimeoutMs", 0);
777
- if (opts.maxRestarts !== void 0) host.maxRestarts = requireCount(opts.maxRestarts, "maxRestarts", 0);
778
- const maxPendingWrites = opts.maxPendingWrites === void 0 ? DEFAULT_MAX_PENDING_WRITES : requireCount(opts.maxPendingWrites, "maxPendingWrites", 1);
779
- return { enabled: true, maxPendingWrites, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host };
2734
+ async function migrateWithTriggerRefresh(conn, tracker, migrations) {
2735
+ const result = await MigrationRunner.run(conn, migrations);
2736
+ if (result.applied.length > 0) await refreshTriggersAfterSchemaChange(tracker, conn);
2737
+ return result;
780
2738
  }
781
-
782
- // src/core/worker/gate.ts
783
- var WriteGate = class {
784
- constructor(limit, retryAfterMs) {
785
- this.limit = limit;
786
- this.retryAfterMs = retryAfterMs;
787
- }
788
- inFlight = 0;
789
- run(op) {
790
- if (this.limit > 0 && this.inFlight >= this.limit) {
791
- return Promise.reject(new WriteOverloadError(this.limit, this.retryAfterMs));
792
- }
793
- this.inFlight++;
794
- return op().finally(() => {
795
- this.inFlight--;
796
- });
797
- }
798
- };
799
-
800
- // src/core/writer-lock.ts
801
- var UNTRACKED = {
802
- run: (operation) => operation(),
803
- isActive: () => false,
804
- exit: (operation) => operation()
805
- };
806
- var WriterLock = class {
807
- tail = Promise.resolve();
808
- context;
809
- constructor(context) {
810
- this.context = context ?? UNTRACKED;
811
- }
812
- run(operation) {
813
- if (this.context.isActive()) {
814
- return operation();
815
- }
816
- const enter = () => this.context.run(operation);
817
- const ticket = this.tail.then(enter, enter);
818
- this.tail = ticket.then(swallow, swallow);
819
- return ticket;
820
- }
821
- isHeld() {
822
- return this.context.isActive();
823
- }
824
- async settle() {
825
- await this.run(() => Promise.resolve());
826
- }
827
- /**
828
- * Work scheduled from inside a held operation inherits it, so shared
829
- * machinery must detach or it runs every caller's work inside one caller's
830
- * transaction.
831
- */
832
- detached(operation) {
833
- return this.context.exit(operation);
834
- }
835
- };
836
- function swallow() {
2739
+ async function rollbackWithTriggerRefresh(conn, tracker, migrations, version) {
2740
+ const result = await MigrationRunner.rollback(conn, migrations, version);
2741
+ if (result.rolledBack.length > 0) await refreshTriggersAfterSchemaChange(tracker, conn);
2742
+ return result;
2743
+ }
2744
+ async function readAppliedMigrations(conn) {
2745
+ if (!await selectTableExists(conn, MIGRATIONS_TABLE)) return [];
2746
+ return selectAppliedMigrations(conn);
837
2747
  }
838
2748
 
839
2749
  // src/core/database.ts
@@ -851,105 +2761,53 @@ var Database = class _Database {
851
2761
  closeListeners = [];
852
2762
  _closed = false;
853
2763
  cdc;
854
- hookRegistry = new HookRegistry();
2764
+ sync;
2765
+ hookRegistry;
855
2766
  observer;
856
2767
  backups;
857
- constructor(id, path, pool, driver, writeGate, options, internals) {
2768
+ reads;
2769
+ constructor(id, path, driver, runtime, options) {
858
2770
  this.id = id;
859
2771
  this.path = path;
860
- this.pool = pool;
861
2772
  this.driver = driver;
862
- this.writeGate = writeGate;
863
- this.writerLock = new WriterLock(driver.createWriterContext?.());
864
- this.backups = new DatabaseBackupController(
865
- (op) => this.writerLock.run(op),
866
- () => this.pool.acquireWriter(),
867
- driver.createBackupEngine?.()
868
- );
2773
+ this.pool = runtime.pool;
2774
+ this.writeGate = runtime.writeGate;
2775
+ this.writerLock = runtime.writerLock;
2776
+ this.hookRegistry = runtime.hookRegistry;
2777
+ this.observer = runtime.observer;
2778
+ this.backups = runtime.backups;
2779
+ this.cdc = runtime.cdc;
2780
+ this.sync = runtime.sync;
2781
+ this.groupCommitter = runtime.groupCommitter;
2782
+ this.reads = { pool: runtime.pool, writerLock: runtime.writerLock, observer: runtime.observer };
869
2783
  this.readOnly = options?.readOnly ?? false;
870
2784
  this.synchronous = options?.synchronous ?? DEFAULT_SYNCHRONOUS;
871
2785
  this.walMode = options?.walMode ?? true;
872
- this.observer = new DatabaseObserver(
873
- id,
874
- this.hookRegistry,
875
- internals?.parentHooks ?? null,
876
- internals?.metrics ?? null
877
- );
878
- this.cdc = new DatabaseCdcController(
879
- (op) => this.writerLock.run(op),
880
- () => this.pool.acquireWriter(),
881
- options?.cdcPollInterval ?? 50,
882
- options?.cdcRetention ?? 36e5
883
- );
884
- this.groupCommitter = new GroupCommitter(this.writerLock, {
885
- acquireWriter: () => this.pool.acquireWriter(),
886
- afterCommit: (writer, sql) => applyDdlSideEffectsIfRelevant(this.cdc.changeTracker, writer, sql)
887
- });
888
2786
  }
889
2787
  static async create(id, path, driver, options, internals) {
890
- const writerWorker = resolveWriterWorkerConfig(options?.writerWorker);
891
- const readOnly = options?.readOnly ?? false;
892
- if (writerWorker.enabled && !readOnly && !driver.startWriterHost) {
893
- throw new SirannonError(
894
- `writerWorker is enabled for database '${id}' but the driver does not carry a worker entry; use a driver that supports worker offload or disable writerWorker`,
895
- "WRITER_WORKER_UNSUPPORTED"
896
- );
897
- }
898
- const pool = await ConnectionPool.create({
899
- driver,
900
- path,
901
- readOnly: options?.readOnly,
902
- readPoolSize: options?.readPoolSize ?? 4,
903
- walMode: options?.walMode ?? true,
904
- synchronous: options?.synchronous,
905
- useWriterWorker: writerWorker.enabled && !readOnly,
906
- workerHostOptions: writerWorker.host
907
- });
908
- const writeGate = new WriteGate(writerWorker.enabled ? writerWorker.maxPendingWrites : 0, writerWorker.retryAfterMs);
909
- return new _Database(id, path, pool, driver, writeGate, options, internals);
2788
+ const runtime = await createDatabaseRuntime(id, path, driver, options, internals);
2789
+ return new _Database(id, path, driver, runtime, options);
2790
+ }
2791
+ async applyChanges(batch, resolver) {
2792
+ this.ensureOpen();
2793
+ if (this.readOnly) throw new ReadOnlyError(this.id);
2794
+ return this.sync.applyChanges(batch, resolver);
2795
+ }
2796
+ deviceSync() {
2797
+ this.ensureNotClosed();
2798
+ return this.sync.devicePort();
910
2799
  }
911
2800
  async query(sql, params, options) {
912
2801
  this.ensureOpen();
913
- return this.observer.withQueryHooks(
914
- sql,
915
- params,
916
- options,
917
- () => this.runRead(sql, (conn) => query(conn, sql, params))
918
- );
2802
+ return readRows(this.reads, sql, params, options);
919
2803
  }
920
- /**
921
- * Returns query rows already encoded for the wire (safe-range integers as
922
- * plain numbers, larger integers and BLOBs as tagged envelopes) in a single
923
- * pass. The server response path uses this so a read walks its values once
924
- * rather than narrowing on the driver and re-scanning to tag.
925
- */
926
2804
  async queryForWire(sql, params, options) {
927
2805
  this.ensureOpen();
928
- return this.observer.withQueryHooks(
929
- sql,
930
- params,
931
- options,
932
- () => this.runRead(sql, (conn) => queryForWire(conn, sql, params))
933
- );
2806
+ return readWireRows(this.reads, sql, params, options);
934
2807
  }
935
2808
  async queryOne(sql, params, options) {
936
2809
  this.ensureOpen();
937
- return this.observer.withQueryHooks(
938
- sql,
939
- params,
940
- options,
941
- () => this.runRead(sql, (conn) => queryOne(conn, sql, params))
942
- );
943
- }
944
- /**
945
- * On a single-connection driver the reader is the writer, so serialise the
946
- * read through the writer lock to avoid a bulk load's uncommitted rows.
947
- */
948
- runRead(sql, op) {
949
- if (this.pool.readerCount === 0) {
950
- return this.writerLock.run(() => this.observer.track(sql, () => op(this.pool.acquireWriter())));
951
- }
952
- return this.observer.track(sql, () => op(this.pool.acquireReader()));
2810
+ return readOneRow(this.reads, sql, params, options);
953
2811
  }
954
2812
  async execute(sql, params, options) {
955
2813
  this.ensureOpen();
@@ -981,7 +2839,14 @@ var Database = class _Database {
981
2839
  );
982
2840
  }
983
2841
  async runInTransaction(writer, sql, run) {
984
- const result = await this.observer.track(sql, () => writer.transaction(run));
2842
+ const result = await this.observer.track(
2843
+ sql,
2844
+ () => writer.transaction(async (txConn) => {
2845
+ const value = await run(txConn);
2846
+ await this.cdc.applyStamps(txConn);
2847
+ return value;
2848
+ })
2849
+ );
985
2850
  await applyDdlSideEffectsIfRelevant(this.cdc.changeTracker, writer, sql);
986
2851
  return result;
987
2852
  }
@@ -1062,6 +2927,13 @@ var Database = class _Database {
1062
2927
  if (this._closed) return;
1063
2928
  await this.writerLock.run(() => op(this.pool.acquireWriter()));
1064
2929
  }
2930
+ async ensureChangeStamping() {
2931
+ this.ensureOpen();
2932
+ if (this.readOnly) {
2933
+ throw new ReadOnlyError(this.id);
2934
+ }
2935
+ await this.cdc.ensureStamping();
2936
+ }
1065
2937
  async unwatch(table) {
1066
2938
  this.ensureOpen();
1067
2939
  await this.cdc.unwatch(table);
@@ -1070,13 +2942,26 @@ var Database = class _Database {
1070
2942
  this.ensureOpen();
1071
2943
  return this.cdc.on(table);
1072
2944
  }
2945
+ async live(sql, params, options) {
2946
+ this.ensureOpen();
2947
+ if (this.readOnly) throw new ReadOnlyError(this.id);
2948
+ return openLiveQuery({ cdc: this.cdc, watch: (table) => this.watch(table) }, sql, params, options);
2949
+ }
1073
2950
  async migrate(migrations) {
1074
2951
  this.ensureOpen();
1075
- return this.writerLock.run(() => MigrationRunner.run(this.pool.acquireWriter(), migrations));
2952
+ return this.writerLock.run(
2953
+ () => migrateWithTriggerRefresh(this.pool.acquireWriter(), this.cdc.changeTracker, migrations)
2954
+ );
2955
+ }
2956
+ async appliedMigrations() {
2957
+ this.ensureNotClosed();
2958
+ return this.writerLock.run(() => readAppliedMigrations(this.pool.acquireWriter()));
1076
2959
  }
1077
2960
  async rollback(migrations, version) {
1078
2961
  this.ensureOpen();
1079
- return this.writerLock.run(() => MigrationRunner.rollback(this.pool.acquireWriter(), migrations, version));
2962
+ return this.writerLock.run(
2963
+ () => rollbackWithTriggerRefresh(this.pool.acquireWriter(), this.cdc.changeTracker, migrations, version)
2964
+ );
1080
2965
  }
1081
2966
  async backup(destPath) {
1082
2967
  this.ensureOpen();
@@ -1097,7 +2982,7 @@ var Database = class _Database {
1097
2982
  this.hookRegistry.register("afterQuery", hook);
1098
2983
  }
1099
2984
  addCloseListener(fn) {
1100
- this.ensureOpen();
2985
+ this.ensureNotClosed();
1101
2986
  this.closeListeners.push(fn);
1102
2987
  }
1103
2988
  async close() {
@@ -1107,6 +2992,7 @@ var Database = class _Database {
1107
2992
  this.backups.cancelAll();
1108
2993
  let poolError;
1109
2994
  try {
2995
+ await this.cdc.closeLiveConnection();
1110
2996
  await this.groupCommitter.drain();
1111
2997
  await this.writerLock.settle();
1112
2998
  await this.pool.close();
@@ -1130,6 +3016,15 @@ var Database = class _Database {
1130
3016
  return this.pool.readerCount;
1131
3017
  }
1132
3018
  ensureOpen() {
3019
+ this.ensureNotClosed();
3020
+ if (this.sync.snapshotLoadBlocked) {
3021
+ throw new SirannonError(
3022
+ `Database '${this.id}' is replacing its data from a sync snapshot; retry once the snapshot load completes`,
3023
+ "SNAPSHOT_IN_PROGRESS"
3024
+ );
3025
+ }
3026
+ }
3027
+ ensureNotClosed() {
1133
3028
  if (this._closed) {
1134
3029
  throw new SirannonError(`Database '${this.id}' is closed`, "DATABASE_CLOSED");
1135
3030
  }
@@ -1353,6 +3248,71 @@ var MetricsCollector = class {
1353
3248
  }
1354
3249
  };
1355
3250
 
3251
+ // src/core/migrations/from-files.ts
3252
+ function basename(key) {
3253
+ const separator = Math.max(key.lastIndexOf("/"), key.lastIndexOf("\\"));
3254
+ return separator === -1 ? key : key.slice(separator + 1);
3255
+ }
3256
+ function migrationsFromFiles(files, options) {
3257
+ const grouped = /* @__PURE__ */ new Map();
3258
+ for (const [key, contents] of Object.entries(files)) {
3259
+ const parsed = parseMigrationFilename(basename(key));
3260
+ if (!parsed) {
3261
+ throw new MigrationError(
3262
+ `Migration file '${key}' does not match the <version>_<name>.up.sql / <version>_<name>.down.sql convention`,
3263
+ 0,
3264
+ "MIGRATION_VALIDATION_ERROR"
3265
+ );
3266
+ }
3267
+ if (typeof contents !== "string") {
3268
+ throw new MigrationError(
3269
+ `Migration file '${key}' must map to its SQL text; received ${typeof contents}. When bundling with import.meta.glob, pass { query: '?raw', import: 'default', eager: true }`,
3270
+ parsed.version,
3271
+ "MIGRATION_VALIDATION_ERROR"
3272
+ );
3273
+ }
3274
+ const sql = contents.trim();
3275
+ if (sql.length === 0) {
3276
+ throw new MigrationError(`Migration file is empty: ${key}`, parsed.version, "MIGRATION_VALIDATION_ERROR");
3277
+ }
3278
+ const existing = grouped.get(parsed.version);
3279
+ if (!existing) {
3280
+ grouped.set(parsed.version, { name: parsed.name, [parsed.direction]: sql });
3281
+ continue;
3282
+ }
3283
+ if (existing.name !== parsed.name) {
3284
+ throw new MigrationError(
3285
+ `Duplicate migration version ${parsed.version}: '${existing.name}' and '${parsed.name}'`,
3286
+ parsed.version,
3287
+ "MIGRATION_DUPLICATE_VERSION"
3288
+ );
3289
+ }
3290
+ if (existing[parsed.direction] !== void 0) {
3291
+ throw new MigrationError(
3292
+ `Duplicate ${parsed.direction} migration for version ${parsed.version} ('${parsed.name}')`,
3293
+ parsed.version,
3294
+ "MIGRATION_DUPLICATE_VERSION"
3295
+ );
3296
+ }
3297
+ existing[parsed.direction] = sql;
3298
+ }
3299
+ const migrations = [];
3300
+ for (const [version, entry] of grouped) {
3301
+ if (entry.up === void 0) {
3302
+ throw new MigrationError(
3303
+ `Migration version ${version} (${entry.name}) is missing an .up.sql file`,
3304
+ version,
3305
+ "MIGRATION_VALIDATION_ERROR"
3306
+ );
3307
+ }
3308
+ migrations.push(
3309
+ entry.down === void 0 ? { version, name: entry.name, up: entry.up } : { version, name: entry.name, up: entry.up, down: entry.down }
3310
+ );
3311
+ }
3312
+ migrations.sort((a, b) => a.version - b.version);
3313
+ return applyBaselineOption(migrations, options?.baseline);
3314
+ }
3315
+
1356
3316
  // src/core/sirannon.ts
1357
3317
  var Sirannon = class {
1358
3318
  constructor(options) {
@@ -1369,6 +3329,8 @@ var Sirannon = class {
1369
3329
  }
1370
3330
  dbs = /* @__PURE__ */ new Map();
1371
3331
  opening = /* @__PURE__ */ new Set();
3332
+ resolving = /* @__PURE__ */ new Map();
3333
+ migrationSet = null;
1372
3334
  _shutdown = false;
1373
3335
  _driver;
1374
3336
  hookRegistry;
@@ -1400,7 +3362,19 @@ var Sirannon = class {
1400
3362
  "DATABASE_OPEN_FAILED"
1401
3363
  );
1402
3364
  }
1403
- this.opening.delete(id);
3365
+ try {
3366
+ await this.applyRegistryMigrations(db);
3367
+ } catch (err) {
3368
+ await db.close().catch(() => {
3369
+ });
3370
+ if (err instanceof SirannonError) throw err;
3371
+ throw new SirannonError(
3372
+ `Failed to migrate database '${id}' at '${path}': ${err instanceof Error ? err.message : String(err)}`,
3373
+ "DATABASE_OPEN_FAILED"
3374
+ );
3375
+ } finally {
3376
+ this.opening.delete(id);
3377
+ }
1404
3378
  if (this._shutdown) {
1405
3379
  await db.close().catch(() => {
1406
3380
  });
@@ -1464,7 +3438,49 @@ var Sirannon = class {
1464
3438
  const db = this.get(id);
1465
3439
  if (db) return db;
1466
3440
  if (this._shutdown) return void 0;
1467
- return this.lifecycleManager?.resolve(id);
3441
+ const manager = this.lifecycleManager;
3442
+ if (!manager) return void 0;
3443
+ const pending = this.resolving.get(id);
3444
+ if (pending) return pending;
3445
+ const inFlight = manager.resolve(id).finally(() => {
3446
+ this.resolving.delete(id);
3447
+ });
3448
+ this.resolving.set(id, inFlight);
3449
+ return inFlight;
3450
+ }
3451
+ registryMigrations() {
3452
+ return this.loadMigrationSet();
3453
+ }
3454
+ async applyRegistryMigrations(db) {
3455
+ if (this.options.migrations === void 0 || db.readOnly) return;
3456
+ const migrations = await this.loadMigrationSet();
3457
+ if (migrations.length === 0) return;
3458
+ await db.migrate(migrations);
3459
+ }
3460
+ loadMigrationSet() {
3461
+ const source = this.options.migrations;
3462
+ if (source === void 0 || Array.isArray(source)) {
3463
+ return Promise.resolve(source ?? []);
3464
+ }
3465
+ if (this.migrationSet) return this.migrationSet;
3466
+ const loading = (async () => {
3467
+ const set = await source();
3468
+ if (!Array.isArray(set)) {
3469
+ throw new MigrationError(
3470
+ "The migrations source must return an array of migrations",
3471
+ 0,
3472
+ "MIGRATION_SOURCE_INVALID"
3473
+ );
3474
+ }
3475
+ return set;
3476
+ })();
3477
+ loading.catch(() => {
3478
+ if (this.migrationSet === loading) {
3479
+ this.migrationSet = null;
3480
+ }
3481
+ });
3482
+ this.migrationSet = loading;
3483
+ return loading;
1468
3484
  }
1469
3485
  has(id) {
1470
3486
  return this.dbs.has(id);
@@ -1512,4 +3528,12 @@ var Sirannon = class {
1512
3528
  }
1513
3529
  };
1514
3530
 
1515
- export { ConnectionPool, Database, HookRegistry, LifecycleManager, MetricsCollector, MigrationRunner, Sirannon, createTenantResolver, sanitizeTenantId, tenantPath };
3531
+ // src/core/operation-registry.ts
3532
+ function operationRef(name) {
3533
+ return { name };
3534
+ }
3535
+ function operationName(operation) {
3536
+ return typeof operation === "string" ? operation : operation.name;
3537
+ }
3538
+
3539
+ export { ConnectionPool, Database, HookRegistry, LifecycleManager, MIGRATION_NAME_RE, MetricsCollector, MigrationRunner, Sirannon, createTenantResolver, migrationsFromFiles, operationName, operationRef, sanitizeTenantId, tenantPath };