@jarenjs/db 0.56.0 → 0.67.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 (79) hide show
  1. package/ARCHITECTURE.md +412 -56
  2. package/README.md +600 -57
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +293 -45
  5. package/docs/LIVE-FORMAT.md +169 -20
  6. package/docs/MIGRATION-FORMAT.md +142 -17
  7. package/docs/MODEL-FORMAT.md +752 -64
  8. package/docs/REPLICATION-FORMAT.md +208 -0
  9. package/package.json +21 -7
  10. package/schemas/jaren-model.draft-07.schema.json +224 -162
  11. package/schemas/jaren-model.schema.json +224 -162
  12. package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
  13. package/schemas/jaren-replication-snapshot.schema.json +83 -0
  14. package/schemas/jaren-replication.draft-07.schema.json +82 -0
  15. package/schemas/jaren-replication.schema.json +82 -0
  16. package/src/algebra.js +227 -9
  17. package/src/backup.js +161 -0
  18. package/src/cancellation.js +48 -0
  19. package/src/capture.js +230 -47
  20. package/src/cli.js +165 -59
  21. package/src/cursor.js +417 -0
  22. package/src/dag-job.js +154 -21
  23. package/src/ddl.js +102 -8
  24. package/src/dialect.js +268 -113
  25. package/src/dialects/expression-read.js +158 -0
  26. package/src/dialects/postgres.js +618 -0
  27. package/src/dialects/rtree-ddl.js +129 -0
  28. package/src/dialects/sqlite.js +244 -11
  29. package/src/document-files.js +311 -0
  30. package/src/document-steps.js +422 -0
  31. package/src/documents.js +335 -0
  32. package/src/driver.js +448 -61
  33. package/src/drivers/bun.js +37 -1
  34. package/src/drivers/indexeddb-snapshot.js +149 -0
  35. package/src/drivers/node-pool.js +11 -0
  36. package/src/drivers/node-worker-endpoint.js +105 -0
  37. package/src/drivers/node-worker.js +204 -0
  38. package/src/drivers/node.js +41 -7
  39. package/src/drivers/postgres.js +331 -0
  40. package/src/drivers/wasm-oo1.js +97 -0
  41. package/src/drivers/wasm-session.js +67 -0
  42. package/src/drivers/wasm.js +17 -83
  43. package/src/drivers/worker-pool.js +183 -0
  44. package/src/drivers/worker-protocol.js +79 -0
  45. package/src/drivers/worker-queue.js +60 -0
  46. package/src/emit.js +339 -48
  47. package/src/entity.js +20 -22
  48. package/src/errors.js +430 -19
  49. package/src/expression.js +284 -0
  50. package/src/graph.js +64 -8
  51. package/src/index.js +48 -17
  52. package/src/introspect.js +583 -0
  53. package/src/jobs.js +843 -107
  54. package/src/json-bytes.js +58 -0
  55. package/src/live-join.js +250 -0
  56. package/src/live-nested.js +120 -0
  57. package/src/live.js +18 -4
  58. package/src/logical-rows.js +90 -0
  59. package/src/maintenance.js +175 -0
  60. package/src/migrate.js +248 -181
  61. package/src/model.js +68 -0
  62. package/src/plan.js +1119 -138
  63. package/src/pragmas.js +314 -0
  64. package/src/profile.js +151 -3
  65. package/src/query.js +1634 -323
  66. package/src/replication-format.js +115 -0
  67. package/src/replication.js +332 -0
  68. package/src/residual.js +17 -0
  69. package/src/series.js +12 -4
  70. package/src/store.js +1567 -273
  71. package/src/tracker.js +203 -29
  72. package/src/udf.js +88 -7
  73. package/types/index.d.ts +1158 -27
  74. package/types/node-pool.d.ts +28 -0
  75. package/types/node-worker.d.ts +54 -0
  76. package/types/node.d.ts +69 -2
  77. package/types/postgres.d.ts +46 -0
  78. package/types/typed.d.ts +27 -4
  79. package/types/wasm.d.ts +14 -0
package/src/driver.js CHANGED
@@ -19,20 +19,28 @@
19
19
  * specifiers. `open()` is where "this driver does not exist here"
20
20
  * becomes the coded `JD0003` instead of a module-load crash.
21
21
  *
22
- * `capabilities` is read once at open — from the library's version
23
- * report, its compile options and the binding's declaration — and is
24
- * the single source of truth for feature gating; never a `typeof`
25
- * sniff at a call site. Two slots are deliberately EMPTY on every
26
- * SQLite driver: `statementTimeout` (no interrupt or progress handler
27
- * exists to build one on) and `rowEstimates` (the query plan is prose,
28
- * not numbers). They exist so a driver that has the facts can fill
22
+ * `capabilities` is read once at open by a PROBE — the SQLite one by
23
+ * default, another engine's through `options.probe` — and is the single
24
+ * source of truth for feature gating; never a `typeof` sniff at a call
25
+ * site. {@link baseCapabilities} gives every slot the conservative
26
+ * answer, so a probe that says nothing about a feature says `false`
27
+ * rather than `undefined`. Two slots are deliberately EMPTY on every
28
+ * driver this package ships: `statementTimeout` (SQLite has no
29
+ * interrupt or progress handler to build one on, and a PostgreSQL
30
+ * server-side timeout is not the same promise as the store's
31
+ * `AbortSignal`) and `rowEstimates` (SQLite's query plan is prose, not
32
+ * numbers). They exist so a driver that has the facts can fill
29
33
  * them without a contract change; pretending SQLite has them is the
