@jarenjs/db 0.49.2 → 0.66.1

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 (110) hide show
  1. package/ARCHITECTURE.md +420 -71
  2. package/README.md +711 -79
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +309 -45
  5. package/docs/LIVE-FORMAT.md +156 -19
  6. package/docs/MIGRATION-FORMAT.md +247 -40
  7. package/docs/MODEL-FORMAT.md +968 -86
  8. package/package.json +21 -8
  9. package/schemas/jaren-migration.draft-07.schema.json +73 -0
  10. package/schemas/jaren-migration.schema.json +73 -0
  11. package/schemas/jaren-model.draft-07.schema.json +224 -162
  12. package/schemas/jaren-model.schema.json +224 -162
  13. package/src/algebra.js +227 -9
  14. package/src/backup.js +161 -0
  15. package/src/cancellation.js +48 -0
  16. package/src/capture.js +255 -44
  17. package/src/cli.js +337 -50
  18. package/src/cursor.js +411 -0
  19. package/src/dag-job.js +154 -21
  20. package/src/ddl.js +125 -11
  21. package/src/dialect.js +267 -112
  22. package/src/dialects/expression-read.js +158 -0
  23. package/src/dialects/postgres.js +618 -0
  24. package/src/dialects/rtree-ddl.js +129 -0
  25. package/src/dialects/sqlite.js +245 -12
  26. package/src/document-files.js +311 -0
  27. package/src/document-steps.js +422 -0
  28. package/src/documents.js +335 -0
  29. package/src/driver.js +503 -69
  30. package/src/drivers/bun.js +37 -1
  31. package/src/drivers/indexeddb-snapshot.js +149 -0
  32. package/src/drivers/node-pool.js +11 -0
  33. package/src/drivers/node-worker-endpoint.js +105 -0
  34. package/src/drivers/node-worker.js +204 -0
  35. package/src/drivers/node.js +41 -7
  36. package/src/drivers/postgres.js +331 -0
  37. package/src/drivers/wasm-oo1.js +97 -0
  38. package/src/drivers/wasm-session.js +67 -0
  39. package/src/drivers/wasm.js +18 -83
  40. package/src/drivers/worker-pool.js +183 -0
  41. package/src/drivers/worker-protocol.js +79 -0
  42. package/src/drivers/worker-queue.js +60 -0
  43. package/src/emit-model.js +14 -0
  44. package/src/emit.js +349 -51
  45. package/src/entity.js +102 -59
  46. package/src/errors.js +430 -2
  47. package/src/expression.js +284 -0
  48. package/src/graph.js +64 -8
  49. package/src/index.js +48 -19
  50. package/src/introspect.js +583 -0
  51. package/src/jobs.js +870 -99
  52. package/src/json-bytes.js +58 -0
  53. package/src/live-time.js +12 -3
  54. package/src/live.js +11 -1
  55. package/src/maintenance.js +175 -0
  56. package/src/migrate.js +606 -333
  57. package/src/model.js +241 -8
  58. package/src/plan.js +1238 -160
  59. package/src/pragmas.js +314 -0
  60. package/src/profile.js +151 -3
  61. package/src/query.js +1748 -312
  62. package/src/residual.js +17 -0
  63. package/src/series.js +12 -4
  64. package/src/store.js +1672 -276
  65. package/src/tracker.js +367 -68
  66. package/src/udf.js +88 -7
  67. package/types/index.d.ts +1246 -32
  68. package/types/node-pool.d.ts +28 -0
  69. package/types/node-worker.d.ts +54 -0
  70. package/types/node.d.ts +72 -3
  71. package/types/postgres.d.ts +46 -0
  72. package/types/typed.d.ts +81 -3
  73. package/types/wasm.d.ts +21 -0
  74. package/dist/types/algebra.d.ts +0 -230
  75. package/dist/types/app.d.ts +0 -49
  76. package/dist/types/capture.d.ts +0 -85
  77. package/dist/types/cli.d.ts +0 -2
  78. package/dist/types/dag-job.d.ts +0 -40
  79. package/dist/types/ddl.d.ts +0 -229
  80. package/dist/types/derive.d.ts +0 -250
  81. package/dist/types/dialect.d.ts +0 -154
  82. package/dist/types/dialects/sqlite.d.ts +0 -9
  83. package/dist/types/driver.d.ts +0 -110
  84. package/dist/types/drivers/bun.d.ts +0 -47
  85. package/dist/types/drivers/node.d.ts +0 -37
  86. package/dist/types/drivers/wasm.d.ts +0 -65
  87. package/dist/types/emit-model.d.ts +0 -44
  88. package/dist/types/emit.d.ts +0 -75
  89. package/dist/types/entity.d.ts +0 -23
  90. package/dist/types/errors.d.ts +0 -170
  91. package/dist/types/graph.d.ts +0 -28
  92. package/dist/types/index.d.ts +0 -37
  93. package/dist/types/jobs.d.ts +0 -140
  94. package/dist/types/knn.d.ts +0 -69
  95. package/dist/types/live-time.d.ts +0 -141
  96. package/dist/types/live.d.ts +0 -64
  97. package/dist/types/migrate.d.ts +0 -170
  98. package/dist/types/model.d.ts +0 -36
  99. package/dist/types/patch-sql.d.ts +0 -37
  100. package/dist/types/plan.d.ts +0 -142
  101. package/dist/types/profile.d.ts +0 -80
  102. package/dist/types/query.d.ts +0 -112
  103. package/dist/types/residual.d.ts +0 -64
  104. package/dist/types/series.d.ts +0 -227
  105. package/dist/types/store.d.ts +0 -60
  106. package/dist/types/tracker.d.ts +0 -43
  107. package/dist/types/typed.d.ts +0 -15
  108. package/dist/types/types.d.ts +0 -26
  109. package/dist/types/udf.d.ts +0 -75
  110. package/dist/types/window.d.ts +0 -52
