@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.
- package/ARCHITECTURE.md +412 -56
- package/README.md +600 -57
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +169 -20
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +752 -64
- package/docs/REPLICATION-FORMAT.md +208 -0
- package/package.json +21 -7
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
- package/schemas/jaren-replication-snapshot.schema.json +83 -0
- package/schemas/jaren-replication.draft-07.schema.json +82 -0
- package/schemas/jaren-replication.schema.json +82 -0
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +230 -47
- package/src/cli.js +165 -59
- package/src/cursor.js +417 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- package/src/dialect.js +268 -113
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +244 -11
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +448 -61
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +17 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit.js +339 -48
- package/src/entity.js +20 -22
- package/src/errors.js +430 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/live-join.js +250 -0
- package/src/live-nested.js +120 -0
- package/src/live.js +18 -4
- package/src/logical-rows.js +90 -0
- package/src/maintenance.js +175 -0
- package/src/migrate.js +248 -181
- package/src/model.js +68 -0
- package/src/plan.js +1119 -138
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1634 -323
- package/src/replication-format.js +115 -0
- package/src/replication.js +332 -0
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1567 -273
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1158 -27
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +69 -2
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +27 -4
- 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 —
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
/**
|
|
318
|
-
*
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
581
|
+
release();
|
|
582
|
+
throw error;
|
|
338
583
|
}
|
|
339
|
-
onStack = wasOnStack;
|
|
340
|
-
|
|
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
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
// two
|
|
374
|
-
//
|
|
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.
|
|
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
|
-
|
|
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
|
|
796
|
+
if (closed) return closeResult;
|
|
431
797
|
closed = true;
|
|
432
|
-
|
|
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
|
}
|