30
- * silent degradation this suite refuses.
34
+ * silent degradation this suite refuses. A third, `lazyIteration`, is
35
+ * probed rather than declared: whether the binding's statements carry
36
+ * a native row iterator, which is what lets a cursor classify itself
37
+ * honestly — a binding without one gets `iterate` composed over `all()`
38
+ * here, and the cursor over it says it buffers.
31
39
  */
32
40
 
33
41
  import { isThenable, chain, toPromise } from '@jarenjs/core/function';
34
42
 
35
- import { DbCompileError, DbRuntimeError } from './errors.js';
43
+ import { DbCompileError, DbRuntimeError, TransactionFailure } from './errors.js';
36
44
 
37
45
  /** The minimum SQLite the store accepts, asserted at open. */
38
46
  export const SQLITE_FLOOR = '3.45.0';
@@ -46,6 +54,19 @@ export const SQLITE_FLOOR = '3.45.0';
46
54
  */
47
55
  export const DEFAULT_QUEUE_TIMEOUT = 5000;
48
56
 
57
+ /**
58
+ * Why a queued caller gave up. It is always a coded refusal — a caller
59
+ * has to be able to tell "you were cancelled before you ran" from every
60
+ * other failure — and the host's own `reason` rides along as the cause,
61
+ * so a cancellation that meant something specific still says it.
62
+ * @param {AbortSignal} [signal]
63
+ */
64
+ function abortReason(signal) {
65
+ return new DbRuntimeError('JD2064',
66
+ 'the call was aborted while it waited for the open transaction to settle; '
67
+ + 'it ran no statement', { cause: signal?.reason });
68
+ }
69
+
49
70
  // the sync-capable-async helpers are `@jarenjs/core/function`'s (one
50
71
  // implementation in the suite); re-exported here because every store
51
72
  // module and the public `@jarenjs/db` surface reach them through this seam
@@ -96,16 +117,40 @@ export function lazyOpen(specifier, reason, use, args) {
96
117
  * @returns {{ run: Function, get: Function, all: Function,
97
118
  * iterate: Function }}
98
119
  */
99
- export function wrapStatement(statement, guard = undefined) {
120
+ export function wrapStatement(statement, guard = undefined, active = undefined) {
100
121
  const before = guard ?? (() => {});
122
+ const track = (source) => {
123
+ if (active === undefined) return source;
124
+ let done = false;
125
+ const iterator = {
126
+ next: () => {
127
+ before();
128
+ if (done) return { done: true, value: undefined };
129
+ return chain(source.next(), (step) => {
130
+ if (step.done) { done = true; active.delete(iterator); }
131
+ return step;
132
+ });
133
+ },
134
+ return: (value) => {
135
+ if (done) return { done: true, value };
136
+ done = true;
137
+ active.delete(iterator);
138
+ return source.return?.(value) ?? { done: true, value };
139
+ },
140
+ [Symbol.iterator]: () => iterator,
141
+ [Symbol.asyncIterator]: () => iterator,
142
+ };
143
+ active.add(iterator);
144
+ return iterator;
145
+ };
101
146
  return {
102
147
  run: (params = []) => { before(); return statement.run(params); },
103
148
  get: (params = []) => { before(); return statement.get(params); },
104
149
  all: (params = []) => { before(); return statement.all(params); },
105
150
  iterate: typeof statement.iterate === 'function'
106
- ? (params = []) => { before(); return /** @type {Function} */ (statement.iterate)(params); }
151
+ ? (params = []) => { before(); return chain(/** @type {Function} */ (statement.iterate)(params), track); }
107
152
  : (params = []) => { before(); return chain(statement.all(params),
108
- (rows) => rows[Symbol.iterator]()); },
153
+ (rows) => track(rows[Symbol.iterator]())); },
109
154
  };
110
155
  }
111
156
 
@@ -133,6 +178,42 @@ export function attempt(call, wrap) {
133
178
  : out;
134
179
  }
135
180
 