@@ -0,0 +1,331 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The PostgreSQL binding: an INJECTED client behind the driver
4
+ * contract, and the PostgreSQL dialect re-exported beside it.
5
+ *
6
+ * This module imports no PostgreSQL package and no runtime builtin.
7
+ * The host supplies a connection source — anything with
8
+ * `connect()` answering `{ query(text, values), release?() }`, which
9
+ * `pg.Pool` is verbatim — and this driver adapts it. That is the whole
10
+ * of D1's "never call a specific npm client outside the adapter seam":
11
+ * there is no seam to leak through, because there is no import.
12
+ *
13
+ * ONE client is acquired at open and held until `close()`, then
14
+ * released exactly once. That is not a simplification: a connection
15
+ * owns one savepoint stack and one transaction, and a store that took a
16
+ * different pooled client per statement would have `BEGIN` on one and
17
+ * `COMMIT` on another.
18
+ *
19
+ * The three normalizations, all forced by the wire rather than chosen:
20
+ *
21
+ * - `int8` and `numeric` arrive as STRINGS, because they can exceed
22
+ * what a double holds. The store's contract is JavaScript numbers —
23
+ * the same ceiling SQLite's INTEGER has — so they are converted, and
24
+ * a value past 2^53 loses precision here exactly as it would there.
25
+ * - `json`/`jsonb` arrive PARSED, because the client's type parsers
26
+ * are the host's configuration. Every document read is already
27
+ * `::text` (the dialect's `jsonText`), but a graph load's built
28
+ * object is not, and the row decoder reads text. One re-encode, and
29
+ * the driver is correct whatever the host configured.
30
+ * - a JavaScript boolean is bound as 1 or 0, because a boolean MEMBER
31
+ * is 1 or 0 in this mapping (the dialect's `typeFor` says
32
+ * `smallint`) and `pg` would otherwise send `'true'`.
33
+ */
34
+
35
+ import { chain, openConnection, baseCapabilities } from '../driver.js';
36
+ import { postgresDialect } from '../dialects/postgres.js';
37
+ import { DbCompileError } from '../errors.js';
38
+
39
+ export { postgresDialect, IDENTIFIER_BYTES } from '../dialects/postgres.js';
40
+
41
+ /** The minimum PostgreSQL this store accepts, as the server's own
42
+ * `server_version_num` spells it: 16.0. */
43
+ export const POSTGRES_FLOOR = 160000;
44
+
45
+ /** Types the wire hands back as text because they can exceed a double,
46
+ * and the two it hands back as text for width alone. The store's
47
+ * contract is JavaScript numbers throughout. */
48
+ const NUMERIC_OIDS = new Set([20, 21, 23, 26, 700, 701, 1700]);
49
+ /** `json` and `jsonb`: parsed by the client unless the host said
50
+ * otherwise, and the row decoder reads text. */
51
+ const JSON_OIDS = new Set([114, 3802]);
52
+ /** `bool`, which every shared form compares against 1 and 0. */
53
+ const BOOL_OID = 16;
54
+
55
+ /**
56
+ * The next server-side statement name, monotonic across this PROCESS
57
+ * rather than across one adapter.
58
+ *
59
+ * A prepared statement belongs to the SESSION, and a pooled client's
60
+ * session outlives the store that borrowed it: a second store handed
61
+ * the same physical connection would otherwise re-use `jaren_s1` for
62
+ * different SQL, and PostgreSQL refuses that by name. One counter for
63
+ * every adapter in the process makes the collision impossible without
64
+ * the driver having to know which client the pool will hand it.
65
+ */
66
+ let statementSequence = 0;
67
+
68
+ /**
69
+ * One column's converter, chosen once per result from the type the
70
+ * server declared rather than per value from what it looks like.
71
+ * @param {number} dataTypeID
72
+ * @returns {((value: any) => any) | null} `null` where the wire value
73
+ * is already what the store reads
74
+ */
75
+ function converterFor(dataTypeID) {
76
+ if (NUMERIC_OIDS.has(dataTypeID)) {
77
+ return (value) => (value === null || value === undefined ? value : Number(value));
78
+ }
79
+ if (dataTypeID === BOOL_OID) {
80
+ return (value) => (value === null || value === undefined ? value : (value ? 1 : 0));
81
+ }
82
+ if (JSON_OIDS.has(dataTypeID)) {
83
+ return (value) => (value === null || value === undefined || typeof value === 'string'
84
+ ? value : JSON.stringify(value));
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Normalize a result's rows in place. Nothing is copied where no column
91
+ * needs converting, which is the common case: a document read is
92
+ * already text and a key is already a string.
93
+ * @param {{ rows?: any[], fields?: { name: string, dataTypeID: number }[] }} result
94
+ * @returns {any[]}
95
+ */
96
+ function normalizeRows(result) {
97
+ const rows = result.rows ?? [];
98
+ const fields = result.fields ?? [];
99
+ /** @type {{ name: string, convert: (value: any) => any }[]} */
100
+ const converters = [];
101
+ for (const field of fields) {
102
+ const convert = converterFor(field.dataTypeID);
103
+ if (convert !== null) converters.push({ name: field.name, convert });
104
+ }
105
+ if (converters.length === 0) return rows;
106
+ for (const row of rows) {
107
+ for (const { name, convert } of converters) row[name] = convert(row[name]);
108
+ }
109
+ return rows;
110
+ }
111
+
112
+ /**
113
+ * One bound value, in the encoding the wire needs.
114
+ * @param {any} value
115
+ * @returns {any}
116
+ */
117
+ function encodeParam(value) {
118
+ if (value === undefined) return null;
119
+ if (typeof value === 'boolean') return value ? 1 : 0;
120
+ if (typeof value === 'bigint') return String(value);
121
+ // a packed vector is bytes; a client that recognizes only its
122
+ // platform's buffer type would otherwise stringify the view
123
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
124
+ const Buf = /** @type {any} */ (globalThis).Buffer;
125
+ if (Buf !== undefined && !Buf.isBuffer(value)) {
126
+ const view = /** @type {any} */ (value);
127
+ return Buf.from(view.buffer, view.byteOffset, view.byteLength);
128
+ }
129
+ }
130
+ return value;
131
+ }
132
+
133
+ /**
134
+ * The PostgreSQL probe. It replaces the SQLite one wholesale — there is
135
+ * no `sqlite_version()` and no compile-options table here — and every
136
+ * capability it does not name reads `false` from
137
+ * {@link baseCapabilities}, which is what makes the store's refusals
138
+ * fire by name rather than at the first statement.
139
+ * @param {any} raw
140
+ * @returns {any} value-or-promise of the frozen capability table
141
+ */
142
+ export function postgresProbe(raw) {
143
+ return chain(raw.prepare("SELECT current_setting('server_version_num') AS num, "
144
+ + "current_setting('server_version') AS version"), (statement) =>
145
+ chain(statement.get([]), (row) => {
146
+ const num = Number(row?.num);
147
+ const version = String(row?.version ?? '');
148
+ if (!Number.isFinite(num) || num < POSTGRES_FLOOR) {
149
+ throw new DbCompileError('JD0001',
150
+ `the PostgreSQL server is ${version || num}, below the supported floor 16.0`);
151
+ }
152
+ return Object.freeze({
153
+ ...baseCapabilities(),
154
+ version,
155
+ jsonb: true,
156
+ generatedColumns: true,
157
+ returning: true,
158
+ upsert: true,
159
+ savepoints: true,
160
+ // a real ALTER, which is the one structural thing this engine
161
+ // has and SQLite does not
162
+ alterTableFull: true,
163
+ // no cursor without a second package; the store's cursor says it
164
+ // buffers rather than pretending it streams
165
+ lazyIteration: false,
166
+ // the closed configuration vocabulary is SQLite's; a PostgreSQL
167
+ // server is configured by its operator
168
+ configurablePragmas: Object.freeze([]),
169
+ // every maintenance operation IS a SQLite pragma
170
+ maintenance: Object.freeze({
171
+ checkpoint: false, integrityCheck: false, foreignKeyCheck: false, optimize: false,
172
+ }),
173
+ // the job queue and the change ledger write their own SQLite
174
+ // statements; both refuse by name at open here
175
+ jobs: false,
176
+ changeCapture: false,
177
+ });
178
+ }));
179
+ }
180
+
181
+ /**
182
+ * Adapt one acquired client into the raw binding contract.
183
+ *
184
+ * Statements are PREPARED by name, so the server plans each one once
185
+ * and the store's statement caches are worth having. The one hazard is
186
+ * a cached plan whose result type changed under it — a migration that
187
+ * added a column to a table a live statement selects `*` from — which
188
+ * PostgreSQL reports as `0A000`; the name is dropped and the statement
189
+ * re-runs unnamed, so the caller sees a slower call rather than an
190
+ * error it could do nothing about.
191
+ * @param {{ query: Function, release?: Function }} client
192
+ * @param {{ onClose?: () => any }} [options]
193
+ * @returns {any} the raw binding for {@link openConnection}
194
+ */
195
+ export function adaptPostgresClient(client, options = undefined) {
196
+ /** @type {Map<string, string>} sql -> the server-side statement name */
197
+ const names = new Map();
198
+ /** The names the server actually holds — a name is only prepared by
199
+ * its first execution, and deallocating one it never saw is an error
200
+ * of its own. */
201
+ const prepared = new Set();
202
+ /** Released exactly once: on success, on failure, and on a second
203
+ * `close()`, which the connection contract makes a no-op anyway. */
204
+ let released = false;
205
+
206
+ const run = (sql, params) => {
207
+ const values = params.map(encodeParam);
208
+ const name = names.get(sql);
209
+ const attempt = name === undefined
210
+ ? client.query(sql, values)
211
+ : client.query({ name, text: sql, values });
212
+ return Promise.resolve(attempt).then((result) => {
213
+ if (name !== undefined) prepared.add(name);
214
+ return result;
215
+ }, (error) => {
216
+ // the cached plan's result type changed under it
217
+ if (error?.code !== '0A000' || name === undefined) throw error;
218
+ names.delete(sql);
219
+ prepared.delete(name);
220
+ return client.query(sql, values);
221
+ });
222
+ };
223
+
224
+ return {
225
+ /** @param {string} sql */
226
+ exec: (sql) => Promise.resolve(client.query(sql)).then(() => undefined),
227
+ /** @param {string} sql */
228
+ prepare: (sql) => {
229
+ if (!names.has(sql)) {
230
+ statementSequence += 1;
231
+ names.set(sql, `jaren_s${statementSequence}`);
232
+ }
233
+ return {
234
+ run: (params = []) => run(sql, params)
235
+ .then((result) => ({ changes: result.rowCount ?? 0 })),
236
+ get: (params = []) => run(sql, params).then((result) => normalizeRows(result)[0]),
237
+ all: (params = []) => run(sql, params).then((result) => normalizeRows(result)),
238
+ };
239
+ },
240
+ close: () => {
241
+ if (released) return Promise.resolve(undefined);
242
+ released = true;
243
+ // the session goes back to the pool without this store's
244
+ // statements on it. A failure here is not the caller's to handle:
245
+ // the client is being released either way, and a leaked plan is
246
+ // memory rather than a wrong answer
247
+ const deallocate = [...prepared].reduce(
248
+ (chained, name) => chained.then(
249
+ () => client.query(`DEALLOCATE "${name}"`), () => undefined).then(
250
+ () => undefined, () => undefined),
251
+ Promise.resolve(undefined));
252
+ return deallocate
253
+ .then(() => options?.onClose?.())
254
+ .then(() => client.release?.(), () => client.release?.())
255
+ .then(() => undefined);
256
+ },
257
+ };
258
+ }
259
+
260
+ /**
261
+ * The PostgreSQL driver over an injected connection source.
262
+ *
263
+ * The source is anything with `connect()` answering a client — a
264
+ * `pg.Pool` is one without adaptation, and a single client becomes one
265
+ * with `{ connect: () => client }`. The store holds the client it
266
+ * acquires for its whole life and releases it once at `close()`.
267
+ *
268
+ * `schema` is where the store lives. It is set on the acquired
269
+ * connection AND given to the dialect, so the catalog statements the
270
+ * shape check and the introspector run look in the same place the DDL
271
+ * created in — one value, two consumers, which is the failure this
272
+ * option exists to prevent.
273
+ * @param {{ connect: Function }} source
274
+ * @param {{ schema?: string, queueTimeout?: number }} [options]
275
+ * @returns {any}
276
+ */
277
+ export function postgresDriver(source, options = undefined) {
278
+ if (source === null || typeof source !== 'object' || typeof source.connect !== 'function') {
279
+ throw new DbCompileError('JD0003',
280
+ 'postgresDriver needs an injected connection source exposing connect() — a pg.Pool is '
281
+ + 'one as it stands, and a single client becomes one with { connect: () => client }');
282
+ }
283
+ const schema = options?.schema;
284
+ if (schema !== undefined && !/^[A-Za-z_][A-Za-z0-9_$]*$/.test(schema)) {
285
+ throw new DbCompileError('JD0003',
286
+ `postgresDriver: '${schema}' is not a schema name this driver will set — the name is `
287
+ + 'written into a SET, so it is a plain identifier or nothing');
288
+ }
289
+ const dialect = postgresDialect(schema === undefined ? undefined : { searchPath: schema });
290
+
291
+ return Object.freeze({
292
+ name: 'postgres',
293
+ dialect,
294
+ /**
295
+ * @param {string} [path] - a PostgreSQL store lives in a SCHEMA on a
296
+ * server, not at a path; anything but the conventional `':memory:'`
297
+ * is refused by name rather than quietly ignored
298
+ * @param {{ queueTimeout?: number }} [openOptions]
299
+ * @returns {Promise<any>}
300
+ */
301
+ open: (path, openOptions) => {
302
+ if (path !== undefined && path !== '' && path !== ':memory:' && path !== schema) {
303
+ return Promise.reject(new DbCompileError('JD0003',
304
+ `this driver was opened with path '${path}', and a PostgreSQL store lives in a `
305
+ + 'schema on a server rather than at a path — name it with '
306
+ + 'postgresDriver(source, { schema })'));
307
+ }
308
+ return Promise.resolve(source.connect()).then((client) => {
309
+ const raw = adaptPostgresClient(client);
310
+ const prepared = schema === undefined
311
+ ? Promise.resolve(undefined)
312
+ // the schema is created by the operator or by the caller; the
313
+ // driver only points the connection at it, and a name that is
314
+ // not there fails as `3F000` — classified `cantopen`
315
+ : raw.exec(`SET search_path TO ${dialect.quoteIdentifier(schema)}`);
316
+ return Promise.resolve(prepared)
317
+ .catch((error) => Promise.resolve(raw.close()).then(() => {
318
+ throw error;
319
+ }, () => {
320
+ throw error;
321
+ }))
322
+ .then(() => openConnection(raw, {
323
+ dialect,
324
+ synchronous: false,
325
+ probe: postgresProbe,
326
+ queueTimeout: openOptions?.queueTimeout ?? options?.queueTimeout,
327
+ }));
328
+ });
329
+ },
330
+ });
331
+ }
@@ -0,0 +1,97 @@
1
+ //@ts-check
2
+ import { sqliteResultError } from '../errors.js';
3
+ import { wasmSessions } from './wasm-session.js';
4
+
5
+ /**
6
+ * Adapt an already-constructed `sqlite3.oo1` database (the official
7
+ * SQLite wasm build's object API) into the raw contract. The oo1 API
8
+ * is SYNCHRONOUS — wasm SQLite computes in place and the SAH-pool
9
+ * OPFS VFS does synchronous I/O inside a dedicated worker — which is
10
+ * exactly what keeps journal capture, live queries and the job queue
11
+ * working unchanged in a browser.
12
+ *
13
+ * Statements are REUSED by the store's prepared caches: every
14
+ * operation ends in `reset()`, never `finalize()`. oo1 user functions
15
+ * receive a context pointer first — stripped here — and register
16
+ * variadic (`arity: -1`), matching the engine's fragment shapes.
17
+ * @param {any} sqlite3 - the loaded sqlite3 module (for `capi`)
18
+ * @param {any} db - an `sqlite3.oo1.DB`-shaped database
19
+ * @returns {any} the raw binding for {@link openConnection}
20
+ */
21
+ export function adaptOo1Database(sqlite3, db) {
22
+ const capture = wasmSessions(sqlite3, db);
23
+ return {
24
+ ...(capture.session === undefined ? {} : { session: capture.session }),
25
+ sessionReason: capture.reason,
26
+ /** @param {string} sql */
27
+ exec: (sql) => {
28
+ db.exec(sql);
29
+ },
30
+ /** @param {string} sql */
31
+ prepare: (sql) => {
32
+ const statement = db.prepare(sql);
33
+ const bind = (params) => {
34
+ statement.reset(true);
35
+ for (let i = 0; i < params.length; i++) {
36
+ const value = params[i];
37
+ // oo1 selects int64 for every integral Number, even beyond its
38
+ // range. JSON numbers retain their IEEE-754 double semantics.
39
+ if (typeof value === 'number' && !Number.isSafeInteger(value)) {
40
+ const rc = sqlite3.capi.sqlite3_bind_double(statement.pointer, i + 1, value);
41
+ if (rc !== 0) throw sqliteResultError(rc, 'wasm number binding');
42
+ }
43
+ else statement.bind(i + 1, value);
44
+ }
45
+ };
46
+ const resetAfter = (fn) => {
47
+ let result;
48
+ try { result = fn(); }
49
+ catch (error) { try { statement.reset(); } catch { /* retain the initiating SQLite failure */ } throw error; }
50
+ statement.reset();
51
+ return result;
52
+ };
53
+ return {
54
+ readOnly: sqlite3.capi.sqlite3_stmt_readonly?.(statement.pointer) === 1,
55
+ run: (params = []) => resetAfter(() => {
56
+ bind(params);
57
+ statement.step();
58
+ return {
59
+ changes: db.changes(),
60
+ lastInsertRowid: Number(
61
+ sqlite3.capi.sqlite3_last_insert_rowid(db)),
62
+ };
63
+ }),
64
+ get: (params = []) => resetAfter(() => {
65
+ bind(params);
66
+ const row = statement.step() ? statement.get({}) : undefined;
67
+ return row;
68
+ }),
69
+ all: (params = []) => resetAfter(() => {
70
+ bind(params);
71
+ const rows = [];
72
+ while (statement.step()) rows.push(statement.get({}));
73
+ return rows;
74
+ }),
75
+ iterate: (params = []) => {
76
+ bind(params);
77
+ return {
78
+ next() {
79
+ if (statement.step()) return { done: false, value: statement.get({}) };
80
+ statement.reset();
81
+ return { done: true, value: undefined };
82
+ },
83
+ return(value) {
84
+ statement.reset();
85
+ return { done: true, value };
86
+ },
87
+ [Symbol.iterator]() { return this; },
88
+ };
89
+ },
90
+ };
91
+ },
92
+ close: () => { capture.close(); db.close(); },
93
+ registerFunction: (name, options, fn) => db.createFunction(name,
94
+ (_context, ...args) => fn(...args),
95
+ { deterministic: options?.deterministic === true, arity: -1 }),
96
+ };
97
+ }
@@ -0,0 +1,67 @@
1
+ //@ts-check
2
+ import { sqliteResultError } from '../errors.js';
3
+ /** Session C bindings are isolated from oo1 statements and Store capture. */
4
+
5
+ /** Probe the live API and own every session allocated for one database.
6
+ * @param {any} sqlite3 @param {any} db
7
+ * @returns {{ session?: (table?: string) => any, reason: string | null, close: () => void }}
8
+ */
9
+ export function wasmSessions(sqlite3, db) {
10
+ const { capi, wasm } = sqlite3;
11
+ const sessions = new Set();
12
+ const close = () => { for (const session of sessions) session.close(); };
13
+ const api = ['sqlite3session_create', 'sqlite3session_attach', 'sqlite3session_changeset', 'sqlite3session_delete', 'sqlite3_free'];
14
+ if (api.some((name) => typeof capi?.[name] !== 'function')
15
+ || ['allocPtr', 'dealloc', 'peekPtr', 'peek', 'heap8u'].some((name) => typeof wasm?.[name] !== 'function'))
16
+ return { reason: 'the loaded wasm binding lacks the session or memory APIs', close };
17
+ const check = (rc) => {
18
+ if (rc !== 0) throw sqliteResultError(rc, 'SQLite session API');
19
+ };
20
+ const create = (table = undefined) => {
21
+ const output = wasm.allocPtr();
22
+ let pointer = 0;
23
+ try {
24
+ const rc = capi.sqlite3session_create(db.pointer, 'main', output);
25
+ pointer = wasm.peekPtr(output);
26
+ check(rc);
27
+ if (!pointer) throw new Error('SQLite session create returned a null handle');
28
+ check(capi.sqlite3session_attach(pointer, table ?? null));
29
+ }
30
+ catch (error) { if (pointer) capi.sqlite3session_delete(pointer); throw error; }
31
+ finally { wasm.dealloc(output); }
32
+ let closed = false;
33
+ const session = {
34
+ changeset() {
35
+ if (closed) throw new Error('the SQLite session is closed');
36
+ const [lengthPointer, dataPointer] = wasm.allocPtr(2);
37
+ let data = 0;
38
+ try {
39
+ const rc = capi.sqlite3session_changeset(pointer, lengthPointer, dataPointer);
40
+ data = wasm.peekPtr(dataPointer);
41
+ check(rc);
42
+ const length = wasm.peek(lengthPointer, 'i32');
43
+ return wasm.heap8u().slice(data, data + length);
44
+ }
45
+ finally {
46
+ if (data) capi.sqlite3_free(data);
47
+ wasm.dealloc(lengthPointer);
48
+ }
49
+ },
50
+ close() {
51
+ if (closed) return;
52
+ closed = true;
53
+ sessions.delete(session);
54
+ capi.sqlite3session_delete(pointer);
55
+ },
56
+ };
57
+ sessions.add(session);
58
+ return session;
59
+ };
60
+ try {
61
+ const probe = create();
62
+ try { probe.changeset(); }
63
+ finally { probe.close(); }
64
+ return { session: create, reason: null, close };
65
+ }
66
+ catch (error) { close(); return { reason: `the disposable session probe failed: ${error.message}`, close }; }
67
+ }
@@ -13,7 +13,9 @@
13
13
  * open(path, options) -> raw | Promise<raw>,
14
14
  * synchronous?: boolean, // default false
15
15
  * declares?: { userFunctions?, deterministicIndexableFunctions?,
16
- * sessions? } // default all false
16
+ * sessions?, // default all false
17
+ * pragmas?, // default: the whole closed set
18
+ * maintenance? } // default: all four operations
17
19
  * }
