@jarenjs/db 0.56.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/ARCHITECTURE.md +412 -56
  2. package/README.md +600 -57
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +293 -45
  5. package/docs/LIVE-FORMAT.md +169 -20
  6. package/docs/MIGRATION-FORMAT.md +142 -17
  7. package/docs/MODEL-FORMAT.md +752 -64
  8. package/docs/REPLICATION-FORMAT.md +208 -0
  9. package/package.json +21 -7
  10. package/schemas/jaren-model.draft-07.schema.json +224 -162
  11. package/schemas/jaren-model.schema.json +224 -162
  12. package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
  13. package/schemas/jaren-replication-snapshot.schema.json +83 -0
  14. package/schemas/jaren-replication.draft-07.schema.json +82 -0
  15. package/schemas/jaren-replication.schema.json +82 -0
  16. package/src/algebra.js +227 -9
  17. package/src/backup.js +161 -0
  18. package/src/cancellation.js +48 -0
  19. package/src/capture.js +230 -47
  20. package/src/cli.js +165 -59
  21. package/src/cursor.js +417 -0
  22. package/src/dag-job.js +154 -21
  23. package/src/ddl.js +102 -8
  24. package/src/dialect.js +268 -113
  25. package/src/dialects/expression-read.js +158 -0
  26. package/src/dialects/postgres.js +618 -0
  27. package/src/dialects/rtree-ddl.js +129 -0
  28. package/src/dialects/sqlite.js +244 -11
  29. package/src/document-files.js +311 -0
  30. package/src/document-steps.js +422 -0
  31. package/src/documents.js +335 -0
  32. package/src/driver.js +448 -61
  33. package/src/drivers/bun.js +37 -1
  34. package/src/drivers/indexeddb-snapshot.js +149 -0
  35. package/src/drivers/node-pool.js +11 -0
  36. package/src/drivers/node-worker-endpoint.js +105 -0
  37. package/src/drivers/node-worker.js +204 -0
  38. package/src/drivers/node.js +41 -7
  39. package/src/drivers/postgres.js +331 -0
  40. package/src/drivers/wasm-oo1.js +97 -0
  41. package/src/drivers/wasm-session.js +67 -0
  42. package/src/drivers/wasm.js +17 -83
  43. package/src/drivers/worker-pool.js +183 -0
  44. package/src/drivers/worker-protocol.js +79 -0
  45. package/src/drivers/worker-queue.js +60 -0
  46. package/src/emit.js +339 -48
  47. package/src/entity.js +20 -22
  48. package/src/errors.js +430 -19
  49. package/src/expression.js +284 -0
  50. package/src/graph.js +64 -8
  51. package/src/index.js +48 -17
  52. package/src/introspect.js +583 -0
  53. package/src/jobs.js +843 -107
  54. package/src/json-bytes.js +58 -0
  55. package/src/live-join.js +250 -0
  56. package/src/live-nested.js +120 -0
  57. package/src/live.js +18 -4
  58. package/src/logical-rows.js +90 -0
  59. package/src/maintenance.js +175 -0
  60. package/src/migrate.js +248 -181
  61. package/src/model.js +68 -0
  62. package/src/plan.js +1119 -138
  63. package/src/pragmas.js +314 -0
  64. package/src/profile.js +151 -3
  65. package/src/query.js +1634 -323
  66. package/src/replication-format.js +115 -0
  67. package/src/replication.js +332 -0
  68. package/src/residual.js +17 -0
  69. package/src/series.js +12 -4
  70. package/src/store.js +1567 -273
  71. package/src/tracker.js +203 -29
  72. package/src/udf.js +88 -7
  73. package/types/index.d.ts +1158 -27
  74. package/types/node-pool.d.ts +28 -0
  75. package/types/node-worker.d.ts +54 -0
  76. package/types/node.d.ts +69 -2
  77. package/types/postgres.d.ts +46 -0
  78. package/types/typed.d.ts +27 -4
  79. package/types/wasm.d.ts +14 -0