181
+ /**
182
+ * Settle a transaction only after its commit succeeds. Deferred constraints
183
+ * can refuse COMMIT or RELEASE after the body returned successfully; that
184
+ * failure owes the same rollback as a failing body.
185
+ * @param {() => any} body
186
+ * @param {() => any} commit
187
+ * @param {() => any} rollback
188
+ * @returns {any}
189
+ */
190
+ function settleTransaction(body, commit, rollback) {
191
+ const fail = (error) => chain(attempt(rollback, (cleanupError) =>
192
+ // Losing one remote generation also loses its rollback channel.
193
+ // That is one failure, not two independent transaction defects.
194
+ error === cleanupError || error?.code === 'JD2090' && cleanupError?.code === 'JD2090'
195
+ && error.generation === cleanupError.generation
196
+ ? error : new TransactionFailure(error, cleanupError)), () => { throw error; });
197
+ const succeed = (value) => {
198
+ let committed;
199
+ try {
200
+ committed = commit();
201
+ }
202
+ catch (error) {
203
+ return fail(error);
204
+ }
205
+ return isThenable(committed) ? committed.then(() => value, fail) : value;
206
+ };
207
+ let out;
208
+ try {
209
+ out = body();
210
+ }
211
+ catch (error) {
212
+ return fail(error);
213
+ }
214
+ return isThenable(out) ? out.then(succeed, fail) : succeed(out);
215
+ }
216
+
136
217
  /**
137
218
  * Finish a raw binding into the connection contract: probe the library
138
219
  * once, assert the version floor, freeze the capability table, and
@@ -140,23 +221,118 @@ export function attempt(call, wrap) {
140
221
  *
141
222
  * The raw shape a binding supplies:
142
223
  * `{ exec(sql), prepare(sql) -> { run, get, all, iterate? }, close(),
143
- * registerFunction?, registerAggregate?, session? }` — every method
144
- * value-or-promise.
224
+ * registerFunction?, registerAggregate?, session?, backup? }` — every
225
+ * method value-or-promise. `backup` is the online-backup primitive
226
+ * triple `{ copy(path, { rate, progress }), rename(from, to),
227
+ * remove(path) }` a binding with a platform backup API and a file
228
+ * system supplies; the store's `backupTo` is built on it and never
229
+ * touches a builtin itself.
145
230
  *
146
231
  * @param {any} raw
147
232
  * @param {{ dialect: any, synchronous?: boolean, queueTimeout?: number,
233
+ * probe?: (raw: any, dialect: any, declared: any) => any,
148
234
  * declared?: { sessions?: boolean, userFunctions?: boolean,
149
235
  * deterministicIndexableFunctions?: boolean,
150
- * aggregateFunctions?: boolean } }} options
236
+ * aggregateFunctions?: boolean,
237
+ * pragmas?: readonly string[],
238
+ * maintenance?: Record<string, boolean>,
239
+ * backup?: boolean } }} options - `declared.pragmas`
240
+ * names the configuration pragmas the binding can apply (the store's
241
+ * closed set, by option name); the store refuses a request outside it
242
+ * and reads every declared one back after open. `declared.maintenance`
243
+ * narrows the maintenance operations (`checkpoint`, `integrityCheck`,
244
+ * `foreignKeyCheck`, `optimize`): every SQLite library runs them, so
245
+ * an operation is available unless the binding declares it `false`
151
246
  * @returns {any} a Connection, or a promise of one
152
247
  */