18
20
  * raw = { exec(sql), prepare(sql) -> { run, get, all, iterate? },
19
21
  * close(), registerFunction?, registerAggregate?, session? }
@@ -26,83 +28,9 @@
26
28
  import { chain, openConnection } from '../driver.js';
27
29
  import { sqliteDialect } from '../dialects/sqlite.js';
28
30
  import { DbCompileError } from '../errors.js';
29
-
30
- /**
31
- * Adapt an already-constructed `sqlite3.oo1` database (the official
32
- * SQLite wasm build's object API) into the raw contract. The oo1 API
33
- * is SYNCHRONOUS — wasm SQLite computes in place and the SAH-pool
34
- * OPFS VFS does synchronous I/O inside a dedicated worker — which is
35
- * exactly what keeps journal capture, live queries and the job queue
36
- * working unchanged in a browser.
37
- *
38
- * Statements are REUSED by the store's prepared caches: every
39
- * operation ends in `reset()`, never `finalize()`. oo1 user functions
40
- * receive a context pointer first — stripped here — and register
41
- * variadic (`arity: -1`), matching the engine's fragment shapes.
42
- * @param {any} sqlite3 - the loaded sqlite3 module (for `capi`)
43
- * @param {any} db - an `sqlite3.oo1.DB`-shaped database
44
- * @returns {any} the raw binding for {@link openConnection}
45
- */
46
- export function adaptOo1Database(sqlite3, db) {
47
- return {
48
- /** @param {string} sql */
49
- exec: (sql) => {
50
- db.exec(sql);
51
- },
52
- /** @param {string} sql */
53
- prepare: (sql) => {
54
- const statement = db.prepare(sql);
55
- const bind = (params) => {
56
- statement.reset();
57
- if (params.length > 0) statement.bind(params);
58
- };
59
- return {
60
- run: (params = []) => {
61
- bind(params);
62
- statement.step();
63
- statement.reset();
64
- return {
65
- changes: db.changes(),
66
- lastInsertRowid: Number(
67
- sqlite3.capi.sqlite3_last_insert_rowid(db)),
68
- };
69
- },
70
- get: (params = []) => {
71
- bind(params);
72
- const row = statement.step() ? statement.get({}) : undefined;
73
- statement.reset();
74
- return row;
75
- },
76
- all: (params = []) => {
77
- bind(params);
78
- const rows = [];
79
- while (statement.step()) rows.push(statement.get({}));
80
- statement.reset();
81
- return rows;
82
- },
83
- iterate: (params = []) => {
84
- bind(params);
85
- return {
86
- next() {
87
- if (statement.step()) return { done: false, value: statement.get({}) };
88
- statement.reset();
89
- return { done: true, value: undefined };
90
- },
91
- return(value) {
92
- statement.reset();
93
- return { done: true, value };
94
- },
95
- [Symbol.iterator]() { return this; },
96
- };
97
- },
98
- };
99
- },
100
- close: () => db.close(),
101
- registerFunction: (name, options, fn) => db.createFunction(name,
102
- (_context, ...args) => fn(...args),
103
- { deterministic: options?.deterministic === true, arity: -1 }),
104
- };
105
- }
31
+ import { PRAGMA_NAMES } from '../pragmas.js';
32
+ import { adaptOo1Database } from './wasm-oo1.js';
33
+ export { adaptOo1Database } from './wasm-oo1.js';
106
34
 
