@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,284 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Model-declared index expressions: a CLOSED abstract syntax tree
|
|
4
|
+
* over declared members, JSON scalars and registered deterministic
|
|
5
|
+
* functions — and never SQL text.
|
|
6
|
+
*
|
|
7
|
+
* An index over a function is a schema dependency: a database whose
|
|
8
|
+
* column is computed by `lower(…)` cannot be written from a connection
|
|
9
|
+
* that has no `lower`. That hazard is the reason this vocabulary is a
|
|
10
|
+
* DECLARATION rather than an escape hatch — the model names the
|
|
11
|
+
* function, every store that opens the model is handed the same
|
|
12
|
+
* declaration, and a store that cannot honour one refuses at open with
|
|
13
|
+
* `JD0004` before a single statement runs. A raw-SQL index would have
|
|
14
|
+
* had the same hazard with none of the checking.
|
|
15
|
+
*
|
|
16
|
+
* Three node kinds, and no fourth:
|
|
17
|
+
*
|
|
18
|
+
* ```jsonc
|
|
19
|
+
* { "member": "$.email" } // a singular member path
|
|
20
|
+
* { "value": "x" } // a JSON scalar
|
|
21
|
+
* { "call": "lower", "args": [ <node>, … ] } // a declared function
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* A DECLARATION is what the host supplies per name:
|
|
25
|
+
*
|
|
26
|
+
* ```jsonc
|
|
27
|
+
* { "arity": 1, "deterministic": true,
|
|
28
|
+
* "apply": (value) => …, // for an engine that registers functions
|
|
29
|
+
* "sql": "lower" } // for one that cannot, and whose function is IMMUTABLE
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Neither half is optional in general: an engine that registers
|
|
33
|
+
* functions needs `apply`, one that does not needs `sql`, and a
|
|
34
|
+
* declaration missing the half its engine needs is `JD0004` at open —
|
|
35
|
+
* not a column that silently computes something else. Nothing here
|
|
36
|
+
* creates a function on the server; `sql` is a promise the host makes
|
|
37
|
+
* about one that already exists.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { DbCompileError } from './errors.js';
|
|
41
|
+
|
|
42
|
+
/** The three node kinds, closed. */
|
|
43
|
+
export const EXPRESSION_KINDS = Object.freeze(['member', 'value', 'call']);
|
|
44
|
+
|
|
45
|
+
/** How deep a declared expression may nest. A bound, not a taste: the
|
|
46
|
+
* canonical form is hashed, compared and emitted, and an unbounded one
|
|
47
|
+
* would let a model document cost a store its stack at open. */
|
|
48
|
+
export const EXPRESSION_DEPTH = 8;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} reason
|
|
52
|
+
* @param {string} docPath
|
|
53
|
+
* @returns {DbCompileError}
|
|
54
|
+
*/
|
|
55
|
+
function refuse(reason, docPath) {
|
|
56
|
+
return new DbCompileError('JD0004', reason, docPath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize one expression node, refusing everything the vocabulary
|
|
61
|
+
* does not name. The result is frozen and canonical: a member's path is
|
|
62
|
+
* the string it was declared as, a value is the scalar itself, and a
|
|
63
|
+
* call carries its resolved arity.
|
|
64
|
+
* @param {any} node
|
|
65
|
+
* @param {string} docPath
|
|
66
|
+
* @param {Record<string, any>} declarations - the host's function
|
|
67
|
+
* declarations, by name
|
|
68
|
+
* @param {number} [depth]
|
|
69
|
+
* @returns {any}
|
|
70
|
+
*/
|
|
71
|
+
export function normalizeExpression(node, docPath, declarations, depth = 0) {
|
|
72
|
+
if (depth > EXPRESSION_DEPTH) {
|
|
73
|
+
throw refuse(`an index expression nests deeper than ${EXPRESSION_DEPTH}`, docPath);
|
|
74
|
+
}
|
|
75
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node))
|
|
76
|
+
throw refuse('an index expression node must be an object', docPath);
|
|
77
|
+
const kinds = EXPRESSION_KINDS.filter((kind) => Object.hasOwn(node, kind));
|
|
78
|
+
if (kinds.length !== 1) {
|
|
79
|
+
throw refuse(
|
|
80
|
+
`an index expression node is exactly one of ${EXPRESSION_KINDS.map((k) => `'${k}'`).join(', ')}`
|
|
81
|
+
+ `${kinds.length === 0 ? '' : `, and this one names ${kinds.length}`}`, docPath);
|
|
82
|
+
}
|
|
83
|
+
const [kind] = kinds;
|
|
84
|
+
|
|
85
|
+
if (kind === 'member') {
|
|
86
|
+
if (typeof node.member !== 'string' || node.member.length === 0)
|
|
87
|
+
throw refuse('a member node takes a JSONPath string', `${docPath}/member`);
|
|
88
|
+
for (const extra of Object.keys(node)) {
|
|
89
|
+
if (extra !== 'member')
|
|
90
|
+
throw refuse(`a member node takes no '${extra}'`, `${docPath}/${extra}`);
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze({ member: node.member });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (kind === 'value') {
|
|
96
|
+
const value = node.value;
|
|
97
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
98
|
+
throw refuse('a value node takes a JSON string, number or boolean — a null or a '
|
|
99
|
+
+ 'compound has no place in an index expression', `${docPath}/value`);
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
102
|
+
throw refuse('a value node takes a finite number', `${docPath}/value`);
|
|
103
|
+
for (const extra of Object.keys(node)) {
|
|
104
|
+
if (extra !== 'value')
|
|
105
|
+
throw refuse(`a value node takes no '${extra}'`, `${docPath}/${extra}`);
|
|
106
|
+
}
|
|
107
|
+
return Object.freeze({ value });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof node.call !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(node.call))
|
|
111
|
+
throw refuse('a call node names a function by identifier', `${docPath}/call`);
|
|
112
|
+
for (const extra of Object.keys(node)) {
|
|
113
|
+
if (extra !== 'call' && extra !== 'args')
|
|
114
|
+
throw refuse(`a call node takes no '${extra}'`, `${docPath}/${extra}`);
|
|
115
|
+
}
|
|
116
|
+
const args = node.args ?? [];
|
|
117
|
+
if (!Array.isArray(args))
|
|
118
|
+
throw refuse('a call node\'s args is an array', `${docPath}/args`);
|
|
119
|
+
|
|
120
|
+
const declared = declarations?.[node.call];
|
|
121
|
+
if (declared === undefined) {
|
|
122
|
+
const known = Object.keys(declarations ?? {}).sort();
|
|
123
|
+
throw refuse(`the index expression names the function '${node.call}', which this store was `
|
|
124
|
+
+ `not given: the declared functions are ${known.length === 0
|
|
125
|
+
? 'none' : known.map((name) => `'${name}'`).join(', ')} — pass them as `
|
|
126
|
+
+ 'openStore({ expressions })', `${docPath}/call`);
|
|
127
|
+
}
|
|
128
|
+
if (declared.deterministic !== true) {
|
|
129
|
+
throw refuse(`the function '${node.call}' is not declared deterministic, and an index over `
|
|
130
|
+
+ 'a function that may answer differently for one row is an index that lies',
|
|
131
|
+
`${docPath}/call`);
|
|
132
|
+
}
|
|
133
|
+
if (!Number.isInteger(declared.arity) || declared.arity < 0)
|
|
134
|
+
throw refuse(`the function '${node.call}' declares no whole arity`, `${docPath}/call`);
|
|
135
|
+
if (args.length !== declared.arity) {
|
|
136
|
+
throw refuse(`the function '${node.call}' takes ${declared.arity} argument(s) and the `
|
|
137
|
+
+ `expression gives it ${args.length}`, `${docPath}/args`);
|
|
138
|
+
}
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
call: node.call,
|
|
141
|
+
args: Object.freeze(args.map((argument, i) =>
|
|
142
|
+
normalizeExpression(argument, `${docPath}/args/${i}`, declarations, depth + 1))),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The canonical text of one normalized expression: the identity two
|
|
148
|
+
* declarations of the same expression share, and the identity the shape
|
|
149
|
+
* hash and the migration diff read.
|
|
150
|
+
*
|
|
151
|
+
* ORDER-PRESERVING, deliberately: `sub(a, b)` and `sub(b, a)` are
|
|
152
|
+
* different expressions and must be different columns, so nothing here
|
|
153
|
+
* sorts.
|
|
154
|
+
* @param {any} node
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
export function canonicalExpression(node) {
|
|
158
|
+
if (Object.hasOwn(node, 'member')) return `m(${JSON.stringify(node.member)})`;
|
|
159
|
+
if (Object.hasOwn(node, 'value')) return `v(${JSON.stringify(node.value)})`;
|
|
160
|
+
return `${node.call}(${node.args.map(canonicalExpression).join(',')})`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Every member path one expression reads, in the order it reads them.
|
|
165
|
+
* @param {any} node
|
|
166
|
+
* @param {string[]} [out]
|
|
167
|
+
* @returns {string[]}
|
|
168
|
+
*/
|
|
169
|
+
export function expressionMembers(node, out = []) {
|
|
170
|
+
if (Object.hasOwn(node, 'member')) out.push(node.member);
|
|
171
|
+
else if (Object.hasOwn(node, 'call')) for (const argument of node.args)
|
|
172
|
+
expressionMembers(argument, out);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Every function one expression calls, sorted and deduplicated — what a
|
|
178
|
+
* store registers before it can so much as SELECT from the table.
|
|
179
|
+
* @param {any} node
|
|
180
|
+
* @param {Set<string>} [out]
|
|
181
|
+
* @returns {string[]}
|
|
182
|
+
*/
|
|
183
|
+
export function expressionFunctions(node, out = new Set()) {
|
|
184
|
+
if (Object.hasOwn(node, 'call')) {
|
|
185
|
+
out.add(node.call);
|
|
186
|
+
for (const argument of node.args) expressionFunctions(argument, out);
|
|
187
|
+
}
|
|
188
|
+
return [...out].sort();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A short, stable, readable column stem for one expression: the
|
|
193
|
+
* outermost call and the members it reads. Distinct expressions that
|
|
194
|
+
* sanitize to one stem are separated by the DDL planner's own
|
|
195
|
+
* collision suffixing; the canonical text is what decides identity.
|
|
196
|
+
* @param {any} node
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function expressionStem(node) {
|
|
200
|
+
const call = Object.hasOwn(node, 'call') ? node.call : 'x';
|
|
201
|
+
const members = expressionMembers(node)
|
|
202
|
+
.map((path) => path.replace(/^\$\.?/, '').replace(/[^A-Za-z0-9]+/g, '_'))
|
|
203
|
+
.filter((part) => part.length > 0);
|
|
204
|
+
return [call, ...members].join('_').replace(/^_+|_+$/g, '');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The SQL an expression compiles to on one dialect, and the check that
|
|
209
|
+
* it can be compiled at all.
|
|
210
|
+
*
|
|
211
|
+
* The two engines take different halves of a declaration: one registers
|
|
212
|
+
* the function and calls it by a generated name, the other calls the
|
|
213
|
+
* IMMUTABLE function the host promised already exists. A declaration
|
|
214
|
+
* missing the half its engine needs is `JD0004` HERE — at planning,
|
|
215
|
+
* before any DDL — which is the whole point of resolving it at open.
|
|
216
|
+
* @param {any} node - a normalized expression
|
|
217
|
+
* @param {any} dialect
|
|
218
|
+
* @param {{ docColumnSql: string, declarations: Record<string, any>,
|
|
219
|
+
* registered: boolean, docPath: string }} context - `registered` is
|
|
220
|
+
* whether this connection can register a deterministic function
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
export function expressionSql(node, dialect, context) {
|
|
224
|
+
if (Object.hasOwn(node, 'value')) {
|
|
225
|
+
return typeof node.value === 'string'
|
|
226
|
+
? dialect.stringLiteral(node.value)
|
|
227
|
+
: typeof node.value === 'boolean'
|
|
228
|
+
? dialect.booleanLiteral(node.value)
|
|
229
|
+
: String(node.value);
|
|
230
|
+
}
|
|
231
|
+
if (Object.hasOwn(node, 'member')) {
|
|
232
|
+
const pathText = dialect.jsonPathText(context.segmentsOf(node.member));
|
|
233
|
+
if (pathText === null) {
|
|
234
|
+
throw refuse(`the index expression reads '${node.member}', a member name the dialect's `
|
|
235
|
+
+ 'JSON path grammar cannot carry', context.docPath);
|
|
236
|
+
}
|
|
237
|
+
// the member's own SCALAR, not its JSON text: a declared function is
|
|
238
|
+
// a function of the VALUE, and `lower` of a string must be the same
|
|
239
|
+
// answer on both engines rather than one of them quoting it first
|
|
240
|
+
return dialect.jsonExtract(context.docColumnSql, pathText, 'scalar');
|
|
241
|
+
}
|
|
242
|
+
const declared = context.declarations[node.call];
|
|
243
|
+
const args = node.args.map((argument) => expressionSql(argument, dialect, context));
|
|
244
|
+
if (context.registered) {
|
|
245
|
+
if (typeof declared.apply !== 'function') {
|
|
246
|
+
throw refuse(`the function '${node.call}' has no 'apply': this connection computes an `
|
|
247
|
+
+ 'index expression by registering a deterministic function, and there is nothing '
|
|
248
|
+
+ 'to register', context.docPath);
|
|
249
|
+
}
|
|
250
|
+
return `${registeredName(node.call)}(${args.join(', ')})`;
|
|
251
|
+
}
|
|
252
|
+
if (typeof declared.sql !== 'string' || !/^[A-Za-z_][A-Za-z0-9_.]*$/.test(declared.sql)) {
|
|
253
|
+
throw refuse(`the function '${node.call}' has no 'sql' name: this connection cannot register `
|
|
254
|
+
+ 'a function, so an index over one needs the name of an IMMUTABLE function the server '
|
|
255
|
+
+ 'already has — and it is an identifier, never SQL text', context.docPath);
|
|
256
|
+
}
|
|
257
|
+
return `${declared.sql}(${args.join(', ')})`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The SQL name a declared function is registered under. Namespaced, so
|
|
262
|
+
* a model's `lower` never shadows the engine's own.
|
|
263
|
+
* @param {string} name
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
export function registeredName(name) {
|
|
267
|
+
return `jaren_x_${name}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Register every function a set of expressions calls on one connection.
|
|
272
|
+
* @param {any} connection
|
|
273
|
+
* @param {readonly string[]} names
|
|
274
|
+
* @param {Record<string, any>} declarations
|
|
275
|
+
* @returns {any} value-or-promise
|
|
276
|
+
*/
|
|
277
|
+
export function registerExpressionFunctions(connection, names, declarations) {
|
|
278
|
+
if (names.length === 0 || connection.registerFunction === null) return null;
|
|
279
|
+
for (const name of names) {
|
|
280
|
+
connection.registerFunction(registeredName(name),
|
|
281
|
+
{ deterministic: true, varargs: false }, declarations[name].apply);
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
package/src/graph.js
CHANGED
|
@@ -6,9 +6,64 @@
|
|
|
6
6
|
* derived epoch columns are skipped because the string never left the
|
|
7
7
|
* document), foreign-key columns fold in the same way, and — for the
|
|
8
8
|
* one-statement graph loads — projected relation JSON parses
|
|
9
|
-
* recursively into child arrays or single children
|
|
9
|
+
* recursively into child arrays or single children — and, per root,
|
|
10
|
+
* holds every included relation to its declared bound (MODEL-FORMAT
|
|
11
|
+
* §10.4): more rows than `maxRows` or more serialised bytes than
|
|
12
|
+
* `maxBytes` is the coded refusal `JD2073` naming the root, the member
|
|
13
|
+
* and the bound, never a truncated graph the caller cannot tell from a
|
|
14
|
+
* whole one.
|
|
10
15
|
*/
|
|
11
16
|
|
|
17
|
+
import { DbRuntimeError } from './errors.js';
|
|
18
|
+
import { utf8Length } from './cursor.js';
|
|
19
|
+
import { jsonBytes, decodeCountedJson } from './json-bytes.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Refuse an include that crossed its per-root bound.
|
|
23
|
+
* @param {any} node - the parent node (`entity`, `entityMapping`)
|
|
24
|
+
* @param {any} include - the include entry (`name`, `maxRows`, `maxBytes`)
|
|
25
|
+
* @param {any} keyed - the parent row or child object the key is read from
|
|
26
|
+
* @param {'maxRows' | 'maxBytes'} bound
|
|
27
|
+
* @param {number} measured
|
|
28
|
+
* @returns {DbRuntimeError}
|
|
29
|
+
*/
|
|
30
|
+
function boundRefusal(node, include, keyed, bound, measured) {
|
|
31
|
+
const limit = include[bound];
|
|
32
|
+
const key = node.entityMapping.keys.map((name) => JSON.stringify(keyed[name])).join(', ');
|
|
33
|
+
const entity = node.entity?.name ?? node.entityMapping.table;
|
|
34
|
+
return new DbRuntimeError('JD2073',
|
|
35
|
+
`the include '${include.name}' of ${entity} ${key} holds ${bound === 'maxRows'
|
|
36
|
+
? `more than ${limit} rows` : `${measured} serialised bytes, more than ${limit}`} — its `
|
|
37
|
+
+ `${bound} bound for one root. Declare ${bound}: Infinity on the include to load it whole `
|
|
38
|
+
+ `by decision, read { count: true } for the size, or page the relation separately`,
|
|
39
|
+
{ collection: entity, key: node.entityMapping.keys.length === 1 ? keyed[node.entityMapping.keys[0]] : undefined });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Hold one include of one parent to its bounds. Bytes are measured on
|
|
44
|
+
* the serialised text the database projected (a nested include arrives
|
|
45
|
+
* already embedded as JSON and carries its decoder-computed size); rows on
|
|
46
|
+
* the parsed array.
|
|
47
|
+
* @param {any} node
|
|
48
|
+
* @param {any} include
|
|
49
|
+
* @param {any} keyed
|
|
50
|
+
* @param {any} raw - the projected value: text at the root, JSON below
|
|
51
|
+
* @param {WeakMap<object, number>} sizes
|
|
52
|
+
* @returns {any} the bounded decoded value
|
|
53
|
+
*/
|
|
54
|
+
function checkBounds(node, include, keyed, raw, sizes) {
|
|
55
|
+
if (include.maxBytes !== null && include.maxBytes !== undefined) {
|
|
56
|
+
const bytes = typeof raw === 'string' ? utf8Length(raw) : jsonBytes(raw, sizes);
|
|
57
|
+
if (bytes > include.maxBytes) throw boundRefusal(node, include, keyed, 'maxBytes', bytes);
|
|
58
|
+
}
|
|
59
|
+
const parsed = typeof raw === 'string' ? decodeCountedJson(raw, sizes) : raw;
|
|
60
|
+
if (include.many && include.maxRows !== null && include.maxRows !== undefined
|
|
61
|
+
&& parsed.length > include.maxRows) {
|
|
62
|
+
throw boundRefusal(node, include, keyed, 'maxRows', parsed.length);
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
|
|
12
67
|
/**
|
|
13
68
|
* Merge one database row back into its entity document.
|
|
14
69
|
* @param {any} entityMapping - `explainMapping(...).entities[name]`
|
|
@@ -42,6 +97,7 @@ export function mergeEntityRow(entityMapping, row, docField = 'doc') {
|
|
|
42
97
|
* @returns {any}
|
|
43
98
|
*/
|
|
44
99
|
export function parseGraphRow(node, row, docField = '__doc') {
|
|
100
|
+
const sizes = new WeakMap();
|
|
45
101
|
const doc = mergeEntityRow(node.entityMapping, row, docField);
|
|
46
102
|
for (const include of node.includes) {
|
|
47
103
|
const raw = row[include.field];
|
|
@@ -53,10 +109,10 @@ export function parseGraphRow(node, row, docField = '__doc') {
|
|
|
53
109
|
doc[include.name] = include.many ? [] : null;
|
|
54
110
|
continue;
|
|
55
111
|
}
|
|
56
|
-
const parsed =
|
|
112
|
+
const parsed = checkBounds(node, include, row, raw, sizes);
|
|
57
113
|
doc[include.name] = include.many
|
|
58
|
-
? parsed.map((child) => parseGraphChild(include.child, child))
|
|
59
|
-
: parseGraphChild(include.child, parsed);
|
|
114
|
+
? parsed.map((child) => parseGraphChild(include.child, child, sizes))
|
|
115
|
+
: parseGraphChild(include.child, parsed, sizes);
|
|
60
116
|
}
|
|
61
117
|
return doc;
|
|
62
118
|
}
|
|
@@ -70,7 +126,7 @@ export function parseGraphRow(node, row, docField = '__doc') {
|
|
|
70
126
|
* @param {any} child
|
|
71
127
|
* @returns {any}
|
|
72
128
|
*/
|
|
73
|
-
function parseGraphChild(node, child) {
|
|
129
|
+
function parseGraphChild(node, child, sizes) {
|
|
74
130
|
const doc = typeof child.__doc === 'string' ? JSON.parse(child.__doc) : child.__doc;
|
|
75
131
|
for (const column of node.entityMapping.columns) {
|
|
76
132
|
if (column.source === 'epoch(document)') continue;
|
|
@@ -92,10 +148,10 @@ function parseGraphChild(node, child) {
|
|
|
92
148
|
doc[include.name] = include.many ? [] : null;
|
|
93
149
|
continue;
|
|
94
150
|
}
|
|
95
|
-
const parsed =
|
|
151
|
+
const parsed = checkBounds(node, include, child, raw, sizes);
|
|
96
152
|
doc[include.name] = include.many
|
|
97
|
-
? parsed.map((grandchild) => parseGraphChild(include.child, grandchild))
|
|
98
|
-
: parseGraphChild(include.child, parsed);
|
|
153
|
+
? parsed.map((grandchild) => parseGraphChild(include.child, grandchild, sizes))
|
|
154
|
+
: parseGraphChild(include.child, parsed, sizes);
|
|
99
155
|
}
|
|
100
156
|
return doc;
|
|
101
157
|
}
|
package/src/index.js
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
/**
|
|
3
|
-
* @file @jarenjs/db — document storage
|
|
4
|
-
* a
|
|
5
|
-
* `/
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* @file @jarenjs/db — document storage through two seams: a driver (how
|
|
4
|
+
* a connection is made: `@jarenjs/db/node`, `/bun`, `/wasm` with an
|
|
5
|
+
* injected handle, or `/postgres` with an injected client) and a
|
|
6
|
+
* dialect (how SQL is spelled). Two engines satisfy both — SQLite and
|
|
7
|
+
* PostgreSQL 16+ — and what differs between them is a declared
|
|
8
|
+
* capability rather than a discovered surprise.
|
|
9
|
+
*
|
|
10
|
+
* This root subpath never touches a runtime builtin and imports no
|
|
11
|
+
* third-party client — a browser bundler resolves it clean; the
|
|
12
|
+
* bindings live behind their own subpaths and load their builtin
|
|
13
|
+
* lazily inside `open()`.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
export { openStore, normalizeModel, MODEL_VERSION } from './store.js';
|
|
12
|
-
export { createDialect } from './dialect.js';
|
|
17
|
+
export { createDialect, DIALECT_CAPABILITIES } from './dialect.js';
|
|
18
|
+
export { rtreeDdl } from './dialects/rtree-ddl.js';
|
|
13
19
|
export { sqliteDialect } from './dialects/sqlite.js';
|
|
20
|
+
export { PRAGMA_NAMES } from './pragmas.js';
|
|
21
|
+
export { CHECKPOINT_MODES, MAINTENANCE_OPERATIONS } from './maintenance.js';
|
|
14
22
|
export {
|
|
15
23
|
SQLITE_FLOOR, chain, toPromise, isThenable, compareVersions,
|
|
16
|
-
openConnection,
|
|
24
|
+
openConnection, finishConnection, sqliteProbe, baseCapabilities,
|
|
25
|
+
wrapStatement, lazyOpen,
|
|
17
26
|
} from './driver.js';
|
|
18
27
|
export {
|
|
19
|
-
planCollection, compileIndexPath, schemaTypeAt, KEY_COLUMN, DOC_COLUMN,
|
|
28
|
+
planCollection, compileIndexPath, schemaTypeAt, columnKindFor, KEY_COLUMN, DOC_COLUMN,
|
|
20
29
|
normalizeDeclaredSql, comparableDeclaredSql,
|
|
21
30
|
} from './ddl.js';
|
|
22
31
|
export {
|
|
23
32
|
planQuery, assertDecidedKind, entityShape, entityPathRef,
|
|
24
|
-
planEntityPredicate, planEntityQuery,
|
|
33
|
+
planEntityPredicate, planEntityQuery, collectEntityRoots, entityRoot,
|
|
34
|
+
PLANNER_REASONS, reasonId,
|
|
25
35
|
} from './plan.js';
|
|
26
36
|
export { emitPlan, createEntityPredicateEmitters, emitEntityPlan } from './emit.js';
|
|
27
37
|
export { mergeEntityRow, parseGraphRow } from './graph.js';
|
|
28
|
-
export {
|
|
38
|
+
export {
|
|
39
|
+
selectPlan, conjoin, assertNoSqlText, effectiveOrder, planOrder, ordersByColumn,
|
|
40
|
+
PLAN_VERSION,
|
|
41
|
+
} from './algebra.js';
|
|
29
42
|
export { typeOfPath, isNumericType } from './types.js';
|
|
30
|
-
export {
|
|
43
|
+
export {
|
|
44
|
+
compileSetResidual, compileRowResidual, compilePackedResidual, sequenceResult,
|
|
45
|
+
} from './residual.js';
|
|
46
|
+
export { createCursor, PAGE_LIMIT_DEFAULT } from './cursor.js';
|
|
31
47
|
export { deterministicFragment, registerFragment } from './udf.js';
|
|
48
|
+
export {
|
|
49
|
+
normalizeExpression, canonicalExpression, expressionMembers, expressionFunctions,
|
|
50
|
+
expressionSql, expressionStem, registeredName, registerExpressionFunctions,
|
|
51
|
+
EXPRESSION_KINDS, EXPRESSION_DEPTH,
|
|
52
|
+
} from './expression.js';
|
|
32
53
|
export {
|
|
33
54
|
DERIVE_KINDS, DERIVE_MAPPING, PHYSICAL_KINDS, BBOX_COMPONENTS, BBOX_INDEX_ORDER,
|
|
34
55
|
PRECISION_MIN, PRECISION_MAX, DIMS_MIN, DIMS_MAX,
|
|
@@ -38,34 +59,42 @@ export {
|
|
|
38
59
|
export { KNN_MARGIN, IDENTITY_CHUNK, cutCandidates, identityBatches } from './knn.js';
|
|
39
60
|
export {
|
|
40
61
|
createQueryEngine, createQueryState, createEntityQueryEngine,
|
|
41
|
-
createLoadEngine, INCLUDE_DEPTH_DEFAULT,
|
|
62
|
+
createLoadEngine, INCLUDE_DEPTH_DEFAULT, INCLUDE_ROWS_DEFAULT, INCLUDE_BYTES_DEFAULT,
|
|
42
63
|
} from './query.js';
|
|
43
64
|
export {
|
|
44
65
|
normalizeProfile, SAFE_PROFILE, translateProfilePredicate,
|
|
45
66
|
applyMandatoryPredicate, applyRowBound,
|
|
46
67
|
} from './profile.js';
|
|
47
68
|
export { translatePatch } from './patch-sql.js';
|
|
48
|
-
export { normalizeEntities, explainMapping } from './model.js';
|
|
69
|
+
export { normalizeEntities, explainMapping, relationTables } from './model.js';
|
|
49
70
|
export { planEntity, planJoinTable } from './ddl.js';
|
|
50
71
|
export { entityCore } from './entity.js';
|
|
51
72
|
export { entityEmitModel } from './emit-model.js';
|
|
52
73
|
export {
|
|
53
74
|
parseChangeset, translateOperations, keyToken, createCaptureEngine,
|
|
54
|
-
CHANGES_TABLE, DEFAULT_RETENTION,
|
|
75
|
+
CHANGES_TABLE, CHANGES_STATE_TABLE, DEFAULT_RETENTION,
|
|
55
76
|
} from './capture.js';
|
|
56
77
|
export {
|
|
57
78
|
createTracker, deepFreeze, BATCH_PARAM_BUDGET, BATCH_ROW_BOUND,
|
|
58
79
|
} from './tracker.js';
|
|
59
80
|
export {
|
|
60
|
-
planMigration, planModelMigration, migrate, migrationStatus, shapeHash,
|
|
81
|
+
planMigration, planModelMigration, migrate, migrationStatus, shapeHash, isPerDocumentAssertion,
|
|
61
82
|
migrationChecksum, createModelShape, schemaShapeOf, compareShapeToModel,
|
|
62
|
-
MIGRATION_VERSION, HISTORY_TABLE,
|
|
83
|
+
MIGRATION_VERSION, HISTORY_TABLE, ASSERTION_BOUNDS_DEFAULT,
|
|
63
84
|
} from './migrate.js';
|
|
64
|
-
export {
|
|
85
|
+
export { introspectModel, readSchema, INTROSPECT_CODES } from './introspect.js';
|
|
86
|
+
export { migrateDocuments, streamDocuments } from './documents.js';
|
|
87
|
+
export {
|
|
88
|
+
compileDocumentStep, checkMigrationDocument, stepFailure, classifyAssertion,
|
|
89
|
+
DOCUMENT_STEP_KINDS, PHYSICAL_STEP_KINDS,
|
|
90
|
+
} from './document-steps.js';
|
|
91
|
+
export {
|
|
92
|
+
DbCompileError, DbRuntimeError, DB_CODES, classifyDriverError, wrapDriverError, isDriverError,
|
|
93
|
+
} from './errors.js';
|
|
65
94
|
export { classifyLiveQuery, createLiveRegistry, diffRows, LIVE_DEFAULTS } from './live.js';
|
|
66
95
|
export { createSortedWindow, compareCodepoint } from './window.js';
|
|
67
96
|
export {
|
|
68
97
|
createJobEngine, JOBS_TABLE, JOB_CHECKPOINTS_TABLE, JOB_DEFAULTS,
|
|
69
98
|
describeValue, serializeResult,
|
|
70
99
|
} from './jobs.js';
|
|
71
|
-
export { createDagJobRunner } from './dag-job.js';
|
|
100
|
+
export { createDagJobRunner, RUN_IDENTITY_NODE } from './dag-job.js';
|