153
248
  export function openConnection(raw, options) {
154
249
  const { dialect } = options;
155
250
  const synchronous = options.synchronous === true;
156
- const declared = options.declared ?? {};
251
+ const probe = options.probe ?? sqliteProbe;
252
+ // The binding has already acquired its handle. A failed probe must
253
+ // release it here, before a store can take ownership of the connection.
254
+ /** @param {any} failure */
255
+ const failClosed = (failure) => {
256
+ /** @param {any} closeError */
257
+ const both = (closeError) => new AggregateError([failure, closeError],
258
+ 'the connection failed to open, and closing its handle failed too');
259
+ const closed = attempt(() => raw.close(), both);
260
+ return chain(closed, () => { throw failure; });
261
+ };
262
+ let opened;
263
+ try {
264
+ opened = chain(probe(raw, dialect, options.declared ?? {}), (capabilities) =>
265
+ finishConnection(raw, dialect, synchronous, capabilities,
266
+ options.queueTimeout ?? DEFAULT_QUEUE_TIMEOUT));
267
+ }
268
+ catch (failure) {
269
+ return failClosed(failure);
270
+ }
271
+ return isThenable(opened) ? opened.then(undefined, failClosed) : opened;
272
+ }
273
+
274
+ /**
275
+ * The capability answers every connection carries, each defaulted to
276
+ * the conservative one. A probe fills in what its engine and binding
277
+ * actually supply; a slot it says nothing about reads `false` rather
278
+ * than `undefined`, so a feature gate is never a `typeof` sniff at a
279
+ * call site.
280
+ * @returns {Record<string, any>}
281
+ */
282
+ export function baseCapabilities() {
283
+ return {
284
+ version: '',
285
+ jsonb: false,
286
+ generatedColumns: false,
287
+ returning: false,
288
+ upsert: false,
289
+ savepoints: false,
290
+ rtree: false,
291
+ fts: false,
292
+ sessions: false,
293
+ sessionReason: null,
294
+ worker: false,
295
+ pooling: false,
296
+ poolReaders: 0,
297
+ poolWriters: 0,
298
+ userFunctions: false,
299
+ deterministicIndexableFunctions: false,
300
+ aggregateFunctions: false,
301
+ configurablePragmas: Object.freeze([]),
302
+ backup: false,
303
+ maintenance: Object.freeze({
304
+ checkpoint: false, integrityCheck: false, foreignKeyCheck: false, optimize: false,
305
+ }),
306
+ alterTableFull: false,
307
+ statementTimeout: false,
308
+ rowEstimates: false,
309
+ lazyIteration: false,
310
+ // the job queue, the change ledger and the live registry are built
311
+ // on SQLite's own spellings; a store on another engine reports them
312
+ // absent by name rather than failing at the first statement
313
+ jobs: false,
314
+ changeCapture: false,
315
+ };
316
+ }
317
+
318
+ /**
319
+ * The SQLite probe: the library's version report and compile options,
320
+ * plus what the binding declares it can do. It is the DEFAULT probe
321
+ * because every binding this package ships is a SQLite one; a driver
322
+ * for another engine passes its own through `options.probe`, and the
323
+ * version floor asserted here goes with it.
324
+ * @param {any} raw
325
+ * @param {any} dialect
326
+ * @param {Record<string, any>} declared
327
+ * @returns {any} value-or-promise of the frozen capability table
328
+ */
329
+ export function sqliteProbe(raw, dialect, declared) {
157
330
  return chain(raw.prepare(dialect.introspect.version()), (versionStatement) =>
158
331
  chain(versionStatement.get([]), (versionRow) => {
159
332
  const version = String(versionRow.version);
333
+ // the binding's statements either carry a lazy iterator or they
334
+ // do not; the probe statement is one of them
335
+ const lazyIteration = typeof versionStatement.iterate === 'function';
160
336
  if (compareVersions(version, SQLITE_FLOOR) < 0) {
161
337
  throw new DbCompileError('JD0001',
162
338
  `the SQLite library is ${version}, below the supported floor ${SQLITE_FLOOR}`);
@@ -164,7 +340,8 @@ export function openConnection(raw, options) {
164
340
  return chain(raw.prepare(dialect.introspect.compileOptions()), (optionsStatement) =>
165
341
  chain(optionsStatement.all([]), (rows) => {
166
342
  const compiled = new Set(rows.map((row) => String(row.name)));
167
- const capabilities = Object.freeze({
343
+ return Object.freeze({
344
+ ...baseCapabilities(),
168
345
  version,
169
346
  // guaranteed by the version floor
170
347
  jsonb: true,
@@ -180,6 +357,7 @@ export function openConnection(raw, options) {
180
357
  sessions: declared.sessions === true
181
358
  && typeof raw.session === 'function'
182
359
  && compiled.has('ENABLE_SESSION'),
360
+ sessionReason: raw.sessionReason ?? null,
183
361
  userFunctions: declared.userFunctions === true
184
362
  && typeof raw.registerFunction === 'function',
185
363
  deterministicIndexableFunctions:
@@ -191,15 +369,40 @@ export function openConnection(raw, options) {
191
369
  // method AND declare it.
192
370
  aggregateFunctions: declared.aggregateFunctions === true
193
371
  && typeof raw.registerAggregate === 'function',
372
+ // the configuration pragmas the binding applies, by the
373
+ // store's option name — a request outside this list is a
374
+ // coded refusal at open, never a silently skipped member
375
+ configurablePragmas: Object.freeze([...(declared.pragmas ?? [])]),
376
+ // the online backup: the binding must supply the primitive
377
+ // triple AND declare it — the platform API and the file
378
+ // system it needs are the binding's, not the library's
379
+ backup: declared.backup === true && raw.backup !== null && typeof raw.backup === 'object'
380
+ && typeof raw.backup.copy === 'function' && typeof raw.backup.rename === 'function'
381
+ && typeof raw.backup.remove === 'function',
382
+ // the maintenance pragmas are the library's, not the
383
+ // binding's: available unless declared absent, and the
384
+ // store's report says so per operation
385
+ maintenance: Object.freeze({
386
+ checkpoint: declared.maintenance?.checkpoint !== false,
387
+ integrityCheck: declared.maintenance?.integrityCheck !== false,
388
+ foreignKeyCheck: declared.maintenance?.foreignKeyCheck !== false,
389
+ optimize: declared.maintenance?.optimize !== false,
390
+ }),
194
391
  // structural SQLite limits — stated, not worked around
195
392
  alterTableFull: false,
196
393
  // the slots every SQLite driver leaves EMPTY (no
197
394
  // interrupt, no progress handler, no estimate API)
198
395
  statementTimeout: false,
199
396
  rowEstimates: false,
397
+ // whether a cursor can pull one row at a time, or the driver
398
+ // materialises the result on the first pull (declared, so
399
+ // the cursor's own report is honest about it)
400
+ lazyIteration,
401
+ // the job queue and the change ledger are SQLite spellings,
402
+ // and every SQLite binding runs them
403
+ jobs: true,
404
+ changeCapture: true,
200
405
  });
201
- return finishConnection(raw, dialect, synchronous, capabilities,
202
- options.queueTimeout ?? DEFAULT_QUEUE_TIMEOUT);
203
406
  }));
204
407
  }));
205
408
  }
@@ -228,14 +431,26 @@ export function openConnection(raw, options) {
228
431
  * @param {any} dialect
229
432
  * @param {boolean} synchronous
230
433
  * @param {Readonly<Record<string, any>>} capabilities
434
+ * @param {number} queueTimeout
231
435
  * @returns {any}
232
436
  */
233
- function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout) {
437
+ export function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout) {
438
+ const activeIterators = new Set();
234
439
  /** Savepoint names are never reused, so a stale name can never be
235
440
  * mistaken for a live one in an error or a log. */
236
441
  let savepointSeq = 0;
237
442
  /** Whether a top-level transaction currently owns the connection. */
238
443
  let owned = false;
444
+ /**
445
+ * Whether a transaction BLOCK is open on this connection right now.
446
+ *
447
+ * Only an engine whose `SAVEPOINT` does not start a transaction needs
448
+ * the answer, and it needs it for one decision: a checkpoint asked
449
+ * for outside a block has to open the block first, because there is
450
+ * nothing for it to be a checkpoint OF. On SQLite a bare `SAVEPOINT`
451
+ * IS the block, and the flag is never read.
452
+ */
453
+ let inBlock = false;
239
454
  /**
240
455
  * Whether an owning callback is on the stack RIGHT NOW — set around the
241
456
  * synchronous extent of every transaction body, cleared the moment it
@@ -252,6 +467,7 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
252
467
  * leaking the binding's own error (or, on a build that tolerates it,
253
468
  * running against a closed handle). */
254
469
  let closed = false;
470
+ let closeResult;
255
471
  const requireOpen = () => {
256
472
  if (closed) {
257
473
  throw new DbRuntimeError('JD2063',
@@ -277,28 +493,46 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
277
493
  * instead of the scope it was handed (never — it is waiting for itself).
278
494
  * The bound turns the second case from a silent hang into a coded error
279
495
  * that names the fix, the same trade SQLite's own busy timeout makes.
496
+ * A caller that gives up while queued — an aborted signal — is taken
497
+ * off the queue and rejected with its own reason, and `work` never
498
+ * runs at all. That is the difference between cancelling a request
499
+ * and cancelling its effect.
280
500
  * @param {() => any} work
281
501
  * @param {string} what - what is waiting, for the timeout message
502
+ * @param {AbortSignal} [signal] - abandons the wait when it aborts
282
503
  * @returns {any} value-or-promise
283
504
  */
284
- const whenFree = (work, what) => {
505
+ const whenFree = (work, what, signal) => {
506
+ if (signal?.aborted === true) return Promise.reject(abortReason(signal));
285
507
  if (!owned) return work();
286
508
  return new Promise((resolve, reject) => {
287
509
  let done = false;
288
- const timer = setTimeout(() => {
510
+ /** Leave the queue without running: the turn passes to the next
511
+ * waiter when the owner releases, exactly as a timeout's does. */
512
+ const abandon = (error) => {
289
513
  done = true;
290
514
  const index = waiting.indexOf(run);
291
515
  if (index >= 0) waiting.splice(index, 1);
292
- reject(new DbCompileError('JD0012',
293
- `${what} waited ${queueTimeout}ms for the open transaction to settle. `
294
- + 'A transaction owns its connection until it commits; work that belongs '
295
- + 'INSIDE it must go through the scope the callback received '
296
- + '(scope.transaction / the store passed to your callback), not the '
297
- + 'outer connection that request waits for itself.'));
298
- }, queueTimeout);
516
+ // a wait that ends by timeout owes the signal its listener back
517
+ signal?.removeEventListener('abort', cancelled);
518
+ reject(error);
519
+ };
520
+ const timer = setTimeout(() => abandon(new DbCompileError('JD0012',
521
+ `${what} waited ${queueTimeout}ms for the open transaction to settle. `
522
+ + 'A transaction owns its connection until it commits; work that belongs '
523
+ + 'INSIDE it must go through the store or client the callback received '
524
+ + '(tx.collection / tx.entity / tx.entities / tx.transaction), not the '
525
+ + 'outer one — that request waits for itself.')),
526
+ queueTimeout);
527
+ const cancelled = () => {
528
+ clearTimeout(timer);
529
+ abandon(abortReason(signal));
530
+ };
531
+ signal?.addEventListener('abort', cancelled, { once: true });
299
532
  const run = () => {
300
533
  if (done) { release(); return; } // already rejected: pass the turn on
301
534
  clearTimeout(timer);
535
+ signal?.removeEventListener('abort', cancelled);
302
536
  done = true;
303
537
  let out;
304
538
  try {
@@ -314,33 +548,136 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
314
548
  });
315
549
  };
316
550
 
317
- /** Open one savepoint around `fn`, at whatever depth we are. `fn`
318
- * receives the scope so nested work can name itself.
551
+ /**
552
+ * Hold the connection for `fn`'s whole extent without opening a
553
+ * savepoint: what an UNRELATED caller needs so its statements cannot
554
+ * fall inside a transaction it is not part of. A store-level read or
555
+ * write is exactly that caller — it waits for the owner, owns the
556
+ * connection while it runs, and hands it on.
557
+ *
558
+ * `fn` receives a scope for the same reason a transaction callback
559
+ * does: work that must nest inside this one (a membership attach, a
560
+ * unit of work's own transaction) says so through the scope rather
561
+ * than queueing behind the caller it is part of.
319
562
  * @param {(scope: any) => any} fn
563
+ * @param {string} [what] - what is waiting, for the timeout message
564
+ * @param {AbortSignal} [signal]
320
565
  */
321
- const savepointAround = (fn) => {
322
- const name = `jaren_sp_${savepointSeq++}`;
323
- const succeed = (result) => chain(raw.exec(dialect.tx.release(name)), () => result);
324
- const fail = (error) => chain(raw.exec(dialect.tx.rollbackTo(name)), () =>
325
- chain(raw.exec(dialect.tx.release(name)), () => {
326
- throw error;
327
- }));
328
- return chain(raw.exec(dialect.tx.savepoint(name)), () => {
329
- let out;
566
+ const exclusively = (fn, what, signal) => {
567
+ requireOpen();
568
+ // a synchronous extent inside an owning callback cannot interleave
569
+ // with anything, so there is nothing to wait for
570
+ if (onStack) return fn(scopeFor());
571
+ return whenFree(() => {
572
+ owned = true;
330
573
  const wasOnStack = onStack;
331
574
  onStack = true;
575
+ let out;
332
576
  try {
333
577
  out = fn(scopeFor());
334
578
  }
335
579
  catch (error) {
336
580
  onStack = wasOnStack;
337
- return fail(error);
581
+ release();
582
+ throw error;
338
583
  }
339
- onStack = wasOnStack; // the body has returned or awaited
340
- return isThenable(out) ? out.then(succeed, fail) : succeed(out);
584
+ onStack = wasOnStack;
585
+ if (!isThenable(out)) {
586
+ release();
587
+ return out;
588
+ }
589
+ return out.then(
590
+ (value) => { release(); return value; },
591
+ (error) => { release(); throw error; });
592
+ }, what ?? 'a store-level call', signal);
593
+ };
594
+
595
+ /**
596
+ * The ONE checkpoint primitive both savepoint kinds are built on: a
597
+ * structured `transaction()` nesting opens one and settles it around
598
+ * its callback, and a scope's manual `savepoint()` opens one the
599
+ * caller settles by name. Every checkpoint wears a generated
600
+ * monotonic identifier from the same sequence, so the two kinds share
601
+ * one engine stack and no caller-supplied label ever reaches SQL.
602
+ * The handle is opaque: the store's label bookkeeping lives above it.
603
+ * @returns {any} value-or-promise of `{ name }`
604
+ */
605
+ const openCheckpoint = () => {
606
+ const name = `jaren_sp_${savepointSeq++}`;
607
+ return chain(raw.exec(dialect.tx.savepoint(name)),
608
+ () => Object.freeze({ name }));
609
+ };
610
+ /** `ROLLBACK TO` a checkpoint: the engine keeps the target active and
611
+ * discards every savepoint opened after it.
612
+ * @param {{ name: string }} checkpoint */
613
+ const rollbackToCheckpoint = (checkpoint) =>
614
+ raw.exec(dialect.tx.rollbackTo(checkpoint.name));
615
+ /** `RELEASE` a checkpoint: the engine removes the target and every
616
+ * savepoint opened after it, keeping their rows.
617
+ * @param {{ name: string }} checkpoint */
618
+ const releaseCheckpoint = (checkpoint) =>
619
+ raw.exec(dialect.tx.release(checkpoint.name));
620
+
621
+ /** Only the synchronous extent of the callback may implicitly nest. */
622
+ const callBody = (fn) => {
623
+ const wasOnStack = onStack;
624
+ onStack = true;
625
+ try {
626
+ return fn(scopeFor());
627
+ }
628
+ finally {
629
+ onStack = wasOnStack;
630
+ }
631
+ };
632
+
633
+ /**
634
+ * Own the connection inside a transaction BLOCK: `BEGIN` (or `BEGIN
635
+ * IMMEDIATE`) around `fn`, committed or rolled back as a whole.
636
+ *
637
+ * The `immediate` mode takes the write lock up front. A body
638
+ * that reads before it writes — a claim: read the record, decide,
639
+ * insert — otherwise meets the read→write upgrade `SQLITE_BUSY` the
640
+ * busy handler cannot retry when another connection commits in
641
+ * between; taking the lock first makes that wait an ordinary busy
642
+ * wait the timeout covers. Nesting inside it is savepoints, as always.
643
+ * @param {(scope: any) => any} fn
644
+ */
645
+ const blockAround = (fn, mode) => {
646
+ const begin = mode === 'immediate' ? dialect.tx.beginImmediate : dialect.tx.begin;
647
+ return chain(raw.exec(begin), () => {
648
+ let out;
649
+ const wasInBlock = inBlock;
650
+ inBlock = true;
651
+ const restore = (value) => { inBlock = wasInBlock; return value; };
652
+ try {
653
+ out = settleTransaction(() => callBody(fn),
654
+ () => raw.exec(dialect.tx.commit), () => raw.exec(dialect.tx.rollback));
655
+ }
656
+ catch (error) {
657
+ restore(undefined);
658
+ throw error;
659
+ }
660
+ return isThenable(out)
661
+ ? out.then(restore, (error) => { restore(undefined); throw error; })
662
+ : restore(out);
341
663
  });
342
664
  };
343
665
 
666
+ /** Open one savepoint around `fn`, at whatever depth we are. `fn`
667
+ * receives the scope so nested work can name itself.
668
+ * @param {(scope: any) => any} fn
669
+ */
670
+ const savepointAround = (fn) => {
671
+ // an engine that refuses a savepoint outside a transaction gets the
672
+ // block it needs; on every other one this is the same statement it
673
+ // always was
674
+ if (!inBlock && dialect.capabilities.savepointStartsTransaction !== true)
675
+ return blockAround(fn, 'deferred');
676
+ return chain(openCheckpoint(), (checkpoint) => settleTransaction(() => callBody(fn),
677
+ () => releaseCheckpoint(checkpoint),
678
+ () => chain(rollbackToCheckpoint(checkpoint), () => releaseCheckpoint(checkpoint))));
679
+ };
680
+
344
681
  /** The scope handed to a transaction callback: the owner's direct
345
682
  * access to the connection, plus nesting. Deliberately narrow —
346
683
  * registering a function or opening a change session belongs to store
@@ -352,10 +689,24 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
352
689
  /** @param {string} sql */
353
690
  exec: (sql) => { requireOpen(); return raw.exec(sql); },
354
691
  /** @param {string} sql */
355
- prepare: (sql) => { requireOpen(); return chain(raw.prepare(sql), (s) => wrapStatement(s, requireOpen)); },
692
+ prepare: (sql, metadata) => { requireOpen(); return chain(raw.prepare(sql, metadata), (s) => wrapStatement(s, requireOpen, activeIterators)); },
356
693
  /** A nested savepoint inside this transaction.
357
694
  * @param {(scope: any) => any} fn */
358
695
  transaction: (fn) => savepointAround(fn),
696
+ /** A MANUAL checkpoint at the current depth, settled by the caller
697
+ * through {@link rollbackTo}/{@link release} rather than around a
698
+ * callback. It is the same primitive structured nesting uses — one
699
+ * generated-identifier stack — so the two kinds cannot cross-release
700
+ * each other by name, and no caller-supplied label reaches SQL. */
701
+ savepoint: () => { requireOpen(); return openCheckpoint(); },
702
+ /** `ROLLBACK TO` a manual checkpoint: the target stays active; every
703
+ * savepoint opened after it is discarded with its rows.
704
+ * @param {{ name: string }} checkpoint */
705
+ rollbackTo: (checkpoint) => { requireOpen(); return rollbackToCheckpoint(checkpoint); },
706
+ /** `RELEASE` a manual checkpoint: the target and every savepoint
707
+ * opened after it are removed; their rows remain.
708
+ * @param {{ name: string }} checkpoint */
709
+ release: (checkpoint) => { requireOpen(); return releaseCheckpoint(checkpoint); },
359
710
  });
360
711
 
361
712
  return Object.freeze({
@@ -363,19 +714,18 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
363
714
  capabilities,
364
715
  dialect,
365
716
  /** @param {string} sql */
366
- // NOT gated. A statement issued while a transaction is open joins it,
367
- // because a SQLite connection has no per-statement transaction scope
368
- // and every caller inside a transaction reaches the connection this
369
- // way. Two consequences, both documented in MODEL-FORMAT §Transactions:
370
- // work inside a callback runs immediately as the owner (right), and an
371
- // UNRELATED caller's bare write on a shared store joins that
372
- // transaction and shares its fate (a single connection cannot tell the
373
- // two apart give each concurrent writer its own store to separate
374
- // them). What the gate below does guarantee is that two TRANSACTIONS
375
- // never interleave, which is what made commits report failure.
717
+ // NOT gated, because a SQLite connection has no per-statement
718
+ // transaction scope: a statement issued here joins whatever is open.
719
+ // That is right for work INSIDE the transaction, which is why the
720
+ // scope exposes the same two methods. An unrelated caller must not
721
+ // reach them it takes `exclusively` instead, which holds the
722
+ // connection for its own extent, and the store's handles do exactly
723
+ // that (MODEL-FORMAT §5.1). What the gate guarantees on top is that
724
+ // two TRANSACTIONS never interleave, which is what made commits
725
+ // report failure.
376
726
  exec: (sql) => { requireOpen(); return raw.exec(sql); },
377
727
  /** @param {string} sql */
378
- prepare: (sql) => { requireOpen(); return chain(raw.prepare(sql), (s) => wrapStatement(s, requireOpen)); },
728
+ prepare: (sql, metadata) => { requireOpen(); return chain(raw.prepare(sql, metadata), (s) => wrapStatement(s, requireOpen, activeIterators)); },
379
729
  /** Whether a transaction issued NOW would have to queue: an owner
380
730
  * holds the connection and no owning callback is on the stack (a
381
731
  * synchronous call from inside the callback nests instead). What a
@@ -383,7 +733,8 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
383
733
  get mustQueue() { return owned && !onStack; },
384
734
  /**
385
735
  * A transaction. `fn`'s value is returned; a throw rolls back exactly
386
- * this level and rethrows. No implicit retry.
736
+ * this level and rethrows. A refused COMMIT or RELEASE also rolls
737
+ * back before the next owner runs. No implicit retry.
387
738
  *
388
739
  * Two shapes, decided here rather than by the caller:
389
740
  *
@@ -401,15 +752,26 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
401
752
  * queues behind the transaction the caller is part of, and
402
753
  * {@link DEFAULT_QUEUE_TIMEOUT} turns that into `JD0012`.
403
754
  * @param {(scope: any) => any} fn
755
+ * @param {AbortSignal} [signal] - abandons a QUEUED transaction; a
756
+ * transaction that has already taken the connection runs on
757
+ * @param {'deferred' | 'immediate'} [mode] - `'immediate'` takes the
758
+ * write lock up front (a top-level transaction only; a nested call
759
+ * is a savepoint whichever mode the root chose)
404
760
  */
405
- transaction(fn) {
761
+ transaction(fn, signal, mode = 'deferred') {
406
762
  requireOpen();
407
763
  if (onStack) return savepointAround(fn);
408
764
  return whenFree(() => {
409
765
  owned = true;
410
766
  let out;
411
767
  try {
412
- out = savepointAround(fn);
768
+ // A top-level transaction is one CHECKPOINT where a savepoint
769
+ // opens a transaction of its own, and a BLOCK where it does
770
+ // not: an engine that refuses `SAVEPOINT` outside a
771
+ // transaction has to be told one is starting.
772
+ out = mode === 'immediate' || dialect.capabilities.savepointStartsTransaction !== true
773
+ ? blockAround(fn, mode)
774
+ : savepointAround(fn);
413
775
  }
414
776
  catch (error) {
415
777
  release();
@@ -422,14 +784,30 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
422
784
  return out.then(
423
785
  (value) => { release(); return value; },
424
786
  (error) => { release(); throw error; });
425
- }, 'a transaction');
787
+ }, 'a transaction', signal);
426
788
  },
789
+ /** Hold the connection for one unrelated caller's whole extent,
790
+ * without a savepoint: what a store-level read or write takes so it
791
+ * cannot fall inside a transaction it is not part of. */
792
+ exclusively,
427
793
  // idempotent: the second close is a no-op on every driver, not a
428
794
  // raw error on one and a resolved promise on another
429
795
  close: () => {
430
- if (closed) return undefined;
796
+ if (closed) return closeResult;
431
797
  closed = true;
432
- return raw.close();
798
+ // Remote hosts own cursor cleanup within their bounded shutdown.
799
+ // Waiting for a row here would postpone that deadline indefinitely.
800
+ if (raw.closeDrainsIterators === true) {
801
+ activeIterators.clear();
802
+ return closeResult = raw.close();
803
+ }
804
+ const pending = [];
805
+ for (const iterator of activeIterators) {
806
+ try { pending.push(iterator.return()); }
807
+ catch (error) { pending.push(Promise.reject(error)); }
808
+ }
809
+ if (pending.some(isThenable)) return closeResult = Promise.allSettled(pending).then(() => raw.close());
810
+ return closeResult = raw.close();
433
811
  },
434
812
  registerFunction: typeof raw.registerFunction === 'function'
435
813
  ? (name, functionOptions, fn) => { requireOpen(); return raw.registerFunction(name, functionOptions, fn); }
@@ -440,5 +818,14 @@ function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout)
440
818
  session: typeof raw.session === 'function'
441
819
  ? (table) => { requireOpen(); return raw.session(table); }
442
820
  : null,
821
+ // the backup primitives, each refused by name once the store is
822
+ // closed exactly as a statement is
823
+ backup: capabilities.backup === true
824
+ ? Object.freeze({
825
+ copy: (path, options) => { requireOpen(); return raw.backup.copy(path, options); },
826
+ rename: (from, to) => { requireOpen(); return raw.backup.rename(from, to); },
827
+ remove: (path) => { requireOpen(); return raw.backup.remove(path); },
828
+ })
829
+ : null,
443
830
  });
444
831
  }