@jarenjs/db 0.46.4 → 0.49.2
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 +106 -2
- package/README.md +145 -2
- package/dist/types/algebra.d.ts +34 -3
- package/dist/types/dialect.d.ts +5 -0
- package/dist/types/errors.d.ts +3 -0
- package/dist/types/live-time.d.ts +141 -0
- package/dist/types/live.d.ts +3 -1
- package/dist/types/plan.d.ts +2 -0
- package/dist/types/query.d.ts +2 -1
- package/dist/types/residual.d.ts +5 -2
- package/dist/types/series.d.ts +227 -0
- package/dist/types/store.d.ts +8 -1
- package/docs/LIVE-FORMAT.md +103 -0
- package/docs/MODEL-FORMAT.md +19 -0
- package/package.json +4 -4
- package/src/algebra.js +22 -3
- package/src/dialect.js +13 -0
- package/src/dialects/sqlite.js +19 -0
- package/src/emit.js +32 -6
- package/src/errors.js +3 -0
- package/src/live-time.js +596 -0
- package/src/live.js +41 -8
- package/src/plan.js +706 -16
- package/src/query.js +160 -11
- package/src/residual.js +15 -6
- package/src/series.js +349 -0
- package/src/store.js +22 -3
- package/types/index.d.ts +107 -13
- package/types/typed.d.ts +4 -4
package/src/query.js
CHANGED
|
@@ -58,15 +58,22 @@ import {
|
|
|
58
58
|
* residual.
|
|
59
59
|
* @param {number} [bound]
|
|
60
60
|
* @param {{ functions?: any, extensions?: any } | null} [operators]
|
|
61
|
+
* @param {any} [zoneProvider] - D7's injected clock, or absent
|
|
61
62
|
* @returns {any}
|
|
62
63
|
*/
|
|
63
|
-
export function createQueryState(bound = undefined, operators = null
|
|
64
|
+
export function createQueryState(bound = undefined, operators = null,
|
|
65
|
+
zoneProvider = undefined) {
|
|
64
66
|
return {
|
|
65
67
|
cache: createSemanticCache(bound ?? 128),
|
|
66
68
|
counters: { hits: 0, misses: 0, evictions: 0 },
|
|
67
69
|
/** Fragment identity → the SQL function name registered for it. */
|
|
68
70
|
registered: new Map(),
|
|
69
71
|
operators: operators ?? null,
|
|
72
|
+
// D7's injected clock: a named zone is host code a database does
|
|
73
|
+
// not have, so a calendar ladder over one walks in the residual —
|
|
74
|
+
// and the residual is the caller's OWN document, so the frozen spec
|
|
75
|
+
// reaches the kernel unchanged rather than being rebuilt in UTC
|
|
76
|
+
zoneProvider: zoneProvider ?? null,
|
|
70
77
|
};
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -135,11 +142,16 @@ export function createQueryEngine(context) {
|
|
|
135
142
|
// residual compilation here. `null` when the store opened with no
|
|
136
143
|
// registry — the whole engine is then byte-identical to before.
|
|
137
144
|
const operators = state.operators ?? null;
|
|
145
|
+
const zoneProvider = state.zoneProvider ?? null;
|
|
138
146
|
const dialect = connection.dialect;
|
|
139
147
|
const shape = {
|
|
140
148
|
collection: collection.name,
|
|
141
149
|
schema: collection.schema,
|
|
142
150
|
columnByCanonical: physicalPlan.columnByCanonical,
|
|
151
|
+
// the DECLARED indexes, in their declared column order: a temporal
|
|
152
|
+
// plan is recognized by the index a model already has, so the
|
|
153
|
+
// planner needs to see the list rather than guess a column's role
|
|
154
|
+
indexes: physicalPlan.expected?.indexes ?? [],
|
|
143
155
|
// the R*Tree virtual tables this collection's `bbox` column sets are
|
|
144
156
|
// realized as, by stem — empty under `physical: 'columns'`, and
|
|
145
157
|
// empty on a driver whose build carries no R*Tree module, because
|
|
@@ -163,6 +175,14 @@ export function createQueryEngine(context) {
|
|
|
163
175
|
* a k-nearest query into a full scan that `explain()` still calls
|
|
164
176
|
* `knn`, which is the one thing this mode may not do quietly. */
|
|
165
177
|
const knnStats = { queries: 0, rows: 0, candidates: 0, fullFetches: 0, diverted: 0 };
|
|
178
|
+
/** The temporal counters `stats()` reports, and the ONLY place a
|
|
179
|
+
* candidate or a result count comes from: `explain()` reads the LAST
|
|
180
|
+
* ACTUAL run's numbers off the cache entry rather than estimating
|
|
181
|
+
* any of them. `statements` is what makes the as-of bound checkable —
|
|
182
|
+
* one narrowing fetch per call, whatever the probes number — and
|
|
183
|
+
* `diverted` counts the calls whose native bucket met a group with no
|
|
184
|
+
* instant and handed the whole question back to the engine. */
|
|
185
|
+
const seriesStats = { queries: 0, statements: 0, candidates: 0, results: 0, diverted: 0 };
|
|
166
186
|
/** The by-identities fetch statements, one per batch size. */
|
|
167
187
|
const identityFetch = new Map();
|
|
168
188
|
|
|
@@ -171,13 +191,15 @@ export function createQueryEngine(context) {
|
|
|
171
191
|
const residualCompileOptions = (limits) => {
|
|
172
192
|
const functions = operators?.functions;
|
|
173
193
|
const extensions = operators?.extensions;
|
|
174
|
-
if (limits === undefined && functions === undefined && extensions === undefined
|
|
194
|
+
if (limits === undefined && functions === undefined && extensions === undefined
|
|
195
|
+
&& zoneProvider === null)
|
|
175
196
|
return undefined;
|
|
176
197
|
/** @type {any} */
|
|
177
198
|
const options = {};
|
|
178
199
|
if (limits !== undefined) options.limits = limits;
|
|
179
200
|
if (functions !== undefined) options.functions = functions;
|
|
180
201
|
if (extensions !== undefined) options.extensions = extensions;
|
|
202
|
+
if (zoneProvider !== null) options.zoneProvider = zoneProvider;
|
|
181
203
|
return options;
|
|
182
204
|
};
|
|
183
205
|
|
|
@@ -302,10 +324,13 @@ export function createQueryEngine(context) {
|
|
|
302
324
|
setResidual: null,
|
|
303
325
|
packedResidual: null,
|
|
304
326
|
rowResidual: planned.mode === 'row'
|
|
305
|
-
? compileRowResidual(planned.rowReturn, limits, operators)
|
|
327
|
+
? compileRowResidual(planned.rowReturn, limits, operators, zoneProvider)
|
|
306
328
|
: null,
|
|
307
329
|
fullScanSql: null,
|
|
308
330
|
fullScanShape: () => shapePlan(selectPlan(collection.name)),
|
|
331
|
+
// the LAST actual execution's numbers, never an estimate: `null`
|
|
332
|
+
// until this document has run once
|
|
333
|
+
seriesCounts: null,
|
|
309
334
|
};
|
|
310
335
|
|
|
311
336
|
const sizeBefore = state.cache.size();
|
|
@@ -320,7 +345,8 @@ export function createQueryEngine(context) {
|
|
|
320
345
|
};
|
|
321
346
|
const setResidualOf = (entry, document) => {
|
|
322
347
|
if (entry.setResidual === null)
|
|
323
|
-
entry.setResidual = compileSetResidual(document, entry.residualLimits, operators
|
|
348
|
+
entry.setResidual = compileSetResidual(document, entry.residualLimits, operators,
|
|
349
|
+
zoneProvider);
|
|
324
350
|
return entry.setResidual;
|
|
325
351
|
};
|
|
326
352
|
/** The item-packing variant for cursors: `[document]` packs the
|
|
@@ -397,6 +423,68 @@ export function createQueryEngine(context) {
|
|
|
397
423
|
|
|
398
424
|
const rowsToDocs = (rows) => rows.map((row) => JSON.parse(row.doc));
|
|
399
425
|
|
|
426
|
+
/**
|
|
427
|
+
* The bucket records a native temporal group answers: the ladder's
|
|
428
|
+
* start under the name the document asked for it by, then one member
|
|
429
|
+
* per aggregate with the caller's own word for "no numbers".
|
|
430
|
+
*
|
|
431
|
+
* `null` when a group has no instant at all — a row whose instant
|
|
432
|
+
* member is missing or is not a number groups under SQL `NULL`, and
|
|
433
|
+
* the kernel REFUSES such a row (`JQ2001`). SQL cannot refuse, so the
|
|
434
|
+
* call diverts and the engine answers, exactly as it does everywhere.
|
|
435
|
+
* @param {any} entry
|
|
436
|
+
* @param {any[]} rows
|
|
437
|
+
* @returns {any[] | null}
|
|
438
|
+
*/
|
|
439
|
+
const bucketItems = (entry, rows) => {
|
|
440
|
+
const bucket = entry.plan.bucket;
|
|
441
|
+
const items = [];
|
|
442
|
+
for (const row of rows) {
|
|
443
|
+
const start = row[bucket.as] ?? null;
|
|
444
|
+
if (start === null) return null;
|
|
445
|
+
/** @type {any} */
|
|
446
|
+
const item = { [bucket.as]: start };
|
|
447
|
+
for (const aggregate of bucket.aggregates) {
|
|
448
|
+
const value = row[aggregate.as] ?? null;
|
|
449
|
+
if (value === null && aggregate.empty === 'omit') continue;
|
|
450
|
+
item[aggregate.as] = value === null && aggregate.empty === 'zero' ? 0 : value;
|
|
451
|
+
}
|
|
452
|
+
items.push(item);
|
|
453
|
+
}
|
|
454
|
+
return items;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/** How many ITEMS an engine result carries (its own shape rule). */
|
|
458
|
+
const itemCount = (answer) => (answer === undefined ? 0
|
|
459
|
+
: Array.isArray(answer) ? answer.length : 1);
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* A native bucket met a group with no instant. The kernel refuses
|
|
463
|
+
* such a row, and SQL cannot, so the whole question goes back to the
|
|
464
|
+
* engine over the whole collection — the same diversion a k-nearest
|
|
465
|
+
* probe of the wrong width takes, and counted the same way.
|
|
466
|
+
*/
|
|
467
|
+
const divertBucket = (entry, document, externals) => {
|
|
468
|
+
seriesStats.diverted++;
|
|
469
|
+
return chain(fullScanOf(entry), (statement) =>
|
|
470
|
+
chain(statement.all(fullScanParams(entry)), (rows) => {
|
|
471
|
+
const docs = rowsToDocs(checkRowBound(entry, rows));
|
|
472
|
+
const answer = setResidualOf(entry, document)(docs, externals);
|
|
473
|
+
countSeries(entry, 2, docs.length, itemCount(answer));
|
|
474
|
+
return answer;
|
|
475
|
+
}));
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/** Record one actual execution against the entry and the store. */
|
|
479
|
+
const countSeries = (entry, statements, candidates, results) => {
|
|
480
|
+
if (entry.planned.series === null) return;
|
|
481
|
+
seriesStats.queries++;
|
|
482
|
+
seriesStats.statements += statements;
|
|
483
|
+
seriesStats.candidates += candidates;
|
|
484
|
+
seriesStats.results += results;
|
|
485
|
+
entry.seriesCounts = { statements, candidates, results };
|
|
486
|
+
};
|
|
487
|
+
|
|
400
488
|
/**
|
|
401
489
|
* The documents of the given row identities, in identity order,
|
|
402
490
|
* through the dialect's by-identities statement — batched, and each
|
|
@@ -512,8 +600,11 @@ export function createQueryEngine(context) {
|
|
|
512
600
|
// the k-nearest cut — and the full document over them, which
|
|
513
601
|
// re-applies its own predicates and ordering (idempotent
|
|
514
602
|
// narrowing: the fetch decided nothing)
|
|
515
|
-
return chain(candidatesOf(entry, externals, diverted), (docs) =>
|
|
516
|
-
setResidualOf(entry, document)(docs, externals)
|
|
603
|
+
return chain(candidatesOf(entry, externals, diverted), (docs) => {
|
|
604
|
+
const answer = setResidualOf(entry, document)(docs, externals);
|
|
605
|
+
countSeries(entry, 1, docs.length, itemCount(answer));
|
|
606
|
+
return answer;
|
|
607
|
+
});
|
|
517
608
|
}
|
|
518
609
|
if (entry.planned.mode === 'row') {
|
|
519
610
|
return chain(statementOf(entry), (statement) =>
|
|
@@ -529,8 +620,19 @@ export function createQueryEngine(context) {
|
|
|
529
620
|
return chain(statement.get(bindParams(entry, externals)),
|
|
530
621
|
(row) => aggregateResult(entry, row));
|
|
531
622
|
}
|
|
532
|
-
|
|
533
|
-
|
|
623
|
+
if (entry.plan.bucket !== null) {
|
|
624
|
+
return chain(statement.all(bindParams(entry, externals)), (rows) => {
|
|
625
|
+
const items = bucketItems(entry, checkRowBound(entry, rows));
|
|
626
|
+
if (items === null) return divertBucket(entry, document, externals);
|
|
627
|
+
countSeries(entry, 1, rows.length, items.length);
|
|
628
|
+
return sequenceResult(items);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return chain(statement.all(bindParams(entry, externals)), (rows) => {
|
|
632
|
+
const docs = rowsToDocs(checkRowBound(entry, rows));
|
|
633
|
+
countSeries(entry, 1, docs.length, docs.length);
|
|
634
|
+
return sequenceResult(docs);
|
|
635
|
+
});
|
|
534
636
|
});
|
|
535
637
|
});
|
|
536
638
|
};
|
|
@@ -571,6 +673,7 @@ export function createQueryEngine(context) {
|
|
|
571
673
|
chain(candidatesOf(entry, externals, diverted), (docs) => {
|
|
572
674
|
buffered = packedResidualOf(entry, document)(docs, externals);
|
|
573
675
|
bufferedAt = 0;
|
|
676
|
+
countSeries(entry, 1, docs.length, buffered.length);
|
|
574
677
|
})));
|
|
575
678
|
}
|
|
576
679
|
return materialized.then(() => {
|
|
@@ -579,6 +682,33 @@ export function createQueryEngine(context) {
|
|
|
579
682
|
return { done: true, value: undefined };
|
|
580
683
|
});
|
|
581
684
|
}
|
|
685
|
+
if (entry.plan.bucket !== null) {
|
|
686
|
+
// a native bucket is a barrier: the groups are the answer
|
|
687
|
+
if (materialized === null) {
|
|
688
|
+
materialized = Promise.resolve(chain(guardScan(entry), () =>
|
|
689
|
+
chain(statementOf(entry), (statement) =>
|
|
690
|
+
chain(statement.all(bindParams(entry, externals)), (rows) => {
|
|
691
|
+
const items = bucketItems(entry, checkRowBound(entry, rows));
|
|
692
|
+
if (items === null) {
|
|
693
|
+
const answer = divertBucket(entry, document, externals);
|
|
694
|
+
return chain(answer, (value) => {
|
|
695
|
+
buffered = value === undefined ? []
|
|
696
|
+
: Array.isArray(value) ? value : [value];
|
|
697
|
+
bufferedAt = 0;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
countSeries(entry, 1, rows.length, items.length);
|
|
701
|
+
buffered = items;
|
|
702
|
+
bufferedAt = 0;
|
|
703
|
+
return null;
|
|
704
|
+
}))));
|
|
705
|
+
}
|
|
706
|
+
return materialized.then(() => {
|
|
707
|
+
if (bufferedAt < buffered.length) return nextFromBuffer();
|
|
708
|
+
done = true;
|
|
709
|
+
return { done: true, value: undefined };
|
|
710
|
+
});
|
|
711
|
+
}
|
|
582
712
|
if (entry.plan.aggregate !== null) {
|
|
583
713
|
// a native aggregate yields exactly one item
|
|
584
714
|
if (materialized === null) {
|
|
@@ -639,7 +769,9 @@ export function createQueryEngine(context) {
|
|
|
639
769
|
* The explanation record: the engine explain shape plus the pushdown
|
|
640
770
|
* facts. `estimatedRows` is deliberately ABSENT — the capability
|
|
641
771
|
* slot is empty on SQLite and no number is fabricated; the
|
|
642
|
-
* database's own plan prose rides in `scanNarrative` instead.
|
|
772
|
+
* database's own plan prose rides in `scanNarrative` instead. So is
|
|
773
|
+
* every count in `series`: they are the LAST ACTUAL execution's, and
|
|
774
|
+
* `null` until this document has run once.
|
|
643
775
|
* @param {any} document
|
|
644
776
|
* @param {{ externals?: any, strict?: boolean }} [options]
|
|
645
777
|
*/
|
|
@@ -668,6 +800,12 @@ export function createQueryEngine(context) {
|
|
|
668
800
|
if (term.ref.column !== null) touchedColumns.add(term.ref.column);
|
|
669
801
|
}
|
|
670
802
|
if (entry.plan.aggregate?.ref?.column) touchedColumns.add(entry.plan.aggregate.ref.column);
|
|
803
|
+
if (entry.plan.bucket !== null) {
|
|
804
|
+
if (entry.plan.bucket.ref.column) touchedColumns.add(entry.plan.bucket.ref.column);
|
|
805
|
+
for (const aggregate of entry.plan.bucket.aggregates) {
|
|
806
|
+
if (aggregate.ref?.column) touchedColumns.add(aggregate.ref.column);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
671
809
|
const indexes = [
|
|
672
810
|
...physicalPlan.expected.indexes
|
|
673
811
|
.filter((index) => index.columns.some((column) => touchedColumns.has(column)))
|
|
@@ -718,11 +856,21 @@ export function createQueryEngine(context) {
|
|
|
718
856
|
? entry.planned.reasons.map((r) => ({ operator: r.construct, reason: r.reason }))
|
|
719
857
|
: [],
|
|
720
858
|
udfs: [...entry.planned.udfs],
|
|
859
|
+
// the temporal record: what the document asked, which declared
|
|
860
|
+
// index the fetch seeks through, and which kernel finished it.
|
|
861
|
+
// The counts are the LAST ACTUAL execution's — `null` before
|
|
862
|
+
// this document has run — because an estimate mislabelled as a
|
|
863
|
+
// count is exactly the thing an honest explain may not print
|
|
864
|
+
series: entry.planned.series === null ? null : {
|
|
865
|
+
...entry.planned.series,
|
|
866
|
+
counts: entry.seriesCounts === null ? null : { ...entry.seriesCounts },
|
|
867
|
+
},
|
|
721
868
|
scanNarrative: rows.map((row) => String(row.detail)).join('; '),
|
|
722
869
|
})));
|
|
723
870
|
};
|
|
724
871
|
|
|
725
|
-
return { execute, query, explain, shape,
|
|
872
|
+
return { execute, query, explain, shape,
|
|
873
|
+
stats: () => ({ knn: { ...knnStats }, series: { ...seriesStats } }) };
|
|
726
874
|
}
|
|
727
875
|
|
|
728
876
|
// ————— The entity query surface (the second document kind) —————
|
|
@@ -745,6 +893,7 @@ export const INCLUDE_DEPTH_DEFAULT = 3;
|
|
|
745
893
|
export function createEntityQueryEngine(context) {
|
|
746
894
|
const { connection, entities, mapping, state } = context;
|
|
747
895
|
const operators = state.operators ?? null;
|
|
896
|
+
const zoneProvider = state.zoneProvider ?? null;
|
|
748
897
|
const dialect = connection.dialect;
|
|
749
898
|
const q = dialect.quoteIdentifier;
|
|
750
899
|
const physicalOf = (name) => ({ table: mapping.entities[name].table });
|
|
@@ -810,7 +959,7 @@ export function createEntityQueryEngine(context) {
|
|
|
810
959
|
|
|
811
960
|
const runResidual = (entry, document, externals) => {
|
|
812
961
|
if (entry.setResidual === null)
|
|
813
|
-
entry.setResidual = compileSetResidual(document, undefined, operators);
|
|
962
|
+
entry.setResidual = compileSetResidual(document, undefined, operators, zoneProvider);
|
|
814
963
|
return chain(fetchRoot(entry), (root) => entry.setResidual(root, externals));
|
|
815
964
|
};
|
|
816
965
|
|
package/src/residual.js
CHANGED
|
@@ -36,16 +36,22 @@ import { compileJsonQuery } from '@jarenjs/json/query';
|
|
|
36
36
|
* @param {{ functions?: any, extensions?: any } | null} [operators]
|
|
37
37
|
* @returns {any}
|
|
38
38
|
*/
|
|
39
|
-
function residualOptions(limits, operators) {
|
|
39
|
+
function residualOptions(limits, operators, zoneProvider) {
|
|
40
40
|
const functions = operators?.functions;
|
|
41
41
|
const extensions = operators?.extensions;
|
|
42
|
-
|
|
42
|
+
const clock = zoneProvider ?? null;
|
|
43
|
+
if (limits === undefined && functions === undefined && extensions === undefined
|
|
44
|
+
&& clock === null)
|
|
43
45
|
return undefined;
|
|
44
46
|
/** @type {any} */
|
|
45
47
|
const options = {};
|
|
46
48
|
if (limits !== undefined) options.limits = limits;
|
|
47
49
|
if (functions !== undefined) options.functions = functions;
|
|
48
50
|
if (extensions !== undefined) options.extensions = extensions;
|
|
51
|
+
// D7's injected clock: without it a named zone is a compile refusal,
|
|
52
|
+
// which is what the language wants — being right for eight months of
|
|
53
|
+
// the year is exactly what a silent UTC fallback would be
|
|
54
|
+
if (clock !== null) options.zoneProvider = clock;
|
|
49
55
|
return options;
|
|
50
56
|
}
|
|
51
57
|
|
|
@@ -57,10 +63,12 @@ function residualOptions(limits, operators) {
|
|
|
57
63
|
* @param {any} [limits]
|
|
58
64
|
* @param {{ functions?: any, extensions?: any } | null} [operators] -
|
|
59
65
|
* the store's registered operators, so the residual can evaluate them
|
|
66
|
+
* @param {any} [zoneProvider] - D7's injected clock, so a calendar
|
|
67
|
+
* ladder on a named zone compiles rather than being refused
|
|
60
68
|
* @returns {(candidates: any[], externals: any) => any}
|
|
61
69
|
*/
|
|
62
|
-
export function compileSetResidual(document, limits, operators) {
|
|
63
|
-
const compiled = compileJsonQuery(document, residualOptions(limits, operators));
|
|
70
|
+
export function compileSetResidual(document, limits, operators, zoneProvider) {
|
|
71
|
+
const compiled = compileJsonQuery(document, residualOptions(limits, operators, zoneProvider));
|
|
64
72
|
return (candidates, externals) => compiled(candidates, externals);
|
|
65
73
|
}
|
|
66
74
|
|
|
@@ -73,10 +81,11 @@ export function compileSetResidual(document, limits, operators) {
|
|
|
73
81
|
* the only place that knows the name.
|
|
74
82
|
* @param {any} [limits]
|
|
75
83
|
* @param {{ functions?: any, extensions?: any } | null} [operators]
|
|
84
|
+
* @param {any} [zoneProvider] - D7's injected clock
|
|
76
85
|
* @returns {(row: any, externals: any) => any[]} the row's items
|
|
77
86
|
*/
|
|
78
|
-
export function compileRowResidual(rowDocument, limits, operators) {
|
|
79
|
-
const compiled = compileJsonQuery(rowDocument, residualOptions(limits, operators));
|
|
87
|
+
export function compileRowResidual(rowDocument, limits, operators, zoneProvider) {
|
|
88
|
+
const compiled = compileJsonQuery(rowDocument, residualOptions(limits, operators, zoneProvider));
|
|
80
89
|
return (row, externals) => {
|
|
81
90
|
const packed = compiled([row], externals);
|
|
82
91
|
// one binding → exactly one packed array of that row's items
|