@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.
Files changed (83) hide show
  1. package/ARCHITECTURE.md +397 -0
  2. package/README.md +218 -0
  3. package/dist/types/algebra.d.ts +133 -0
  4. package/dist/types/app.d.ts +49 -0
  5. package/dist/types/capture.d.ts +85 -0
  6. package/dist/types/cli.d.ts +2 -0
  7. package/dist/types/dag-job.d.ts +40 -0
  8. package/dist/types/ddl.d.ts +170 -0
  9. package/dist/types/dialect.d.ts +130 -0
  10. package/dist/types/dialects/sqlite.d.ts +9 -0
  11. package/dist/types/driver.d.ts +128 -0
  12. package/dist/types/drivers/bun.d.ts +47 -0
  13. package/dist/types/drivers/node.d.ts +37 -0
  14. package/dist/types/drivers/wasm.d.ts +65 -0
  15. package/dist/types/emit-model.d.ts +44 -0
  16. package/dist/types/emit.d.ts +72 -0
  17. package/dist/types/entity.d.ts +23 -0
  18. package/dist/types/errors.d.ts +165 -0
  19. package/dist/types/graph.d.ts +28 -0
  20. package/dist/types/index.d.ts +35 -0
  21. package/dist/types/jobs.d.ts +134 -0
  22. package/dist/types/live.d.ts +62 -0
  23. package/dist/types/migrate.d.ts +163 -0
  24. package/dist/types/model.d.ts +36 -0
  25. package/dist/types/patch-sql.d.ts +37 -0
  26. package/dist/types/plan.d.ts +119 -0
  27. package/dist/types/profile.d.ts +80 -0
  28. package/dist/types/query.d.ts +100 -0
  29. package/dist/types/residual.d.ts +50 -0
  30. package/dist/types/store.d.ts +53 -0
  31. package/dist/types/tracker.d.ts +43 -0
  32. package/dist/types/typed.d.ts +15 -0
  33. package/dist/types/types.d.ts +26 -0
  34. package/dist/types/udf.d.ts +70 -0
  35. package/dist/types/window.d.ts +52 -0
  36. package/docs/JOBS-FORMAT.md +218 -0
  37. package/docs/LIVE-FORMAT.md +348 -0
  38. package/docs/MIGRATION-FORMAT.md +302 -0
  39. package/docs/MODEL-FORMAT.md +928 -0
  40. package/package.json +81 -0
  41. package/schemas/jaren-migration.draft-07.schema.json +144 -0
  42. package/schemas/jaren-migration.schema.json +144 -0
  43. package/schemas/jaren-model.draft-07.schema.json +149 -0
  44. package/schemas/jaren-model.schema.json +149 -0
  45. package/src/algebra.js +105 -0
  46. package/src/app.js +108 -0
  47. package/src/capture.js +584 -0
  48. package/src/cli.js +264 -0
  49. package/src/dag-job.js +86 -0
  50. package/src/ddl.js +588 -0
  51. package/src/dialect.js +297 -0
  52. package/src/dialects/sqlite.js +175 -0
  53. package/src/driver.js +419 -0
  54. package/src/drivers/bun.js +101 -0
  55. package/src/drivers/node.js +93 -0
  56. package/src/drivers/wasm.js +178 -0
  57. package/src/emit-model.js +208 -0
  58. package/src/emit.js +393 -0
  59. package/src/entity.js +367 -0
  60. package/src/errors.js +173 -0
  61. package/src/graph.js +101 -0
  62. package/src/index.js +64 -0
  63. package/src/jobs.js +507 -0
  64. package/src/live.js +899 -0
  65. package/src/migrate.js +1411 -0
  66. package/src/model.js +476 -0
  67. package/src/patch-sql.js +150 -0
  68. package/src/plan.js +1038 -0
  69. package/src/profile.js +131 -0
  70. package/src/query.js +1010 -0
  71. package/src/residual.js +91 -0
  72. package/src/store.js +1422 -0
  73. package/src/tracker.js +776 -0
  74. package/src/typed.js +19 -0
  75. package/src/types.js +36 -0
  76. package/src/udf.js +132 -0
  77. package/src/window.js +125 -0
  78. package/types/app.d.ts +36 -0
  79. package/types/bun.d.ts +9 -0
  80. package/types/index.d.ts +592 -0
  81. package/types/node.d.ts +15 -0
  82. package/types/typed.d.ts +108 -0
  83. 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
+ }