@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.
- package/ARCHITECTURE.md +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- 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
|
+
}
|
package/src/drivers/wasm.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* the
|
|
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:
|
|
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';
|