@jarenjs/db 0.34.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 +397 -0
- package/README.md +218 -0
- package/dist/types/algebra.d.ts +133 -0
- package/dist/types/app.d.ts +49 -0
- package/dist/types/capture.d.ts +85 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/dag-job.d.ts +40 -0
- package/dist/types/ddl.d.ts +170 -0
- package/dist/types/dialect.d.ts +130 -0
- package/dist/types/dialects/sqlite.d.ts +9 -0
- package/dist/types/driver.d.ts +128 -0
- package/dist/types/drivers/bun.d.ts +47 -0
- package/dist/types/drivers/node.d.ts +37 -0
- package/dist/types/drivers/wasm.d.ts +65 -0
- package/dist/types/emit-model.d.ts +44 -0
- package/dist/types/emit.d.ts +72 -0
- package/dist/types/entity.d.ts +23 -0
- package/dist/types/errors.d.ts +165 -0
- package/dist/types/graph.d.ts +28 -0
- package/dist/types/index.d.ts +35 -0
- package/dist/types/jobs.d.ts +134 -0
- package/dist/types/live.d.ts +62 -0
- package/dist/types/migrate.d.ts +163 -0
- package/dist/types/model.d.ts +36 -0
- package/dist/types/patch-sql.d.ts +37 -0
- package/dist/types/plan.d.ts +119 -0
- package/dist/types/profile.d.ts +80 -0
- package/dist/types/query.d.ts +100 -0
- package/dist/types/residual.d.ts +50 -0
- package/dist/types/store.d.ts +53 -0
- package/dist/types/tracker.d.ts +43 -0
- package/dist/types/typed.d.ts +15 -0
- package/dist/types/types.d.ts +26 -0
- package/dist/types/udf.d.ts +70 -0
- package/dist/types/window.d.ts +52 -0
- package/docs/JOBS-FORMAT.md +218 -0
- package/docs/LIVE-FORMAT.md +348 -0
- package/docs/MIGRATION-FORMAT.md +302 -0
- package/docs/MODEL-FORMAT.md +928 -0
- package/package.json +81 -0
- package/schemas/jaren-migration.draft-07.schema.json +144 -0
- package/schemas/jaren-migration.schema.json +144 -0
- package/schemas/jaren-model.draft-07.schema.json +149 -0
- package/schemas/jaren-model.schema.json +149 -0
- package/src/algebra.js +105 -0
- package/src/app.js +108 -0
- package/src/capture.js +584 -0
- package/src/cli.js +264 -0
- package/src/dag-job.js +86 -0
- package/src/ddl.js +588 -0
- package/src/dialect.js +297 -0
- package/src/dialects/sqlite.js +175 -0
- package/src/driver.js +419 -0
- package/src/drivers/bun.js +101 -0
- package/src/drivers/node.js +93 -0
- package/src/drivers/wasm.js +178 -0
- package/src/emit-model.js +208 -0
- package/src/emit.js +393 -0
- package/src/entity.js +367 -0
- package/src/errors.js +173 -0
- package/src/graph.js +101 -0
- package/src/index.js +64 -0
- package/src/jobs.js +507 -0
- package/src/live.js +899 -0
- package/src/migrate.js +1411 -0
- package/src/model.js +476 -0
- package/src/patch-sql.js +150 -0
- package/src/plan.js +1038 -0
- package/src/profile.js +131 -0
- package/src/query.js +1010 -0
- package/src/residual.js +91 -0
- package/src/store.js +1422 -0
- package/src/tracker.js +776 -0
- package/src/typed.js +19 -0
- package/src/types.js +36 -0
- package/src/udf.js +132 -0
- package/src/window.js +125 -0
- package/types/app.d.ts +36 -0
- package/types/bun.d.ts +9 -0
- package/types/index.d.ts +592 -0
- package/types/node.d.ts +15 -0
- package/types/typed.d.ts +108 -0
- package/types/wasm.d.ts +5 -0
package/src/driver.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The driver seam: the contract every binding satisfies, the
|
|
4
|
+
* capability probe that runs once at open, and the sync-capable-async
|
|
5
|
+
* helpers the store composes with.
|
|
6
|
+
*
|
|
7
|
+
* A driver is `{ name, dialect, open(path, options) }`; `open` returns
|
|
8
|
+
* a `Connection` or a promise of one. Every connection method may
|
|
9
|
+
* return a value or a promise — the store never assumes either, and
|
|
10
|
+
* composes through {@link chain}, which does not allocate a promise
|
|
11
|
+
* when the driver answered with a value. That is what keeps the public
|
|
12
|
+
* asynchronous surface from paying twice while the synchronous fast
|
|
13
|
+
* path stays exact.
|
|
14
|
+
*
|
|
15
|
+
* The runtime builtin behind a binding is imported LAZILY inside
|
|
16
|
+
* `open()` via {@link lazyOpen} — never at module scope — because the
|
|
17
|
+
* packed-consumer gate imports every export subpath under Node *and*
|
|
18
|
+
* Bun, Bun ships no `node:sqlite`, and Node cannot resolve `bun:`
|
|
19
|
+
* specifiers. `open()` is where "this driver does not exist here"
|
|
20
|
+
* becomes the coded `JD0003` instead of a module-load crash.
|
|
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
|
|
29
|
+
* them without a contract change; pretending SQLite has them is the
|
|
30
|
+
* silent degradation this suite refuses.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { DbCompileError } from './errors.js';
|
|
34
|
+
|
|
35
|
+
/** The minimum SQLite the store accepts, asserted at open. */
|
|
36
|
+
export const SQLITE_FLOOR = '3.45.0';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How long work may wait for an open transaction to settle before it is
|
|
40
|
+
* rejected with `JD0012`. Matches the store's default busy timeout: the
|
|
41
|
+
* question "has this waited unreasonably long?" has one answer per
|
|
42
|
+
* connection whether the contention is another process (SQLite's own
|
|
43
|
+
* busy timeout) or another transaction on this one.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_QUEUE_TIMEOUT = 5000;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {any} value
|
|
49
|
+
* @returns {boolean} true when the value is a thenable
|
|
50
|
+
*/
|
|
51
|
+
export function isThenable(value) {
|
|
52
|
+
return value !== null && typeof value === 'object' && typeof value.then === 'function';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sync-capable-async composition: apply `next` to a driver result
|
|
57
|
+
* without allocating a promise when the result is already a value.
|
|
58
|
+
* @param {any} value - A driver return: a value or a promise
|
|
59
|
+
* @param {(value: any) => any} next
|
|
60
|
+
* @returns {any} `next`'s result, promise-wrapped only if the input was
|
|
61
|
+
*/
|
|
62
|
+
export function chain(value, next) {
|
|
63
|
+
return isThenable(value) ? value.then(next) : next(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lift a driver result into a promise — the ONE allocation the public
|
|
68
|
+
* asynchronous surface pays per call.
|
|
69
|
+
* @param {any} value
|
|
70
|
+
* @returns {Promise<any>}
|
|
71
|
+
*/
|
|
72
|
+
export function toPromise(value) {
|
|
73
|
+
return isThenable(value) ? value : Promise.resolve(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compare two dotted version strings numerically.
|
|
78
|
+
* @param {string} a
|
|
79
|
+
* @param {string} b
|
|
80
|
+
* @returns {number} negative when a < b, zero when equal
|
|
81
|
+
*/
|
|
82
|
+
export function compareVersions(a, b) {
|
|
83
|
+
const pa = String(a).split('.');
|
|
84
|
+
const pb = String(b).split('.');
|
|
85
|
+
for (let i = 0; i < 3; i++) {
|
|
86
|
+
const d = (Number(pa[i]) || 0) - (Number(pb[i]) || 0);
|
|
87
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load a runtime builtin lazily and hand it to the binding's adapter.
|
|
94
|
+
* A failed import — the specifier does not exist on this runtime —
|
|
95
|
+
* becomes `JD0003` carrying the loader's error as `cause`.
|
|
96
|
+
* @param {string} specifier - The builtin module specifier
|
|
97
|
+
* @param {string} reason - The `JD0003` reason for this binding
|
|
98
|
+
* @param {(mod: any, ...args: any[]) => any} use - The binding's
|
|
99
|
+
* module-to-connection adapter (a named export so the suite can
|
|
100
|
+
* exercise it with a substitute module on any runtime)
|
|
101
|
+
* @param {any[]} args - Extra arguments forwarded to `use`
|
|
102
|
+
* @returns {Promise<any>}
|
|
103
|
+
*/
|
|
104
|
+
export function lazyOpen(specifier, reason, use, args) {
|
|
105
|
+
return import(specifier).then(
|
|
106
|
+
(mod) => use(mod, ...args),
|
|
107
|
+
(cause) => {
|
|
108
|
+
throw new DbCompileError('JD0003', reason, undefined, cause);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalize a raw statement to the contract shape. A binding without a
|
|
114
|
+
* native `iterate` gets one composed over `all` — eager, but the same
|
|
115
|
+
* rows in the same order.
|
|
116
|
+
* @param {{ run: Function, get: Function, all: Function,
|
|
117
|
+
* iterate?: Function }} statement
|
|
118
|
+
* @returns {{ run: Function, get: Function, all: Function,
|
|
119
|
+
* iterate: Function }}
|
|
120
|
+
*/
|
|
121
|
+
export function wrapStatement(statement) {
|
|
122
|
+
return {
|
|
123
|
+
run: (params = []) => statement.run(params),
|
|
124
|
+
get: (params = []) => statement.get(params),
|
|
125
|
+
all: (params = []) => statement.all(params),
|
|
126
|
+
iterate: typeof statement.iterate === 'function'
|
|
127
|
+
? (params = []) => /** @type {Function} */ (statement.iterate)(params)
|
|
128
|
+
: (params = []) => chain(statement.all(params),
|
|
129
|
+
(rows) => rows[Symbol.iterator]()),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Finish a raw binding into the connection contract: probe the library
|
|
135
|
+
* once, assert the version floor, freeze the capability table, and
|
|
136
|
+
* attach the savepoint-nested `transaction`.
|
|
137
|
+
*
|
|
138
|
+
* The raw shape a binding supplies:
|
|
139
|
+
* `{ exec(sql), prepare(sql) -> { run, get, all, iterate? }, close(),
|
|
140
|
+
* registerFunction?, registerAggregate?, session? }` — every method
|
|
141
|
+
* value-or-promise.
|
|
142
|
+
*
|
|
143
|
+
* @param {any} raw
|
|
144
|
+
* @param {{ dialect: any, synchronous?: boolean, queueTimeout?: number,
|
|
145
|
+
* declared?: { sessions?: boolean, userFunctions?: boolean,
|
|
146
|
+
* deterministicIndexableFunctions?: boolean,
|
|
147
|
+
* aggregateFunctions?: boolean } }} options
|
|
148
|
+
* @returns {any} a Connection, or a promise of one
|
|
149
|
+
*/
|
|
150
|
+
export function openConnection(raw, options) {
|
|
151
|
+
const { dialect } = options;
|
|
152
|
+
const synchronous = options.synchronous === true;
|
|
153
|
+
const declared = options.declared ?? {};
|
|
154
|
+
return chain(raw.prepare(dialect.introspect.version()), (versionStatement) =>
|
|
155
|
+
chain(versionStatement.get([]), (versionRow) => {
|
|
156
|
+
const version = String(versionRow.version);
|
|
157
|
+
if (compareVersions(version, SQLITE_FLOOR) < 0) {
|
|
158
|
+
throw new DbCompileError('JD0001',
|
|
159
|
+
`the SQLite library is ${version}, below the supported floor ${SQLITE_FLOOR}`);
|
|
160
|
+
}
|
|
161
|
+
return chain(raw.prepare(dialect.introspect.compileOptions()), (optionsStatement) =>
|
|
162
|
+
chain(optionsStatement.all([]), (rows) => {
|
|
163
|
+
const compiled = new Set(rows.map((row) => String(row.name)));
|
|
164
|
+
const capabilities = Object.freeze({
|
|
165
|
+
version,
|
|
166
|
+
// guaranteed by the version floor
|
|
167
|
+
jsonb: true,
|
|
168
|
+
generatedColumns: true,
|
|
169
|
+
returning: true,
|
|
170
|
+
upsert: true,
|
|
171
|
+
savepoints: true,
|
|
172
|
+
// read from the library's compile options
|
|
173
|
+
rtree: compiled.has('ENABLE_RTREE'),
|
|
174
|
+
fts: compiled.has('ENABLE_FTS5'),
|
|
175
|
+
// the binding must expose the API AND the library must
|
|
176
|
+
// carry the extension — either alone is not the capability
|
|
177
|
+
sessions: declared.sessions === true
|
|
178
|
+
&& typeof raw.session === 'function'
|
|
179
|
+
&& compiled.has('ENABLE_SESSION'),
|
|
180
|
+
userFunctions: declared.userFunctions === true
|
|
181
|
+
&& typeof raw.registerFunction === 'function',
|
|
182
|
+
deterministicIndexableFunctions:
|
|
183
|
+
declared.deterministicIndexableFunctions === true
|
|
184
|
+
&& typeof raw.registerFunction === 'function',
|
|
185
|
+
// aggregate UDFs (`db.aggregate` step/final) — node has them,
|
|
186
|
+
// bun does not; a registry's `pushable:'aggregate'` subset is
|
|
187
|
+
// gated on this (Ring 3). The binding must expose the
|
|
188
|
+
// method AND declare it.
|
|
189
|
+
aggregateFunctions: declared.aggregateFunctions === true
|
|
190
|
+
&& typeof raw.registerAggregate === 'function',
|
|
191
|
+
// structural SQLite limits — stated, not worked around
|
|
192
|
+
alterTableFull: false,
|
|
193
|
+
// the slots every SQLite driver leaves EMPTY (no
|
|
194
|
+
// interrupt, no progress handler, no estimate API)
|
|
195
|
+
statementTimeout: false,
|
|
196
|
+
rowEstimates: false,
|
|
197
|
+
});
|
|
198
|
+
return finishConnection(raw, dialect, synchronous, capabilities,
|
|
199
|
+
options.queueTimeout ?? DEFAULT_QUEUE_TIMEOUT);
|
|
200
|
+
}));
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Assemble the frozen connection object around a probed raw binding.
|
|
206
|
+
*
|
|
207
|
+
* Transaction OWNERSHIP is the load-bearing part. A SQLite connection
|
|
208
|
+
* holds ONE savepoint stack, so two transactions that overlap in time on
|
|
209
|
+
* one connection cannot both be correct: whichever released first would
|
|
210
|
+
* release the other's savepoint with it (`RELEASE` discards everything
|
|
211
|
+
* opened after its target), leaving the second to fail with "no such
|
|
212
|
+
* savepoint" over rows it had already committed. Unique names do not help
|
|
213
|
+
* — the stack is a stack.
|
|
214
|
+
*
|
|
215
|
+
* So a top-level transaction OWNS the connection until it settles, and
|
|
216
|
+
* everything else — another top-level transaction, an ordinary read or
|
|
217
|
+
* write — waits in a FIFO. Work that genuinely belongs INSIDE the
|
|
218
|
+
* transaction says so explicitly: the callback receives a scope, and
|
|
219
|
+
* `scope.transaction()` nests through a savepoint while `scope.exec`/
|
|
220
|
+
* `scope.prepare` run immediately as the owner. That explicitness is
|
|
221
|
+
* what separates "nest me inside the open transaction" from "I am an
|
|
222
|
+
* unrelated caller, hold my work until it commits", which no implicit
|
|
223
|
+
* implicit counter can tell apart once a callback awaits.
|
|
224
|
+
* @param {any} raw
|
|
225
|
+
* @param {any} dialect
|
|
226
|
+
* @param {boolean} synchronous
|
|
227
|
+
* @param {Readonly<Record<string, any>>} capabilities
|
|
228
|
+
* @returns {any}
|
|
229
|
+
*/
|
|
230
|
+
function finishConnection(raw, dialect, synchronous, capabilities, queueTimeout) {
|
|
231
|
+
/** Savepoint names are never reused, so a stale name can never be
|
|
232
|
+
* mistaken for a live one in an error or a log. */
|
|
233
|
+
let savepointSeq = 0;
|
|
234
|
+
/** Whether a top-level transaction currently owns the connection. */
|
|
235
|
+
let owned = false;
|
|
236
|
+
/**
|
|
237
|
+
* Whether an owning callback is on the stack RIGHT NOW — set around the
|
|
238
|
+
* synchronous extent of every transaction body, cleared the moment it
|
|
239
|
+
* returns or awaits. It is the one discriminator a connection has: while
|
|
240
|
+
* a callback is still running synchronously nothing else can possibly be
|
|
241
|
+
* interleaved, so a `transaction()` arriving then is a nested call. Once
|
|
242
|
+
* the callback has awaited, the same request could be an unrelated
|
|
243
|
+
* caller, and only the scope it was handed can say otherwise.
|
|
244
|
+
*/
|
|
245
|
+
let onStack = false;
|
|
246
|
+
/** @type {Array<() => void>} FIFO of work waiting for the owner. */
|
|
247
|
+
const waiting = [];
|
|
248
|
+
|
|
249
|
+
/** Hand the connection to the next waiter, in arrival order. */
|
|
250
|
+
const release = () => {
|
|
251
|
+
owned = false;
|
|
252
|
+
const next = waiting.shift();
|
|
253
|
+
if (next !== undefined) next();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Run `work` now if the connection is free, else when it becomes free.
|
|
258
|
+
*
|
|
259
|
+
* A wait that outlives `queueTimeout` is rejected rather than left
|
|
260
|
+
* pending, because the two ways to get here look identical from inside
|
|
261
|
+
* and only one of them can ever resolve: an unrelated caller waiting
|
|
262
|
+
* out a long transaction (fine, it proceeds when the commit lands), or
|
|
263
|
+
* a transaction callback that reached back through the OUTER connection
|
|
264
|
+
* instead of the scope it was handed (never — it is waiting for itself).
|
|
265
|
+
* The bound turns the second case from a silent hang into a coded error
|
|
266
|
+
* that names the fix, the same trade SQLite's own busy timeout makes.
|
|
267
|
+
* @param {() => any} work
|
|
268
|
+
* @param {string} what - what is waiting, for the timeout message
|
|
269
|
+
* @returns {any} value-or-promise
|
|
270
|
+
*/
|
|
271
|
+
const whenFree = (work, what) => {
|
|
272
|
+
if (!owned) return work();
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
let done = false;
|
|
275
|
+
const timer = setTimeout(() => {
|
|
276
|
+
done = true;
|
|
277
|
+
const index = waiting.indexOf(run);
|
|
278
|
+
if (index >= 0) waiting.splice(index, 1);
|
|
279
|
+
reject(new DbCompileError('JD0012',
|
|
280
|
+
`${what} waited ${queueTimeout}ms for the open transaction to settle. `
|
|
281
|
+
+ 'A transaction owns its connection until it commits; work that belongs '
|
|
282
|
+
+ 'INSIDE it must go through the scope the callback received '
|
|
283
|
+
+ '(scope.transaction / the store passed to your callback), not the '
|
|
284
|
+
+ 'outer connection — that request waits for itself.'));
|
|
285
|
+
}, queueTimeout);
|
|
286
|
+
const run = () => {
|
|
287
|
+
if (done) { release(); return; } // already rejected: pass the turn on
|
|
288
|
+
clearTimeout(timer);
|
|
289
|
+
done = true;
|
|
290
|
+
let out;
|
|
291
|
+
try {
|
|
292
|
+
out = work();
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
reject(error);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
toPromise(out).then(resolve, reject);
|
|
299
|
+
};
|
|
300
|
+
waiting.push(run);
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** Open one savepoint around `fn`, at whatever depth we are. `fn`
|
|
305
|
+
* receives the scope so nested work can name itself.
|
|
306
|
+
* @param {(scope: any) => any} fn
|
|
307
|
+
*/
|
|
308
|
+
const savepointAround = (fn) => {
|
|
309
|
+
const name = `jaren_sp_${savepointSeq++}`;
|
|
310
|
+
const succeed = (result) => chain(raw.exec(dialect.tx.release(name)), () => result);
|
|
311
|
+
const fail = (error) => chain(raw.exec(dialect.tx.rollbackTo(name)), () =>
|
|
312
|
+
chain(raw.exec(dialect.tx.release(name)), () => {
|
|
313
|
+
throw error;
|
|
314
|
+
}));
|
|
315
|
+
return chain(raw.exec(dialect.tx.savepoint(name)), () => {
|
|
316
|
+
let out;
|
|
317
|
+
const wasOnStack = onStack;
|
|
318
|
+
onStack = true;
|
|
319
|
+
try {
|
|
320
|
+
out = fn(scopeFor());
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
onStack = wasOnStack;
|
|
324
|
+
return fail(error);
|
|
325
|
+
}
|
|
326
|
+
onStack = wasOnStack; // the body has returned or awaited
|
|
327
|
+
return isThenable(out) ? out.then(succeed, fail) : succeed(out);
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/** The scope handed to a transaction callback: the owner's direct
|
|
332
|
+
* access to the connection, plus nesting. Deliberately narrow —
|
|
333
|
+
* registering a function or opening a change session belongs to store
|
|
334
|
+
* setup, not to a transaction body. */
|
|
335
|
+
const scopeFor = () => Object.freeze({
|
|
336
|
+
synchronous,
|
|
337
|
+
capabilities,
|
|
338
|
+
dialect,
|
|
339
|
+
/** @param {string} sql */
|
|
340
|
+
exec: (sql) => raw.exec(sql),
|
|
341
|
+
/** @param {string} sql */
|
|
342
|
+
prepare: (sql) => chain(raw.prepare(sql), wrapStatement),
|
|
343
|
+
/** A nested savepoint inside this transaction.
|
|
344
|
+
* @param {(scope: any) => any} fn */
|
|
345
|
+
transaction: (fn) => savepointAround(fn),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return Object.freeze({
|
|
349
|
+
synchronous,
|
|
350
|
+
capabilities,
|
|
351
|
+
dialect,
|
|
352
|
+
/** @param {string} sql */
|
|
353
|
+
// NOT gated. A statement issued while a transaction is open joins it,
|
|
354
|
+
// because a SQLite connection has no per-statement transaction scope
|
|
355
|
+
// and every caller inside a transaction reaches the connection this
|
|
356
|
+
// way. Two consequences, both documented in MODEL-FORMAT §Transactions:
|
|
357
|
+
// work inside a callback runs immediately as the owner (right), and an
|
|
358
|
+
// UNRELATED caller's bare write on a shared store joins that
|
|
359
|
+
// transaction and shares its fate (a single connection cannot tell the
|
|
360
|
+
// two apart — give each concurrent writer its own store to separate
|
|
361
|
+
// them). What the gate below does guarantee is that two TRANSACTIONS
|
|
362
|
+
// never interleave, which is what made commits report failure.
|
|
363
|
+
exec: (sql) => raw.exec(sql),
|
|
364
|
+
/** @param {string} sql */
|
|
365
|
+
prepare: (sql) => chain(raw.prepare(sql), wrapStatement),
|
|
366
|
+
/**
|
|
367
|
+
* A transaction. `fn`'s value is returned; a throw rolls back exactly
|
|
368
|
+
* this level and rethrows. No implicit retry.
|
|
369
|
+
*
|
|
370
|
+
* Two shapes, decided here rather than by the caller:
|
|
371
|
+
*
|
|
372
|
+
* - **Nested**, when an owning callback is on the stack: a savepoint
|
|
373
|
+
* inside it, synchronously, exactly as before. This is the only
|
|
374
|
+
* thing it can be — synchronous code cannot interleave.
|
|
375
|
+
* - **Top level** otherwise: it takes the connection and holds it
|
|
376
|
+
* until it settles, so two transactions never share a savepoint
|
|
377
|
+
* stack. An overlapping one waits its turn instead of nesting into
|
|
378
|
+
* a stranger's rollback.
|
|
379
|
+
*
|
|
380
|
+
* An ASYNC callback that wants to nest cannot rely on the stack — it
|
|
381
|
+
* has already awaited — so it nests through the scope it was handed
|
|
382
|
+
* (`fn` receives it). Reaching back through the connection instead
|
|
383
|
+
* queues behind the transaction the caller is part of, and
|
|
384
|
+
* {@link DEFAULT_QUEUE_TIMEOUT} turns that into `JD0012`.
|
|
385
|
+
* @param {(scope: any) => any} fn
|
|
386
|
+
*/
|
|
387
|
+
transaction(fn) {
|
|
388
|
+
if (onStack) return savepointAround(fn);
|
|
389
|
+
return whenFree(() => {
|
|
390
|
+
owned = true;
|
|
391
|
+
let out;
|
|
392
|
+
try {
|
|
393
|
+
out = savepointAround(fn);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
release();
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
if (!isThenable(out)) {
|
|
400
|
+
release();
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
return out.then(
|
|
404
|
+
(value) => { release(); return value; },
|
|
405
|
+
(error) => { release(); throw error; });
|
|
406
|
+
}, 'a transaction');
|
|
407
|
+
},
|
|
408
|
+
close: () => raw.close(),
|
|
409
|
+
registerFunction: typeof raw.registerFunction === 'function'
|
|
410
|
+
? (name, functionOptions, fn) => raw.registerFunction(name, functionOptions, fn)
|
|
411
|
+
: null,
|
|
412
|
+
registerAggregate: typeof raw.registerAggregate === 'function'
|
|
413
|
+
? (name, spec) => raw.registerAggregate(name, spec)
|
|
414
|
+
: null,
|
|
415
|
+
session: typeof raw.session === 'function'
|
|
416
|
+
? (table) => raw.session(table)
|
|
417
|
+
: null,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The Bun binding: `bun:sqlite` behind the driver contract. The
|
|
4
|
+
* builtin is imported lazily inside `open()` — never at module scope —
|
|
5
|
+
* so this module itself loads under any runtime; on a runtime that
|
|
6
|
+
* cannot resolve `bun:` specifiers the open fails with the coded
|
|
7
|
+
* `JD0003`.
|
|
8
|
+
*
|
|
9
|
+
* Probed reality this binding declares rather than papers over:
|
|
10
|
+
* `bun:sqlite`'s `Database` exposes no `function`, no `aggregate` and
|
|
11
|
+
* no `createSession` — on Bun the UDF hatch does not exist and there
|
|
12
|
+
* is no session-based change capture. The capability table says so.
|
|
13
|
+
*
|
|
14
|
+
* What it DOES expose is a native lazy row iterator, forwarded below.
|
|
15
|
+
* Without it the driver's generic fallback composes a cursor over
|
|
16
|
+
* `all()`, which materialises every row first — so a query that streams
|
|
17
|
+
* on Node would spike memory in a compiled Bun binary, on the same code
|
|
18
|
+
* and the same data. A cursor that is not lazy is not a cursor.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { lazyOpen, openConnection } from '../driver.js';
|
|
22
|
+
import { sqliteDialect } from '../dialects/sqlite.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adapt an already-constructed `bun:sqlite` `Database` (or any object
|
|
26
|
+
* with its shape) into a probed connection. Exported so the adapter is
|
|
27
|
+
* exercisable without the builtin.
|
|
28
|
+
* @param {any} db - A Bun `Database`-shaped database
|
|
29
|
+
* @param {{ queueTimeout?: number }} [options]
|
|
30
|
+
* @returns {any} a Connection, or a promise of one
|
|
31
|
+
*/
|
|
32
|
+
export function adaptBunDatabase(db, options) {
|
|
33
|
+
const raw = {
|
|
34
|
+
/** @param {string} sql */
|
|
35
|
+
exec: (sql) => db.run(sql),
|
|
36
|
+
/** @param {string} sql */
|
|
37
|
+
prepare: (sql) => {
|
|
38
|
+
const statement = db.prepare(sql);
|
|
39
|
+
return {
|
|
40
|
+
run: (params = []) => statement.run(...params),
|
|
41
|
+
// the driver contract says a missing row reads UNDEFINED;
|
|
42
|
+
// bun:sqlite answers null — normalize at the seam, or every
|
|
43
|
+
// create-or-verify and absence check misfires
|
|
44
|
+
get: (params = []) => statement.get(...params) ?? undefined,
|
|
45
|
+
all: (params = []) => statement.all(...params),
|
|
46
|
+
// the native lazy iterator when this build has one; the
|
|
47
|
+
// driver-level wrapper composes one over `all` when it does not,
|
|
48
|
+
// and `iterate` stays absent here so that fallback is reached
|
|
49
|
+
...(typeof statement.iterate === 'function'
|
|
50
|
+
? { iterate: (params = []) => statement.iterate(...params) }
|
|
51
|
+
: undefined),
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
close: () => db.close(),
|
|
55
|
+
};
|
|
56
|
+
return openConnection(raw, {
|
|
57
|
+
dialect: sqliteDialect,
|
|
58
|
+
synchronous: true,
|
|
59
|
+
queueTimeout: options?.queueTimeout,
|
|
60
|
+
declared: {
|
|
61
|
+
sessions: false,
|
|
62
|
+
userFunctions: false,
|
|
63
|
+
deterministicIndexableFunctions: false,
|
|
64
|
+
aggregateFunctions: false,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Construct and adapt the database from a loaded `bun:sqlite` module.
|
|
71
|
+
* Exported so the whole open path runs under any runtime with a
|
|
72
|
+
* substitute module.
|
|
73
|
+
* @param {any} mod - The `bun:sqlite` module (or a substitute)
|
|
74
|
+
* @param {string} path
|
|
75
|
+
* @param {{ readOnly?: boolean, queueTimeout?: number }} [options]
|
|
76
|
+
* @returns {any}
|
|
77
|
+
*/
|
|
78
|
+
export function fromBunModule(mod, path, options) {
|
|
79
|
+
return adaptBunDatabase(options?.readOnly === true
|
|
80
|
+
? new mod.Database(path, { readonly: true })
|
|
81
|
+
: new mod.Database(path), options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The Bun driver: `{ name, dialect, open }` over `bun:sqlite`.
|
|
86
|
+
* @returns {any}
|
|
87
|
+
*/
|
|
88
|
+
export function bunDriver() {
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
name: 'bun-sqlite',
|
|
91
|
+
dialect: sqliteDialect,
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} path
|
|
94
|
+
* @param {{ readOnly?: boolean, queueTimeout?: number }} [options]
|
|
95
|
+
* @returns {Promise<any>}
|
|
96
|
+
*/
|
|
97
|
+
open: (path, options) => lazyOpen('bun:sqlite',
|
|
98
|
+
"the Bun SQLite binding ('bun:sqlite') is unavailable on this runtime",
|
|
99
|
+
fromBunModule, [path, options]),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The Node binding: `node:sqlite` behind the driver contract.
|
|
4
|
+
* The builtin is imported lazily inside `open()` — never at module
|
|
5
|
+
* scope — so this module itself loads under any runtime; on a runtime
|
|
6
|
+
* without `node:sqlite` the open fails with the coded `JD0003`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { lazyOpen, openConnection } from '../driver.js';
|
|
10
|
+
import { sqliteDialect } from '../dialects/sqlite.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adapt an already-constructed `node:sqlite` `DatabaseSync` (or any
|
|
14
|
+
* object with its shape) into a probed connection. Exported so the
|
|
15
|
+
* adapter is exercisable without the builtin.
|
|
16
|
+
* @param {any} db - A `DatabaseSync`-shaped database
|
|
17
|
+
* @param {{ queueTimeout?: number }} [options]
|
|
18
|
+
* @returns {any} a Connection, or a promise of one
|
|
19
|
+
*/
|
|
20
|
+
export function adaptNodeDatabase(db, options) {
|
|
21
|
+
const raw = {
|
|
22
|
+
/** @param {string} sql */
|
|
23
|
+
exec: (sql) => db.exec(sql),
|
|
24
|
+
/** @param {string} sql */
|
|
25
|
+
prepare: (sql) => {
|
|
26
|
+
const statement = db.prepare(sql);
|
|
27
|
+
return {
|
|
28
|
+
run: (params = []) => statement.run(...params),
|
|
29
|
+
get: (params = []) => statement.get(...params),
|
|
30
|
+
all: (params = []) => statement.all(...params),
|
|
31
|
+
iterate: (params = []) => statement.iterate(...params),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
close: () => db.close(),
|
|
35
|
+
registerFunction: (name, options, fn) => db.function(name, options, fn),
|
|
36
|
+
registerAggregate: (name, spec) => db.aggregate(name, spec),
|
|
37
|
+
session: (table) => (table === undefined
|
|
38
|
+
? db.createSession()
|
|
39
|
+
: db.createSession({ table })),
|
|
40
|
+
};
|
|
41
|
+
return openConnection(raw, {
|
|
42
|
+
dialect: sqliteDialect,
|
|
43
|
+
synchronous: true,
|
|
44
|
+
queueTimeout: options?.queueTimeout,
|
|
45
|
+
declared: {
|
|
46
|
+
sessions: true,
|
|
47
|
+
userFunctions: true,
|
|
48
|
+
deterministicIndexableFunctions: true,
|
|
49
|
+
aggregateFunctions: true,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Construct and adapt the database from a loaded `node:sqlite` module.
|
|
56
|
+
* The seam {@link nodeDriver} feeds through `lazyOpen`; exported so the
|
|
57
|
+
* whole open path runs under any runtime with a substitute module.
|
|
58
|
+
* @param {any} mod - The `node:sqlite` module (or a substitute)
|
|
59
|
+
* @param {string} path
|
|
60
|
+
* @param {{ timeout?: number, readOnly?: boolean,
|
|
61
|
+
* queueTimeout?: number }} [options]
|
|
62
|
+
* @returns {any}
|
|
63
|
+
*/
|
|
64
|
+
export function fromNodeModule(mod, path, options) {
|
|
65
|
+
/** @type {any} */
|
|
66
|
+
const open = {};
|
|
67
|
+
if (options?.timeout !== undefined) open.timeout = options.timeout;
|
|
68
|
+
if (options?.readOnly === true) open.readOnly = true;
|
|
69
|
+
const db = Object.keys(open).length > 0
|
|
70
|
+
? new mod.DatabaseSync(path, open)
|
|
71
|
+
: new mod.DatabaseSync(path);
|
|
72
|
+
return adaptNodeDatabase(db, options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The Node driver: `{ name, dialect, open }` over `node:sqlite`.
|
|
77
|
+
* @returns {any}
|
|
78
|
+
*/
|
|
79
|
+
export function nodeDriver() {
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
name: 'node-sqlite',
|
|
82
|
+
dialect: sqliteDialect,
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} path
|
|
85
|
+
* @param {{ timeout?: number, readOnly?: boolean,
|
|
86
|
+
* queueTimeout?: number }} [options]
|
|
87
|
+
* @returns {Promise<any>}
|
|
88
|
+
*/
|
|
89
|
+
open: (path, options) => lazyOpen('node:sqlite',
|
|
90
|
+
"the Node SQLite binding ('node:sqlite') is unavailable on this runtime",
|
|
91
|
+
fromNodeModule, [path, options]),
|
|
92
|
+
});
|
|
93
|
+
}
|