@jarenjs/db 0.34.0 → 0.43.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 +115 -12
- package/README.md +47 -0
- package/dist/types/algebra.d.ts +38 -2
- package/dist/types/ddl.d.ts +40 -6
- package/dist/types/derive.d.ts +161 -0
- package/dist/types/dialect.d.ts +11 -2
- package/dist/types/driver.d.ts +2 -20
- package/dist/types/emit.d.ts +6 -3
- package/dist/types/errors.d.ts +6 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/jobs.d.ts +7 -1
- package/dist/types/migrate.d.ts +7 -1
- package/dist/types/plan.d.ts +15 -1
- package/dist/types/residual.d.ts +17 -6
- package/dist/types/udf.d.ts +6 -1
- package/docs/JOBS-FORMAT.md +12 -1
- package/docs/MIGRATION-FORMAT.md +30 -1
- package/docs/MODEL-FORMAT.md +155 -4
- package/package.json +4 -4
- package/schemas/jaren-migration.draft-07.schema.json +71 -0
- package/schemas/jaren-migration.schema.json +71 -0
- package/schemas/jaren-model.draft-07.schema.json +14 -1
- package/schemas/jaren-model.schema.json +18 -5
- package/src/algebra.js +17 -2
- package/src/ddl.js +146 -17
- package/src/derive.js +284 -0
- package/src/dialect.js +89 -25
- package/src/dialects/sqlite.js +16 -1
- package/src/driver.js +6 -28
- package/src/emit.js +122 -22
- package/src/errors.js +6 -4
- package/src/index.js +5 -0
- package/src/jobs.js +36 -7
- package/src/migrate.js +132 -19
- package/src/plan.js +514 -32
- package/src/query.js +61 -9
- package/src/residual.js +18 -10
- package/src/store.js +122 -8
- package/src/udf.js +12 -3
package/src/query.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { emitPlan, emitEntityPlan, createEntityPredicateEmitters } from './emit.js';
|
|
32
32
|
import { selectPlan } from './algebra.js';
|
|
33
33
|
import { compileSetResidual, compileRowResidual, sequenceResult } from './residual.js';
|
|
34
|
+
import { derivedSlotValue, probeBox } from './derive.js';
|
|
34
35
|
import { deterministicFragment, registerFragment } from './udf.js';
|
|
35
36
|
import {
|
|
36
37
|
normalizeProfile, translateProfilePredicate,
|
|
@@ -63,6 +64,43 @@ function bindable(value) {
|
|
|
63
64
|
|| (typeof value === 'number' && Number.isFinite(value));
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The value one parameter slot binds for a call. A DERIVED slot holds
|
|
69
|
+
* no value of its own: it names one edge of a bound external's
|
|
70
|
+
* bounding box, computed here because a GeoJSON object is not
|
|
71
|
+
* something any database binds.
|
|
72
|
+
* @param {import('./emit.js').ParamSlot} slot
|
|
73
|
+
* @param {any} externals
|
|
74
|
+
* @returns {any}
|
|
75
|
+
*/
|
|
76
|
+
function slotValue(slot, externals) {
|
|
77
|
+
if ('literal' in slot) return slot.literal;
|
|
78
|
+
if ('derived' in slot)
|
|
79
|
+
return derivedSlotValue(slot.derived, externals[slot.derived.external]);
|
|
80
|
+
return externals[slot.external];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* How one external reaches the statement. An external reached ONLY
|
|
85
|
+
* through derived slots is bindable when its bound value HAS a box —
|
|
86
|
+
* the object itself never had to be bindable. One reached directly
|
|
87
|
+
* must be a string or a finite number, as before; and an external in
|
|
88
|
+
* the document that reaches no slot at all still forces the diversion,
|
|
89
|
+
* because the residual needs the engine's own semantics for it.
|
|
90
|
+
* @param {import('./emit.js').ParamSlot[]} slots
|
|
91
|
+
* @returns {Map<string, 'plain' | 'derived'>}
|
|
92
|
+
*/
|
|
93
|
+
function externalSlotKinds(slots) {
|
|
94
|
+
/** @type {Map<string, 'plain' | 'derived'>} */
|
|
95
|
+
const kinds = new Map();
|
|
96
|
+
for (const slot of slots) {
|
|
97
|
+
if ('external' in slot) kinds.set(slot.external, 'plain');
|
|
98
|
+
else if ('derived' in slot && !kinds.has(slot.derived.external))
|
|
99
|
+
kinds.set(slot.derived.external, 'derived');
|
|
100
|
+
}
|
|
101
|
+
return kinds;
|
|
102
|
+
}
|
|
103
|
+
|
|
66
104
|
/**
|
|
67
105
|
* The query engine for one collection.
|
|
68
106
|
* @param {{ connection: any, state: any, collection: any,
|
|
@@ -109,9 +147,9 @@ export function createQueryEngine(context) {
|
|
|
109
147
|
};
|
|
110
148
|
|
|
111
149
|
const udfHook = connection.capabilities.userFunctions
|
|
112
|
-
? (fragment) => {
|
|
150
|
+
? (/** @type {any} */ fragment, /** @type {string} */ binding) => {
|
|
113
151
|
// Ring 3: admit the registry's pushable:'scalar' operators too
|
|
114
|
-
const qualified = deterministicFragment(fragment, operators);
|
|
152
|
+
const qualified = deterministicFragment(fragment, operators, binding);
|
|
115
153
|
if (qualified === null) return null;
|
|
116
154
|
// the store owns the final name: a fingerprint clash between two
|
|
117
155
|
// distinct fragments is disambiguated at registration
|
|
@@ -165,6 +203,7 @@ export function createQueryEngine(context) {
|
|
|
165
203
|
reasons: [{ construct: 'pushdown', reason: 'disabled by the harness switch' }],
|
|
166
204
|
rowReturn: null,
|
|
167
205
|
udfs: [],
|
|
206
|
+
prefilters: [],
|
|
168
207
|
};
|
|
169
208
|
}
|
|
170
209
|
|
|
@@ -213,6 +252,7 @@ export function createQueryEngine(context) {
|
|
|
213
252
|
sql: emitted.sql,
|
|
214
253
|
slots: emitted.slots,
|
|
215
254
|
externalNames,
|
|
255
|
+
externalSlotKinds: externalSlotKinds(emitted.slots),
|
|
216
256
|
dependencies: planned.analysis.dependencies,
|
|
217
257
|
limits: planned.analysis.limits,
|
|
218
258
|
residualLimits: limits,
|
|
@@ -305,11 +345,13 @@ export function createQueryEngine(context) {
|
|
|
305
345
|
|
|
306
346
|
/** Bind slots against the call's externals. */
|
|
307
347
|
const bindParams = (entry, externals) =>
|
|
308
|
-
entry.slots.map((slot) => (
|
|
348
|
+
entry.slots.map((slot) => slotValue(slot, externals));
|
|
309
349
|
|
|
310
350
|
/** Must this call divert to the residual? */
|
|
311
351
|
const mustDivert = (entry, externals) =>
|
|
312
|
-
entry.externalNames.some((name) =>
|
|
352
|
+
entry.externalNames.some((name) => (entry.externalSlotKinds.get(name) === 'derived'
|
|
353
|
+
? probeBox(externals[name]) === null
|
|
354
|
+
: !bindable(externals[name])));
|
|
313
355
|
|
|
314
356
|
const rowsToDocs = (rows) => rows.map((row) => JSON.parse(row.doc));
|
|
315
357
|
|
|
@@ -499,6 +541,10 @@ export function createQueryEngine(context) {
|
|
|
499
541
|
if (pred === null) return;
|
|
500
542
|
if (pred.p === 'and' || pred.p === 'or') pred.items.forEach(collectColumns);
|
|
501
543
|
else if (pred.p === 'not') collectColumns(pred.item);
|
|
544
|
+
else if (pred.p === 'bboxOverlap')
|
|
545
|
+
for (const column of Object.values(pred.columns)) touchedColumns.add(column);
|
|
546
|
+
else if (pred.p === 'cellIn' || pred.p === 'cellPrefix')
|
|
547
|
+
touchedColumns.add(pred.column);
|
|
502
548
|
else if ('ref' in pred && pred.ref?.column) touchedColumns.add(pred.ref.column);
|
|
503
549
|
};
|
|
504
550
|
collectColumns(entry.plan.filter);
|
|
@@ -510,11 +556,15 @@ export function createQueryEngine(context) {
|
|
|
510
556
|
.filter((index) => index.columns.some((column) => touchedColumns.has(column)))
|
|
511
557
|
.map((index) => index.name);
|
|
512
558
|
|
|
513
|
-
const params = entry.slots.map((slot) =>
|
|
514
|
-
('external' in slot
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
559
|
+
const params = entry.slots.map((slot) => {
|
|
560
|
+
if ('external' in slot) return { external: slot.external };
|
|
561
|
+
if ('derived' in slot) return { derived: { ...slot.derived } };
|
|
562
|
+
return { literal: slot.literal };
|
|
563
|
+
});
|
|
564
|
+
const eqpParams = entry.slots.map((slot) => {
|
|
565
|
+
const value = slotValue(slot, externals);
|
|
566
|
+
return bindable(value) ? value : null;
|
|
567
|
+
});
|
|
518
568
|
|
|
519
569
|
return chain(connection.prepare(dialect.explainQuery(entry.sql)), (statement) =>
|
|
520
570
|
chain(statement.all(eqpParams), (rows) => ({
|
|
@@ -526,6 +576,8 @@ export function createQueryEngine(context) {
|
|
|
526
576
|
sql: entry.sql,
|
|
527
577
|
params,
|
|
528
578
|
indexes,
|
|
579
|
+
prefilters: entry.planned.prefilters.map((prefilter) => ({ ...prefilter,
|
|
580
|
+
columns: [...prefilter.columns] })),
|
|
529
581
|
residual: entry.planned.mode === 'native'
|
|
530
582
|
? null
|
|
531
583
|
: { mode: entry.planned.mode, reasons: entry.planned.reasons },
|
package/src/residual.js
CHANGED
|
@@ -10,10 +10,17 @@
|
|
|
10
10
|
* materialized candidate array. Re-applying pushed conjuncts is
|
|
11
11
|
* idempotent, so SQL-side narrowing never changes the answer.
|
|
12
12
|
* - `row` — only the projection stayed behind: each fetched row runs
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* the planner's one-row document over the one-row array; the array
|
|
14
|
+
* wrapper packs the item sequence so an array-VALUED item stays
|
|
15
|
+
* unambiguous, and the per-row results concatenate in row order
|
|
16
|
+
* (streamable).
|
|
17
|
+
*
|
|
18
|
+
* Neither mode builds a query document here. The collection binding is
|
|
19
|
+
* named by the caller's document — `it`, `user`, anything — so a wrapper
|
|
20
|
+
* synthesized in this file could only guess it, and a guess that
|
|
21
|
+
* disagreed with the projection's references would surface as an
|
|
22
|
+
* unbound-external error at request time rather than at compile time.
|
|
23
|
+
* The planner knows the name and hands both modes something complete.
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
import { compileJsonQuery } from '@jarenjs/json/query';
|
|
@@ -59,16 +66,17 @@ export function compileSetResidual(document, limits, operators) {
|
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
68
|
* Compile the per-row projection for row-mode evaluation.
|
|
62
|
-
* @param {any}
|
|
69
|
+
* @param {any} rowDocument - The planner's complete one-row document
|
|
70
|
+
* (`{ $for: { <the document's own binding>: '$[*]' },
|
|
71
|
+
* $return: [ <its $return> ] }`). It arrives whole because the binding
|
|
72
|
+
* and the projection that references it must agree, and the planner is
|
|
73
|
+
* the only place that knows the name.
|
|
63
74
|
* @param {any} [limits]
|
|
64
75
|
* @param {{ functions?: any, extensions?: any } | null} [operators]
|
|
65
76
|
* @returns {(row: any, externals: any) => any[]} the row's items
|
|
66
77
|
*/
|
|
67
|
-
export function compileRowResidual(
|
|
68
|
-
const compiled = compileJsonQuery(
|
|
69
|
-
$for: { it: '$[*]' },
|
|
70
|
-
$return: [returnExpression],
|
|
71
|
-
}, residualOptions(limits, operators));
|
|
78
|
+
export function compileRowResidual(rowDocument, limits, operators) {
|
|
79
|
+
const compiled = compileJsonQuery(rowDocument, residualOptions(limits, operators));
|
|
72
80
|
return (row, externals) => {
|
|
73
81
|
const packed = compiled([row], externals);
|
|
74
82
|
// one binding → exactly one packed array of that row's items
|
package/src/store.js
CHANGED
|
@@ -36,6 +36,10 @@ import { createCaptureEngine, DEFAULT_RETENTION } from './capture.js';
|
|
|
36
36
|
import { createLiveRegistry, classifyLiveQuery, LIVE_DEFAULTS } from './live.js';
|
|
37
37
|
import { createJobEngine } from './jobs.js';
|
|
38
38
|
import { collectEntityRoots } from './plan.js';
|
|
39
|
+
import {
|
|
40
|
+
DERIVE_KINDS, PRECISION_MIN, PRECISION_MAX, derivedValue, memberAt,
|
|
41
|
+
storedMemberForm, registerDeriveFunctions,
|
|
42
|
+
} from './derive.js';
|
|
39
43
|
|
|
40
44
|
/** The model format version this store implements. */
|
|
41
45
|
export const MODEL_VERSION = '0.1';
|
|
@@ -53,6 +57,70 @@ function modelError(code, reason, docPath) {
|
|
|
53
57
|
return new DbCompileError(code, reason, docPath);
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Normalize one index's `derive` declaration — the spatial storage
|
|
62
|
+
* vocabulary. `derive` says what is COMPUTED from the selected member,
|
|
63
|
+
* never how the member is selected, so the singular-path rule and its
|
|
64
|
+
* `JD0004` are untouched; these are the refusals a derived index adds.
|
|
65
|
+
*
|
|
66
|
+
* Every one of them is a mistake worth catching at open rather than at
|
|
67
|
+
* the first query that quietly returns nothing.
|
|
68
|
+
* @param {any} index - the declared index member
|
|
69
|
+
* @param {string[]} paths
|
|
70
|
+
* @param {string} docPath
|
|
71
|
+
* @returns {{ kind: string | null, precision: number | undefined }}
|
|
72
|
+
*/
|
|
73
|
+
function normalizeDerive(index, paths, docPath) {
|
|
74
|
+
const declared = index.derive;
|
|
75
|
+
if (declared === undefined) {
|
|
76
|
+
if (index.precision !== undefined) {
|
|
77
|
+
throw modelError('JD0004',
|
|
78
|
+
"precision belongs to a derived index — declare derive: 'geohash' beside it",
|
|
79
|
+
`${docPath}/precision`);
|
|
80
|
+
}
|
|
81
|
+
return { kind: null, precision: undefined };
|
|
82
|
+
}
|
|
83
|
+
if (typeof declared !== 'string' || !DERIVE_KINDS.has(declared)) {
|
|
84
|
+
throw modelError('JD0004',
|
|
85
|
+
`derive is a closed set ('geohash' or 'bbox'), got ${JSON.stringify(declared)} — `
|
|
86
|
+
+ 'an open expression member would be a second query language inside the model',
|
|
87
|
+
`${docPath}/derive`);
|
|
88
|
+
}
|
|
89
|
+
if (paths.length !== 1) {
|
|
90
|
+
throw modelError('JD0004',
|
|
91
|
+
`a ${declared} index derives its columns from ONE member; a composite path declares several`,
|
|
92
|
+
`${docPath}/path`);
|
|
93
|
+
}
|
|
94
|
+
if (index.unique === true) {
|
|
95
|
+
throw modelError('JD0004',
|
|
96
|
+
`a ${declared} index is never unique: distinct positions share a cell (and a box edge) by construction`,
|
|
97
|
+
`${docPath}/unique`);
|
|
98
|
+
}
|
|
99
|
+
if (declared === 'bbox') {
|
|
100
|
+
if (index.precision !== undefined) {
|
|
101
|
+
throw modelError('JD0004',
|
|
102
|
+
'precision applies to a geohash index; a bbox index has no cell size',
|
|
103
|
+
`${docPath}/precision`);
|
|
104
|
+
}
|
|
105
|
+
return { kind: 'bbox', precision: undefined };
|
|
106
|
+
}
|
|
107
|
+
const precision = index.precision;
|
|
108
|
+
if (precision === undefined) {
|
|
109
|
+
throw modelError('JD0004',
|
|
110
|
+
`a geohash index must declare precision (${PRECISION_MIN}..${PRECISION_MAX} characters) — `
|
|
111
|
+
+ 'there is no safe default: the right cell size depends on the query radius, '
|
|
112
|
+
+ 'which the model cannot know',
|
|
113
|
+
`${docPath}/precision`);
|
|
114
|
+
}
|
|
115
|
+
if (typeof precision !== 'number' || !Number.isInteger(precision)
|
|
116
|
+
|| precision < PRECISION_MIN || precision > PRECISION_MAX) {
|
|
117
|
+
throw modelError('JD0004',
|
|
118
|
+
`precision must be an integer ${PRECISION_MIN}..${PRECISION_MAX}, got ${JSON.stringify(precision)}`,
|
|
119
|
+
`${docPath}/precision`);
|
|
120
|
+
}
|
|
121
|
+
return { kind: 'geohash', precision };
|
|
122
|
+
}
|
|
123
|
+
|
|
56
124
|
/**
|
|
57
125
|
* Normalize and check a model document. Every failure is `JD0005` with
|
|
58
126
|
* a `docPath` into the model.
|
|
@@ -155,10 +223,13 @@ export function normalizeModel(model) {
|
|
|
155
223
|
'an index path must be a JSONPath string (a composite index takes a non-empty array of them)',
|
|
156
224
|
`${indexDocPath}/path`);
|
|
157
225
|
}
|
|
226
|
+
const derive = normalizeDerive(index, paths, indexDocPath);
|
|
158
227
|
indexes.push({
|
|
159
228
|
name: index.name,
|
|
160
229
|
paths,
|
|
161
230
|
unique: index.unique === true,
|
|
231
|
+
derive: derive.kind,
|
|
232
|
+
precision: derive.precision,
|
|
162
233
|
docPath: indexDocPath,
|
|
163
234
|
});
|
|
164
235
|
}
|
|
@@ -377,10 +448,39 @@ function ensureEntityShape(connection, entityPlans, entities, readOnly) {
|
|
|
377
448
|
*/
|
|
378
449
|
function collectionCore(connection, collection, plan, validate, queryState, storeProfileRef) {
|
|
379
450
|
const dialect = connection.dialect;
|
|
451
|
+
// the STORED branch (a driver that cannot index a registered
|
|
452
|
+
// function): the derived columns are ordinary ones, so every write
|
|
453
|
+
// carries their values. Empty everywhere else, and the statements are
|
|
454
|
+
// then byte-identical to what they were before derived indexes existed
|
|
455
|
+
const storedNames = new Set(plan.generated
|
|
456
|
+
.filter((column) => column.stored === true).map((column) => column.name));
|
|
457
|
+
const storedDerived = plan.derived.filter((column) => storedNames.has(column.name));
|
|
380
458
|
const shape = {
|
|
381
459
|
table: plan.table,
|
|
382
460
|
keyColumn: plan.keyColumn,
|
|
383
461
|
docColumn: plan.docColumn,
|
|
462
|
+
stored: storedDerived.length > 0
|
|
463
|
+
? storedDerived.map((column) => column.name) : undefined,
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* The stored derived values for one document, in column order. The
|
|
467
|
+
* four columns of one bbox index share a segments array by identity,
|
|
468
|
+
* so the member is read and normalized once per index, not once per
|
|
469
|
+
* column.
|
|
470
|
+
* @param {any} doc
|
|
471
|
+
* @returns {any[]}
|
|
472
|
+
*/
|
|
473
|
+
const derivedFor = (doc) => {
|
|
474
|
+
/** @type {Map<any, any>} */
|
|
475
|
+
const members = new Map();
|
|
476
|
+
return storedDerived.map((column) => {
|
|
477
|
+
let member = members.get(column.segments);
|
|
478
|
+
if (member === undefined) {
|
|
479
|
+
member = { value: storedMemberForm(memberAt(doc, column.segments)) };
|
|
480
|
+
members.set(column.segments, member);
|
|
481
|
+
}
|
|
482
|
+
return derivedValue(column, member.value);
|
|
483
|
+
});
|
|
384
484
|
};
|
|
385
485
|
/** @type {Map<string, any>} */
|
|
386
486
|
const statements = new Map();
|
|
@@ -456,12 +556,12 @@ function collectionCore(connection, collection, plan, validate, queryState, stor
|
|
|
456
556
|
if (key === null) {
|
|
457
557
|
return chain(
|
|
458
558
|
runWrite('insertAllocated', dialect.dml.insertAllocated(shape),
|
|
459
|
-
[JSON.stringify(doc)], undefined, true),
|
|
559
|
+
[JSON.stringify(doc), ...derivedFor(doc)], undefined, true),
|
|
460
560
|
(row) => row.key);
|
|
461
561
|
}
|
|
462
562
|
return chain(
|
|
463
563
|
runWrite('insert', dialect.dml.insert(shape),
|
|
464
|
-
[key, JSON.stringify(doc)], key, false),
|
|
564
|
+
[key, JSON.stringify(doc), ...derivedFor(doc)], key, false),
|
|
465
565
|
() => key);
|
|
466
566
|
},
|
|
467
567
|
put(doc, explicitKey) {
|
|
@@ -470,12 +570,12 @@ function collectionCore(connection, collection, plan, validate, queryState, stor
|
|
|
470
570
|
if (key === null) {
|
|
471
571
|
return chain(
|
|
472
572
|
runWrite('insertAllocated', dialect.dml.insertAllocated(shape),
|
|
473
|
-
[JSON.stringify(doc)], undefined, true),
|
|
573
|
+
[JSON.stringify(doc), ...derivedFor(doc)], undefined, true),
|
|
474
574
|
(row) => row.key);
|
|
475
575
|
}
|
|
476
576
|
return chain(
|
|
477
577
|
runWrite('upsert', dialect.dml.upsert(shape),
|
|
478
|
-
[key, JSON.stringify(doc)], key, false),
|
|
578
|
+
[key, JSON.stringify(doc), ...derivedFor(doc)], key, false),
|
|
479
579
|
() => key);
|
|
480
580
|
},
|
|
481
581
|
patch(key, ops) {
|
|
@@ -495,7 +595,7 @@ function collectionCore(connection, collection, plan, validate, queryState, stor
|
|
|
495
595
|
return chain(
|
|
496
596
|
runWrite('patchFallback',
|
|
497
597
|
dialect.dml.updateDoc(shape, dialect.jsonEncode(dialect.parameterRef(1, 'doc')), 2),
|
|
498
|
-
[JSON.stringify(next), key], key, false),
|
|
598
|
+
[JSON.stringify(next), ...derivedFor(next), key], key, false),
|
|
499
599
|
() => next);
|
|
500
600
|
}
|
|
501
601
|
stats.patchTranslated++;
|
|
@@ -504,7 +604,7 @@ function collectionCore(connection, collection, plan, validate, queryState, stor
|
|
|
504
604
|
const sql = dialect.dml.updateDoc(shape, expression, params.length + 1);
|
|
505
605
|
return chain(prepared(`patch:${sql}`, sql), (statement) => {
|
|
506
606
|
try {
|
|
507
|
-
statement.run([...params, key]);
|
|
607
|
+
statement.run([...params, ...derivedFor(next), key]);
|
|
508
608
|
}
|
|
509
609
|
catch (error) {
|
|
510
610
|
throw wrapWriteError(error, plan, collection.name, collection.docPath, key);
|
|
@@ -747,10 +847,23 @@ export function openStore(model, options) {
|
|
|
747
847
|
let topLevelTransaction = (fn) => withScope(opened.transaction, fn);
|
|
748
848
|
|
|
749
849
|
const dialect = connection.dialect;
|
|
850
|
+
// The PHYSICAL MAPPING BRANCH for derived index columns. A driver
|
|
851
|
+
// that can index a registered deterministic function generates
|
|
852
|
+
// them; one that cannot has the store write them. It is a
|
|
853
|
+
// property of the driver that created the file, so a database
|
|
854
|
+
// built under one and opened under the other legitimately reports
|
|
855
|
+
// drift — that is a migration, not an open.
|
|
856
|
+
const derivedMapping = connection.capabilities.deterministicIndexableFunctions === true
|
|
857
|
+
? 'virtual' : 'stored';
|
|
750
858
|
/** @type {Map<string, any>} */
|
|
751
859
|
const plans = new Map();
|
|
752
860
|
for (const [name, collection] of collections)
|
|
753
|
-
plans.set(name, planCollection(name, collection, dialect));
|
|
861
|
+
plans.set(name, planCollection(name, collection, dialect, { derived: derivedMapping }));
|
|
862
|
+
// a table whose column expression calls a function this connection
|
|
863
|
+
// has not registered cannot even be SELECTed (probed), so the
|
|
864
|
+
// registration precedes every statement over it
|
|
865
|
+
const needsDeriveFunctions = derivedMapping === 'virtual'
|
|
866
|
+
&& [...plans.values()].some((plan) => plan.derived.length > 0);
|
|
754
867
|
/** @type {Map<string, any>} */
|
|
755
868
|
const entityPlans = new Map();
|
|
756
869
|
if (mapping !== null) {
|
|
@@ -815,6 +928,7 @@ export function openStore(model, options) {
|
|
|
815
928
|
: Promise.reject(error);
|
|
816
929
|
};
|
|
817
930
|
const opening = () => chain(pragmas, () =>
|
|
931
|
+
chain(needsDeriveFunctions ? registerDeriveFunctions(connection) : null, () =>
|
|
818
932
|
chain(ensureShape(connection, collections, plans, readOnly), () =>
|
|
819
933
|
chain(ensureEntityShape(connection, entityPlans, entities, readOnly), () => {
|
|
820
934
|
/** @type {Map<string, any>} */
|
|
@@ -1408,7 +1522,7 @@ export function openStore(model, options) {
|
|
|
1408
1522
|
return chain(capture === null ? null : capture.ready,
|
|
1409
1523
|
() => chain(jobsEngine === null ? null : jobsEngine.ready,
|
|
1410
1524
|
() => Object.freeze(store)));
|
|
1411
|
-
})));
|
|
1525
|
+
}))));
|
|
1412
1526
|
|
|
1413
1527
|
let opened_;
|
|
1414
1528
|
try {
|
package/src/udf.js
CHANGED
|
@@ -55,16 +55,22 @@ const functionNameFor = (identity) => `jaren_p_${hashContent(identity)}`;
|
|
|
55
55
|
* [operators] - the store's registered operators (Ring 3); only its
|
|
56
56
|
* `pushable:'scalar'` subset is admitted. `null`/absent keeps the
|
|
57
57
|
* original engine-internal-only rule (no host function pushes).
|
|
58
|
+
* @param {string} [binding='it'] - the name the caller's document gave
|
|
59
|
+
* the collection binding. The fragment references it, so the wrapper
|
|
60
|
+
* below must bind it: under any other name every reference reads as an
|
|
61
|
+
* external, the determinism check below rejects the fragment, and the
|
|
62
|
+
* hatch silently never engages.
|
|
58
63
|
* @returns {{ key: string, name: string,
|
|
59
64
|
* compile: () => (docText: string) => number } | null}
|
|
60
65
|
*/
|
|
61
|
-
export function deterministicFragment(fragment, operators = null) {
|
|
66
|
+
export function deterministicFragment(fragment, operators = null, binding = 'it') {
|
|
62
67
|
const analyzeOpts = operators === null
|
|
63
68
|
? undefined
|
|
64
69
|
: { functions: operators.functions, extensions: operators.extensions };
|
|
70
|
+
const wrap = (/** @type {any} */ body) => ({ $let: { [binding]: '$' }, $return: body });
|
|
65
71
|
let dependencies;
|
|
66
72
|
try {
|
|
67
|
-
dependencies = analyzeQuery(
|
|
73
|
+
dependencies = analyzeQuery(wrap(fragment), analyzeOpts).dependencies;
|
|
68
74
|
}
|
|
69
75
|
catch {
|
|
70
76
|
return null;
|
|
@@ -101,7 +107,10 @@ export function deterministicFragment(fragment, operators = null) {
|
|
|
101
107
|
key,
|
|
102
108
|
name: functionNameFor(key),
|
|
103
109
|
compile: () => {
|
|
104
|
-
|
|
110
|
+
// the SAME wrapper the analysis ran over: a compile that bound a
|
|
111
|
+
// different name than the analysis would judge one document and
|
|
112
|
+
// run another
|
|
113
|
+
const compiled = compileJsonQuery(wrap(fragment), analyzeOpts);
|
|
105
114
|
return (docText) => (compiled.ebv(JSON.parse(docText)) ? 1 : 0);
|
|
106
115
|
},
|
|
107
116
|
};
|