107
35
  /**
108
36
  * Build the injected HANDLE from a loaded sqlite3 module — the D6
@@ -110,10 +38,9 @@ export function adaptOo1Database(sqlite3, db) {
110
38
  * (`sqlite3.oo1.DB` for `:memory:`, the SAH-pool util's `OpfsSAHPoolDb`
111
39
  * for OPFS persistence), and this package only adapts it.
112
40
  *
113
- * `sessions` is deliberately NOT declared even though the canonical
114
- * wasm build compiles `ENABLE_SESSION`: this adapter does not yet map
115
- * the session C API, so capture runs in the journal mode — stated in
116
- * the capability matrix, adapting it is a roadmap item.
41
+ * Session support is observed by creating, attaching, reading and deleting
42
+ * a disposable session on the opened database. A missing or failed API
43
+ * leaves the journal fallback available with a named capability reason.
117
44
  * @param {any} sqlite3 - the loaded sqlite3 module
118
45
  * @param {{ DbClass?: any }} [handleOptions] - the database class to
119
46
  * construct (default `sqlite3.oo1.DB`)
@@ -131,7 +58,7 @@ export function sqlite3Handle(sqlite3, handleOptions) {
131
58
  declares: {
132
59
  userFunctions: true,
133
60
  deterministicIndexableFunctions: true,
134
- sessions: false,
61
+ sessions: true,
135
62
  },
136
63
  /**
137
64
  * @param {string} path
@@ -166,13 +93,21 @@ export function wasmDriver(handle) {
166
93
  openConnection(raw, {
167
94
  dialect: sqliteDialect,
168
95
  synchronous: handle.synchronous === true,
96
+ queueTimeout: options?.queueTimeout,
169
97
  declared: {
170
98
  sessions: handle.declares?.sessions === true,
171
99
  userFunctions: handle.declares?.userFunctions === true,
172
100
  deterministicIndexableFunctions:
173
101
  handle.declares?.deterministicIndexableFunctions === true,
174
102
  aggregateFunctions: handle.declares?.aggregateFunctions === true,
103
+ // a handle that knows its build narrows the set; the default
104
+ // is the whole closed set, because the pragmas are the
105
+ // library's and the read-back refuses one a build lacks
106
+ pragmas: handle.declares?.pragmas ?? PRAGMA_NAMES,
107
+ maintenance: handle.declares?.maintenance,
175
108
  },
176
109
  })),
177
110
  });
178
111
  }
112
+
113
+ export { indexedDbSnapshotHandle, openSnapshotStorage } from './indexeddb-snapshot.js';