@@ -0,0 +1,183 @@
1
+ //@ts-check
2
+ /** One writer and bounded read-only WAL workers behind a Connection. */
3
+ import { finishConnection } from '../driver.js';
4
+ import { DbCompileError, DbRuntimeError } from '../errors.js';
5
+ import { sqliteDialect } from '../dialects/sqlite.js';
6
+ import { positiveOption } from './worker-protocol.js';
7
+ import { workerQueue } from './worker-queue.js';
8
+
9
+ /**
10
+ * Reads explicitly classified by their compiled operation may use a read worker.
11
+ * All other work uses the writer; an open transaction keeps one worker.
12
+ * @param {{ readers?: number, queueCapacity?: number, graceMs?: number,
13
+ * worker?: any }} [configuration]
14
+ * @returns {any} a Driver
15
+ */
16
+ export function workerPoolDriver(configuration, driverFactory) {
17
+ const readers = configuration.readers ?? 2;
18
+ const capacity = configuration.queueCapacity ?? 64;
19
+ if (!Number.isSafeInteger(readers) || readers < 0 || readers > 32) throw new TypeError('readers must be an integer from 0 through 32');
20
+ if (!Number.isSafeInteger(capacity) || capacity < 0) throw new TypeError('queueCapacity must be a nonnegative safe integer');
21
+ const graceMs = positiveOption('graceMs', configuration.graceMs, 5000);
22
+ return Object.freeze({
23
+ name: 'node-worker-pool-sqlite', dialect: sqliteDialect,
24
+ open: async (path = ':memory:', options = {}) => {
25
+ const driver = driverFactory(configuration.worker);
26
+ const slots = [];
27
+ let closed = false;
28
+ let transaction = null;
29
+ let depth = 0;
30
+ let committing = null;
31
+ let sequence = 0;
32
+ const memory = path === ':memory:' || path === '';
33
+ const makeSlot = async (readOnly) => {
34
+ const connection = await driver.open(path, { ...options, readOnly });
35
+ return { connection, readOnly, active: false, healthy: true, generation: connection.generation,
36
+ statements: new Map(), executions: 0 };
37
+ };
38
+ try {
39
+ slots.push(await makeSlot(options.readOnly === true));
40
+ if (!memory) {
41
+ const writer = slots[0].connection;
42
+ const statement = await writer.prepare(options.readOnly === true ? sqliteDialect.introspect.pragma('journal_mode') : sqliteDialect.pragma.set('journal_mode', 'WAL'));
43
+ const mode = await statement.get();
44
+ if (String(Object.values(mode)[0]).toLowerCase() !== 'wal')
45
+ throw new DbCompileError('JD0008', 'a worker pool requires a file in WAL mode');
46
+ for (let i = 0; i < readers; i++) slots.push(await makeSlot(true));
47
+ }
48
+ }
49
+ catch (error) { await Promise.allSettled(slots.map((slot) => slot.connection.close())); throw error; }
50
+ const queue = workerQueue(slots, capacity, () => performance.now());
51
+ const readLane = (readOnly) => options.readOnly === true || (readOnly && slots.length > 1);
52
+ const replace = async (slot) => {
53
+ if (closed || slot.healthy) return;
54
+ await slot.connection.close().catch(() => {});
55
+ try {
56
+ const fresh = await makeSlot(slot.readOnly);
57
+ Object.assign(slot, fresh);
58
+ queue.wake();
59
+ }
60
+ catch (error) { queue.stop(error); }
61
+ };
62
+ const execute = async (lease, fn) => {
63
+ try { lease.slot.executions++; return await fn(lease.slot); }
64
+ catch (error) { if (error?.code === 'JD2090') lease.slot.healthy = false; throw error; }
65
+ };
66
+ const withLease = async (readOnly, fn) => {
67
+ if (closed) throw new DbRuntimeError('JD2063', 'the worker pool is closed');
68
+ if (transaction !== null) return execute(transaction, fn);
69
+ const lease = await queue.acquire(readLane(readOnly));
70
+ try { return await execute(lease, fn); }
71
+ finally { lease.release(); await replace(lease.slot); }
72
+ };
73
+ const raw = {
74
+ closeDrainsIterators: true,
75
+ exec: async (sql) => {
76
+ const begin = /^(?:SAVEPOINT|BEGIN)\b/.test(sql);
77
+ const release = /^RELEASE\b/.test(sql);
78
+ const end = /^(?:COMMIT|ROLLBACK(?! TO))\b/.test(sql);
79
+ const rollback = /^ROLLBACK\b/.test(sql);
80
+ if (begin && transaction === null) transaction = await queue.acquire(options.readOnly === true);
81
+ if (begin) depth++;
82
+ const operation = withLease(false, (slot) => slot.connection.exec(sql));
83
+ if (end || (release && depth === 1)) committing = operation;
84
+ let succeeded = false;
85
+ try { const value = await operation; succeeded = true; return value; }
86
+ catch (error) {
87
+ // A failed begin owns no new savepoint. A lost generation
88
+ // cannot acknowledge rollback or the following release; unwind
89
+ // exactly this scope, leaving any outer owner pinned until its
90
+ // own rollback settles. A failed COMMIT keeps its lease for
91
+ // the normal rollback path.
92
+ if (begin || (rollback && error?.code === 'JD2090'))
93
+ depth = end ? 0 : Math.max(0, depth - 1);
94
+ throw error;
95
+ }
96
+ finally {
97
+ if (succeeded && release) depth = Math.max(0, depth - 1);
98
+ if (succeeded && end) depth = 0;
99
+ if (transaction !== null && depth === 0) {
100
+ const lease = transaction;
101
+ transaction = null;
102
+ lease.release();
103
+ await replace(lease.slot);
104
+ }
105
+ if (committing === operation) committing = null;
106
+ }
107
+ },
108
+ prepare: (sql, metadata = {}) => {
109
+ const id = ++sequence;
110
+ const statementFor = async (slot) => {
111
+ if (!slot.statements.has(id) || metadata.ephemeral) {
112
+ const statement = await slot.connection.prepare(sql, metadata);
113
+ if (metadata.ephemeral) return statement;
114
+ slot.statements.set(id, statement);
115
+ }
116
+ return slot.statements.get(id);
117
+ };
118
+ const call = (method, params) => withLease(metadata.readOnly === true,
119
+ async (slot) => (await statementFor(slot))[method](params));
120
+ return {
121
+ run: (params = []) => call('run', params),
122
+ get: (params = []) => call('get', params),
123
+ all: (params = []) => call('all', params),
124
+ iterate: async (params = []) => {
125
+ const pinned = transaction;
126
+ const lease = pinned ?? await queue.acquire(readLane(metadata.readOnly === true));
127
+ let iterator;
128
+ try { iterator = await execute(lease, async (slot) => (await statementFor(slot)).iterate(params)); }
129
+ catch (error) { if (pinned === null) { lease.release(); await replace(lease.slot); } throw error; }
130
+ let done = false;
131
+ const finish = async () => {
132
+ if (done) return;
133
+ done = true;
134
+ try { await iterator.return(); }
135
+ finally { if (pinned === null) { lease.release(); await replace(lease.slot); } }
136
+ };
137
+ return {
138
+ next: async () => {
139
+ if (done) return { done: true, value: undefined };
140
+ try {
141
+ const step = await execute(lease, () => iterator.next());
142
+ if (step.done) await finish();
143
+ return step;
144
+ }
145
+ catch (error) { await finish().catch(() => {}); throw error; }
146
+ },
147
+ return: async () => { await finish(); return { done: true, value: undefined }; },
148
+ };
149
+ },
150
+ };
151
+ },
152
+ close: async () => {
153
+ if (closed) return;
154
+ closed = true;
155
+ queue.stop();
156
+ let timer;
157
+ await Promise.race([queue.drain(), new Promise((resolve) => { timer = setTimeout(resolve, graceMs); })]);
158
+ clearTimeout(timer);
159
+ // A commit already sent to SQLite owns its settlement. Closing
160
+ // never terminates that writer while its outcome is pending.
161
+ if (committing !== null) await committing.catch(() => {});
162
+ try { await Promise.all(slots.map((slot) => slot.connection.close())); }
163
+ finally {
164
+ for (const slot of slots) {
165
+ slot.healthy = false;
166
+ slot.active = false;
167
+ slot.statements.clear();
168
+ }
169
+ }
170
+ },
171
+ };
172
+ const capabilities = Object.freeze({ ...slots[0].connection.capabilities,
173
+ pooling: true, poolReaders: slots.filter((slot) => slot.readOnly).length,
174
+ poolWriters: slots.filter((slot) => !slot.readOnly).length });
175
+ const connection = finishConnection(raw, sqliteDialect, false, capabilities, options.queueTimeout);
176
+ return Object.freeze({ ...connection, get mustQueue() { return connection.mustQueue; },
177
+ metrics: () => Object.freeze({ ...queue.metrics(), workers: Object.freeze(slots.map((slot) =>
178
+ Object.freeze({ readOnly: slot.readOnly, healthy: slot.healthy, active: slot.active,
179
+ generation: slot.generation, executions: slot.executions }))) }),
180
+ });
181
+ },
182
+ });
183
+ }
@@ -0,0 +1,79 @@
1
+ //@ts-check
2
+ /** Structured-clone frames; identities belong to one connection generation. */
3
+ import { DbRuntimeError } from '../errors.js';
4
+ import { jsonStringBytes } from '../json-bytes.js';
5
+
6
+ export const WORKER_PROTOCOL_VERSION = 1;
7
+ const operations = new Set(['exec', 'prepare', 'run', 'get', 'iterate', 'next', 'return', 'finalize', 'close']);
8
+
9
+ /** @param {number} generation @param {boolean} [transaction] @param {unknown} [cause] */
10
+ export function generationFailure(generation, transaction = false, cause = undefined) {
11
+ return Object.assign(new DbRuntimeError('JD2090',
12
+ `driver generation ${generation} is no longer available; reopen the connection; no operation was replayed`,
13
+ { cause }), { class: 'generation', retryable: !transaction, generation });
14
+ }
15
+
16
+ /** @param {any} frame @returns {boolean} */
17
+ export function validRequest(frame) {
18
+ if (frame === null || typeof frame !== 'object' || frame.v !== WORKER_PROTOCOL_VERSION
19
+ || frame.kind !== 'request' || !Number.isSafeInteger(frame.id) || frame.id < 1
20
+ || !Number.isSafeInteger(frame.generation) || frame.generation < 1 || !operations.has(frame.op)) return false;
21
+ if (frame.op === 'exec' || frame.op === 'prepare') return typeof frame.sql === 'string';
22
+ if (['run', 'get', 'iterate'].includes(frame.op))
23
+ return Number.isSafeInteger(frame.statement) && frame.statement > 0 && Array.isArray(frame.params);
24
+ if (frame.op === 'next') return Number.isSafeInteger(frame.cursor) && frame.cursor > 0
25
+ && Number.isSafeInteger(frame.rows) && frame.rows > 0 && Number.isSafeInteger(frame.bytes) && frame.bytes > 0;
26
+ if (frame.op === 'return') return Number.isSafeInteger(frame.cursor) && frame.cursor > 0;
27
+ if (frame.op === 'finalize') return Number.isSafeInteger(frame.statement) && frame.statement > 0;
28
+ return true;
29
+ }
30
+
31
+ /** Validate clone frames before dereferencing a remote payload.
32
+ * @param {any} frame @param {number} generation @returns {boolean}
33
+ */
34
+ export function validResponse(frame, generation) {
35
+ if (frame === null || typeof frame !== 'object' || frame.v !== WORKER_PROTOCOL_VERSION
36
+ || frame.generation !== generation || !Number.isSafeInteger(frame.id) || frame.id < 0) return false;
37
+ if (frame.kind === 'ready') return frame.id === 0 && frame.capabilities !== null
38
+ && typeof frame.capabilities === 'object' && typeof frame.capabilities.version === 'string';
39
+ if (frame.kind === 'failure') return frame.error !== null && typeof frame.error === 'object'
40
+ && typeof frame.error.message === 'string';
41
+ return frame.kind === 'result' && frame.id > 0;
42
+ }
43
+
44
+ /** @param {string} op @param {any} value @param {{rows:number, bytes:number}} limits */
45
+ export function validResult(op, value, limits) {
46
+ if (op === 'prepare' || op === 'iterate') return Number.isSafeInteger(value) && value > 0;
47
+ if (op !== 'next') return true;
48
+ return value !== null && typeof value === 'object' && Array.isArray(value.rows)
49
+ && value.rows.length <= limits.rows && typeof value.done === 'boolean'
50
+ && (value.rows.length > 0 || value.done) && Number.isSafeInteger(value.bytes)
51
+ && value.bytes >= 0 && value.bytes <= limits.bytes
52
+ && value.rows.reduce((sum, row) => sum + rowBytes(row), 0) === value.bytes;
53
+ }
54
+
55
+ /** A stable credit measure: JSON keys/scalars and raw blob bytes. Transport
56
+ * metadata is fixed per frame; no JSON string or copied blob is allocated.
57
+ * @param {any} value @returns {number}
58
+ */
59
+ export function rowBytes(value) {
60
+ if (value === null || value === undefined) return 4;
61
+ if (ArrayBuffer.isView(value)) return value.byteLength;
62
+ if (typeof value === 'string') return jsonStringBytes(value);
63
+ if (typeof value !== 'object') return String(value).length;
64
+ let bytes = 2;
65
+ for (const key of Object.keys(value)) bytes += jsonStringBytes(key) + 2 + rowBytes(value[key]);
66
+ return bytes;
67
+ }
68
+
69
+ /** @param {string} name @param {any} value @param {number} fallback @returns {number} */
70
+ export function positiveOption(name, value, fallback) {
71
+ if (value === undefined) return fallback;
72
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive safe integer`);
73
+ return value;
74
+ }
75
+
76
+ /** @param {string} reason @param {number} depth */
77
+ export function queueFailure(reason, depth) {
78
+ return Object.assign(new DbRuntimeError('JD2091', reason), { class: 'queue', retryable: true, depth });
79
+ }
@@ -0,0 +1,60 @@
1
+ //@ts-check
2
+ /** Bounded FIFO admission, with a deterministic clock and inspectable leases. */
3
+ import { DbRuntimeError } from '../errors.js';
4
+ import { queueFailure } from './worker-protocol.js';
5
+
6
+ /** @param {any[]} slots @param {number} capacity @param {() => number} now */
7
+ export function workerQueue(slots, capacity, now) {
8
+ const waiting = [];
9
+ const waits = [];
10
+ const observers = new Set();
11
+ let stopped = null;
12
+ const idle = () => { if (slots.every((slot) => !slot.active)) for (const fn of observers) fn(); };
13
+ const choose = (readOnly) => slots.find((slot) => !slot.active && slot.healthy
14
+ && (readOnly ? slot.readOnly : !slot.readOnly));
15
+ const grant = (slot, entry) => {
16
+ slot.active = true;
17
+ const wait = Math.max(0, now() - entry.at);
18
+ if (waits.length === 1024) waits.shift();
19
+ waits.push(wait);
20
+ let released = false;
21
+ entry.resolve({ slot, release: () => {
22
+ if (released) return;
23
+ released = true;
24
+ slot.active = false;
25
+ pump();
26
+ idle();
27
+ } });
28
+ };
29
+ const pump = () => {
30
+ while (waiting.length > 0) {
31
+ const slot = choose(waiting[0].readOnly);
32
+ if (slot === undefined) break;
33
+ grant(slot, waiting.shift());
34
+ }
35
+ };
36
+ return {
37
+ acquire: (readOnly) => new Promise((resolve, reject) => {
38
+ if (stopped !== null) { reject(stopped); return; }
39
+ const entry = { readOnly, resolve, reject, at: now() };
40
+ const slot = waiting.length === 0 ? choose(readOnly) : undefined;
41
+ if (slot !== undefined) { grant(slot, entry); return; }
42
+ if (waiting.length >= capacity) { reject(queueFailure(`worker pool queue capacity ${capacity} exceeded`, waiting.length)); return; }
43
+ waiting.push(entry);
44
+ }),
45
+ stop(error = new DbRuntimeError('JD2063', 'the worker pool closed before the queued work ran')) {
46
+ stopped = error;
47
+ for (const entry of waiting.splice(0)) entry.reject(error);
48
+ },
49
+ drain: () => slots.every((slot) => !slot.active) ? Promise.resolve()
50
+ : new Promise((resolve) => { const done = () => { observers.delete(done); resolve(undefined); }; observers.add(done); }),
51
+ wake: pump,
52
+ metrics: () => {
53
+ const values = [...waits].sort((a, b) => a - b);
54
+ return Object.freeze({ active: slots.filter((s) => s.active).length,
55
+ idle: slots.filter((s) => !s.active && s.healthy).length, queued: waiting.length,
56
+ waitMs: Object.freeze({ p50: values[Math.floor(values.length * 0.5)] ?? 0,
57
+ p95: values[Math.min(values.length - 1, Math.floor(values.length * 0.95))] ?? 0 }) });
58
+ },
59
+ };
60
+ }