@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,58 @@
1
+ //@ts-check
2
+ /** JSON byte accounting over decoded data, without allocating encoded text. */
3
+
4
+ /** UTF-8 bytes of a JSON string, including quotes and well-formed escapes.
5
+ * @param {string} value @returns {number}
6
+ */
7
+ export function jsonStringBytes(value) {
8
+ let bytes = 2;
9
+ for (let i = 0; i < value.length; i++) {
10
+ const code = value.charCodeAt(i);
11
+ if (code === 34 || code === 92 || code === 8 || code === 9
12
+ || code === 10 || code === 12 || code === 13) bytes += 2;
13
+ else if (code < 32) bytes += 6;
14
+ else if (code < 128) bytes++;
15
+ else if (code < 2048) bytes += 2;
16
+ else if (code >= 0xd800 && code <= 0xdfff) {
17
+ const next = value.charCodeAt(i + 1);
18
+ if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) { bytes += 4; i++; }
19
+ else bytes += 6;
20
+ }
21
+ else bytes += 3;
22
+ }
23
+ return bytes;
24
+ }
25
+
26
+ /** The JSON serialization size of decoded JSON data. Objects are memoized
27
+ * during decoding, so a nested include reads its size without another walk.
28
+ * @param {any} value @param {WeakMap<object, number>} [sizes]
29
+ * @returns {number}
30
+ */
31
+ export function jsonBytes(value, sizes = new WeakMap()) {
32
+ if (value === null) return 4;
33
+ if (typeof value === 'string') return jsonStringBytes(value);
34
+ if (typeof value === 'boolean') return value ? 4 : 5;
35
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value).length : 4;
36
+ if (typeof value !== 'object') throw new TypeError('byte accounting requires decoded JSON data');
37
+ const cached = sizes.get(value);
38
+ if (cached !== undefined) return cached;
39
+ const keys = Object.keys(value);
40
+ let bytes = 2 + Math.max(0, keys.length - 1);
41
+ const array = Array.isArray(value);
42
+ for (const key of keys) bytes += (array ? 0 : jsonStringBytes(key) + 1) + jsonBytes(value[key], sizes);
43
+ sizes.set(value, bytes);
44
+ return bytes;
45
+ }
46
+
47
+ /** Decode and account bottom-up in the decoder's construction traversal.
48
+ * The caller checks the incoming text bound before decoding and checks the
49
+ * recorded nested bounds before attaching any reconstructed children.
50
+ * @param {string} text @param {WeakMap<object, number>} sizes
51
+ * @returns {any}
52
+ */
53
+ export function decodeCountedJson(text, sizes) {
54
+ return JSON.parse(text, (_key, value) => {
55
+ if (value !== null && typeof value === 'object') jsonBytes(value, sizes);
56
+ return value;
57
+ });
58
+ }
package/src/live-time.js CHANGED
@@ -77,7 +77,7 @@ export function normalizeEventTime(options, collection) {
77
77
  const declared = options?.eventTime;
78
78
  if (declared === undefined || declared === null) return null;
79
79
  const refuse = (reason) => {
80
- throw new DbCompileError('JD0053', reason, { collection });
80
+ throw new DbCompileError('JD0053', reason, `/collections/${collection}`);
81
81
  };
82
82
  if (!isJsonObject(declared))
83
83
  refuse('live eventTime is an object with a path, a watermark and a retention');
@@ -178,6 +178,15 @@ export function classifyEventTime(inner, windowed, keyed, eventTime) {
178
178
  return rerun(`'${name}' — a named zone resolves through the injected provider, which `
179
179
  + 'maintenance would have to consult per boundary');
180
180
  }
181
+ // the kernel reads member NAMES (`at`, `value`); the document spells
182
+ // them as row selectors (`$.at`) — handed the document's spelling, the
183
+ // kernel read `row['$.at']` and every fold died on its first reading
184
+ const valueMember = spec.value === undefined ? null : singularSelector(spec.value);
185
+ if (spec.value !== undefined && valueMember === null) {
186
+ return rerun(`'${name}' — the spec reads a value selector this view cannot follow`);
187
+ }
188
+ const kernelSpec = { ...spec, at: eventTime.member,
189
+ ...(valueMember === null ? {} : { value: valueMember }) };
181
190
  const aggregate = spec.aggregate ?? 'mean';
182
191
  if (!MAINTAINED_AGGREGATES.includes(aggregate)) {
183
192
  return rerun(`'${name}' — '${aggregate}' names a row by its position in the series, `
@@ -215,7 +224,7 @@ export function classifyEventTime(inner, windowed, keyed, eventTime) {
215
224
  return {
216
225
  strategy: 'rolling',
217
226
  source: operand.source,
218
- spec,
227
+ spec: kernelSpec,
219
228
  member: eventTime.member,
220
229
  width: span.width,
221
230
  eventTime,
@@ -236,7 +245,7 @@ export function classifyEventTime(inner, windowed, keyed, eventTime) {
236
245
  return {
237
246
  strategy: 'bucket',
238
247
  source: operand.source,
239
- spec,
248
+ spec: kernelSpec,
240
249
  member: eventTime.member,
241
250
  ladder,
242
251
  fill,
package/src/live.js CHANGED
@@ -79,12 +79,22 @@ function unwrapDocument(document) {
79
79
  return { inner: doc, whole, windowed, offset, limit, aggregate };
80
80
  }
81
81
 
82
+ /** Whether a `$for` source is the whole collection: the bare `$[*]`, or
83
+ * its packed spelling `["$[*]"]` — one array item a `$for` unpacks back
84
+ * into the rows, which is how `@jarenjs/linq` binds every source so an
85
+ * array-valued row stays one item. Over a collection the two are the
86
+ * same rows (the planner reads through the packing the same way). */
87
+ function isWholeCollection(source) {
88
+ return source === '$[*]'
89
+ || (Array.isArray(source) && source.length === 1 && source[0] === '$[*]');
90
+ }
91
+
82
92
  /** The single for-binding name of a canonical flwor, or null. */
83
93
  function bindingNameOf(inner) {
84
94
  if (!isJsonObject(inner) || !isJsonObject(inner.$for)) return null;
85
95
  const names = Object.keys(inner.$for);
86
96
  if (names.length !== 1) return null;
87
- return inner.$for[names[0]] === '$[*]' ? names[0] : null;
97
+ return isWholeCollection(inner.$for[names[0]]) ? names[0] : null;
88
98
  }
89
99
 
90
100
  /** The whole-documents read behind a flwor: same binding, same
@@ -0,0 +1,175 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The maintenance surface: the four operations an operator runs
4
+ * on a production SQLite database — a WAL checkpoint, an integrity
5
+ * check, a foreign-key check and `PRAGMA optimize` — as typed store
6
+ * operations. Each runs under the store gate (so it never interleaves
7
+ * an in-flight write), returns SQLite's own answer as typed data, and
8
+ * is refused by code (`JD2077`) exactly where the capability report
9
+ * says it is unavailable: a driver whose binding does not declare it,
10
+ * or — for the two that write — a read-only store, on which the engine
11
+ * would otherwise answer a checkpoint with a silent no-op.
12
+ *
13
+ * Nothing here interprets the engine's numbers. A checkpoint answers
14
+ * the row `PRAGMA wal_checkpoint` returns, so a store that is not in
15
+ * WAL mode reports `-1` frames as the engine does; a second passive
16
+ * checkpoint reports the same frame counts as the first (the frames
17
+ * stay in the log until a writer restarts it) and a second `truncate`
18
+ * reports zeros; an integrity check answers `ok: true` for the single
19
+ * `ok` row and the engine's problem rows verbatim otherwise — corruption
20
+ * is the RESULT, never a throw. Only a driver failure throws (`JD2078`).
21
+ */
22
+
23
+ import { DbRuntimeError, wrapDriverError } from './errors.js';
24
+ import { chain, attempt } from './driver.js';
25
+ import { refuseCancelled } from './cancellation.js';
26
+
27
+ /** The checkpoint modes `PRAGMA wal_checkpoint` accepts, closed. */
28
+ export const CHECKPOINT_MODES = Object.freeze(['passive', 'full', 'restart', 'truncate']);
29
+
30
+ /** The operations, in the order the capability report lists them. */
31
+ export const MAINTENANCE_OPERATIONS = Object.freeze(
32
+ ['checkpoint', 'integrityCheck', 'foreignKeyCheck', 'optimize']);
33
+
34
+ /** The two operations that write, refused on a read-only store. */
35
+ const WRITES = new Set(['checkpoint', 'optimize']);
36
+
37
+ /**
38
+ * The driver failure wrap: an error that already carries a code is the
39
+ * error; the driver's own failure becomes `JD2078` with the original as
40
+ * `cause`.
41
+ * @param {string} operation
42
+ * @returns {(error: any) => Error}
43
+ */
44
+ const failed = (operation) => (error) => wrapDriverError(error,
45
+ { code: 'JD2078', reason: `the maintenance operation '${operation}' failed`, always: true });
46
+
47
+ /**
48
+ * The per-operation capability booleans of one store: the binding's
49
+ * declaration, and for the writing operations the store's read-only
50
+ * flag as well — the report says `false` exactly where a call is
51
+ * refused.
52
+ * @param {Readonly<Record<string, boolean>> | undefined} declared - the
53
+ * connection's `capabilities.maintenance`
54
+ * @param {boolean} readOnly
55
+ * @returns {Readonly<Record<string, boolean>>}
56
+ */
57
+ export function maintenanceCapabilities(declared, readOnly) {
58
+ /** @type {Record<string, boolean>} */
59
+ const out = {};
60
+ for (const operation of MAINTENANCE_OPERATIONS) {
61
+ out[operation] = declared?.[operation] === true && !(readOnly && WRITES.has(operation));
62
+ }
63
+ return Object.freeze(out);
64
+ }
65
+
66
+ /**
67
+ * Build the maintenance operations over a connection. The caller owns
68
+ * the gate: every operation here is value-or-promise and issues its
69
+ * statements wherever the connection routes them.
70
+ * Every operation takes `{ signal, deadline }` and checks them once,
71
+ * before the one statement it issues — the granularity the driver has
72
+ * (`JD2081` for an abort, `JD2075` for a passed deadline, on the clock
73
+ * the store was opened with).
74
+ * @param {{ connection: any, readOnly: boolean, now: () => number }} context
75
+ * @returns {{ capabilities: Readonly<Record<string, boolean>>,
76
+ * checkpoint: (options?: { mode?: string, signal?: AbortSignal, deadline?: number }) => any,
77
+ * integrityCheck: (options?: { limit?: number, signal?: AbortSignal, deadline?: number }) => any,
78
+ * foreignKeyCheck: (options?: { signal?: AbortSignal, deadline?: number }) => any,
79
+ * optimize: (options?: { signal?: AbortSignal, deadline?: number }) => any }}
80
+ */
81
+ export function createMaintenance({ connection, readOnly, now }) {
82
+ const dialect = connection.dialect;
83
+ const capabilities = maintenanceCapabilities(connection.capabilities.maintenance, readOnly);
84
+
85
+ /** The cancellation check before an operation's statement.
86
+ * @param {any} options @param {string} operation */
87
+ const callable = (options, operation) => refuseCancelled(options, now,
88
+ { abortCode: 'JD2081', aborted: `'${operation}' ran`, passed: `'${operation}' ran` });
89
+
90
+ /** @param {string} operation */
91
+ const require = (operation) => {
92
+ if (capabilities[operation] === true) return;
93
+ throw new DbRuntimeError('JD2077',
94
+ readOnly && WRITES.has(operation) && connection.capabilities.maintenance?.[operation] === true
95
+ ? `the maintenance operation '${operation}' is unavailable on a read-only store: it writes`
96
+ : `the maintenance operation '${operation}' is unavailable on this store: the driver's `
97
+ + 'binding does not declare it');
98
+ };
99
+
100
+ /** Run one pragma statement and answer its rows, driver failures wrapped.
101
+ * @param {string} operation @param {string} sql */
102
+ const rows = (operation, sql) => attempt(
103
+ () => chain(connection.prepare(sql), (statement) => statement.all([])),
104
+ failed(operation));
105
+
106
+ return {
107
+ capabilities,
108
+ /**
109
+ * `PRAGMA wal_checkpoint(<mode>)`: the engine's row, typed. `busy`
110
+ * is whether the checkpoint could not complete because a reader or
111
+ * writer held it; the frame counts are the engine's (`-1` when the
112
+ * database is not in WAL mode).
113
+ * @param {{ mode?: string, signal?: AbortSignal, deadline?: number }} [options]
114
+ */
115
+ checkpoint(options = undefined) {
116
+ const mode = options?.mode ?? 'passive';
117
+ if (typeof mode !== 'string' || !CHECKPOINT_MODES.includes(mode)) {
118
+ throw new TypeError(`checkpoint: mode is one of ${
119
+ CHECKPOINT_MODES.map((m) => `'${m}'`).join(', ')}, got ${JSON.stringify(mode)}`);
120
+ }
121
+ callable(options, 'checkpoint');
122
+ require('checkpoint');
123
+ return chain(rows('checkpoint', dialect.pragma.walCheckpoint(mode)), (answer) => {
124
+ const row = answer[0] ?? {};
125
+ return {
126
+ busy: Number(row.busy) === 1,
127
+ logFrames: Number(row.log ?? -1),
128
+ checkpointedFrames: Number(row.checkpointed ?? -1),
129
+ };
130
+ });
131
+ },
132
+ /**
133
+ * `PRAGMA integrity_check(<limit>)`: `ok` for the single `ok` row,
134
+ * otherwise the engine's problem rows verbatim. Corruption is the
135
+ * result, not a throw.
136
+ * @param {{ limit?: number, signal?: AbortSignal, deadline?: number }} [options]
137
+ */
138
+ integrityCheck(options = undefined) {
139
+ const limit = options?.limit;
140
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
141
+ throw new TypeError(`integrityCheck: limit is a positive integer, got ${JSON.stringify(limit)}`);
142
+ }
143
+ callable(options, 'integrityCheck');
144
+ require('integrityCheck');
145
+ return chain(rows('integrityCheck', dialect.pragma.integrityCheck(limit)), (answer) => {
146
+ const problems = answer.map((row) => String(Object.values(row)[0]));
147
+ const ok = problems.length === 1 && problems[0] === 'ok';
148
+ return { ok, problems: ok ? [] : problems };
149
+ });
150
+ },
151
+ /** `PRAGMA foreign_key_check`: every violating row, typed.
152
+ * @param {{ signal?: AbortSignal, deadline?: number }} [options] */
153
+ foreignKeyCheck(options = undefined) {
154
+ callable(options, 'foreignKeyCheck');
155
+ require('foreignKeyCheck');
156
+ return chain(rows('foreignKeyCheck', dialect.pragma.foreignKeyCheck()), (answer) => ({
157
+ ok: answer.length === 0,
158
+ violations: answer.map((row) => ({
159
+ table: String(row.table),
160
+ rowId: row.rowid === null || row.rowid === undefined ? null : Number(row.rowid),
161
+ parent: String(row.parent),
162
+ fkid: Number(row.fkid),
163
+ })),
164
+ }));
165
+ },
166
+ /** `PRAGMA optimize`: the engine answers no rows, so the honest
167
+ * result is that it ran — no invented statistics.
168
+ * @param {{ signal?: AbortSignal, deadline?: number }} [options] */
169
+ optimize(options = undefined) {
170
+ callable(options, 'optimize');
171
+ require('optimize');
172
+ return chain(rows('optimize', dialect.pragma.optimize()), () => ({ ran: true }));
173
+ },
174
+ };
175
+ }