@jarenjs/db 0.73.0 → 0.83.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 +70 -7
- package/README.md +69 -6
- package/docs/HOSTS.md +17 -0
- package/docs/JOBS-FORMAT.md +26 -0
- package/docs/LIVE-FORMAT.md +52 -13
- package/docs/MIGRATION-FORMAT.md +34 -0
- package/docs/MODEL-FORMAT.md +163 -15
- package/docs/NATIVE-PLANS.md +111 -0
- package/docs/REPLICATION-FORMAT.md +19 -13
- package/docs/SEARCH.md +55 -0
- package/package.json +8 -4
- package/schemas/jaren-migration.draft-07.schema.json +54 -5
- package/schemas/jaren-migration.schema.json +49 -0
- package/schemas/jaren-model.authoring.schema.json +360 -0
- package/schemas/jaren-model.draft-07.schema.json +128 -0
- package/schemas/jaren-model.schema.json +128 -0
- package/src/algebra.js +26 -4
- package/src/backup.js +12 -7
- package/src/cursor.js +27 -4
- package/src/dag-job.js +2 -1
- package/src/ddl.js +13 -0
- package/src/derive.js +14 -3
- package/src/dialect.js +12 -0
- package/src/dialects/check-read.js +151 -0
- package/src/dialects/invariant-sql.js +117 -0
- package/src/dialects/postgres.js +28 -4
- package/src/dialects/sqlite.js +23 -3
- package/src/driver.js +1 -0
- package/src/drivers/bun.js +22 -4
- package/src/emit.js +133 -25
- package/src/entity.js +98 -41
- package/src/errors.js +8 -0
- package/src/graph.js +8 -1
- package/src/index.js +3 -0
- package/src/introspect.js +81 -12
- package/src/invariants.js +45 -0
- package/src/jobs.js +39 -6
- package/src/live-nested.js +27 -10
- package/src/live.js +51 -136
- package/src/migrate.js +136 -22
- package/src/model.js +12 -0
- package/src/mutation.js +165 -0
- package/src/physical.js +147 -0
- package/src/plan.js +275 -64
- package/src/query.js +175 -78
- package/src/search.js +144 -0
- package/src/sql.js +60 -0
- package/src/store.js +49 -13
- package/src/tracker.js +63 -39
- package/src/window.js +1 -0
- package/types/index.d.ts +59 -4
- package/types/search.d.ts +20 -0
- package/types/typed.d.ts +1 -0
package/src/query.js
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
* the whole document runs over them.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import { physicalSelection, columnCodec } from './physical.js';
|
|
35
|
+
|
|
34
36
|
import { createSemanticCache } from '@jarenjs/core/cache';
|
|
35
37
|
import { analyzeQuery } from '@jarenjs/json/query';
|
|
36
38
|
|
|
@@ -47,7 +49,7 @@ import {
|
|
|
47
49
|
} from './residual.js';
|
|
48
50
|
import { createCursor, createSyncCursor, drainPage, utf8Length, PAGE_LIMIT_DEFAULT, rowClassOf } from './cursor.js';
|
|
49
51
|
import { deepFreeze } from '@jarenjs/core/object';
|
|
50
|
-
import { derivedSlotValue,
|
|
52
|
+
import { derivedSlotValue, probeVector, columnScore } from './derive.js';
|
|
51
53
|
import { cutCandidates, identityBatches } from './knn.js';
|
|
52
54
|
import {
|
|
53
55
|
deterministicFragment, registerFragment, registerAggregateOperator,
|
|
@@ -149,7 +151,8 @@ function slotValue(slot, externals, anchors = null) {
|
|
|
149
151
|
return value === undefined ? undefined : JSON.stringify(value);
|
|
150
152
|
}
|
|
151
153
|
if ('derived' in slot)
|
|
152
|
-
return derivedSlotValue(slot.derived,
|
|
154
|
+
return derivedSlotValue(slot.derived, slot.derived.kind === 'bboxAxis'
|
|
155
|
+
? externals[slot.derived.external] : externals);
|
|
153
156
|
if ('typed' in slot) {
|
|
154
157
|
// a typed slot is only ever emitted beside the seek that fills it,
|
|
155
158
|
// so a bind that never resolved the seeks is a defect in the
|
|
@@ -179,8 +182,14 @@ function externalSlotKinds(slots, rank) {
|
|
|
179
182
|
const kinds = new Map();
|
|
180
183
|
for (const slot of slots) {
|
|
181
184
|
if ('external' in slot) kinds.set(slot.external, 'plain');
|
|
182
|
-
else if ('derived' in slot
|
|
183
|
-
|
|
185
|
+
else if ('derived' in slot) {
|
|
186
|
+
const inputs = slot.derived.kind === 'bboxAxis'
|
|
187
|
+
? [slot.derived] : [slot.derived.centre, slot.derived.radius];
|
|
188
|
+
for (const input of inputs) {
|
|
189
|
+
if ('external' in input && !kinds.has(input.external))
|
|
190
|
+
kinds.set(input.external, 'derived');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
184
193
|
}
|
|
185
194
|
if (rank !== null && 'ext' in rank.probe && !kinds.has(rank.probe.ext))
|
|
186
195
|
kinds.set(rank.probe.ext, 'probe');
|
|
@@ -285,7 +294,11 @@ function segmentsOf(ref) {
|
|
|
285
294
|
* @param {string} suffix
|
|
286
295
|
* @returns {any[]} the item, or nothing where the member is absent
|
|
287
296
|
*/
|
|
288
|
-
function leafItems(row, suffix) {
|
|
297
|
+
function leafItems(row, suffix, column = undefined) {
|
|
298
|
+
if (column !== undefined) {
|
|
299
|
+
const value = columnCodec(column).decode(row[`v${suffix}`]);
|
|
300
|
+
return value === undefined ? [] : [value];
|
|
301
|
+
}
|
|
289
302
|
const type = row[`t${suffix}`];
|
|
290
303
|
if (type === null || type === undefined) return [];
|
|
291
304
|
const value = row[`v${suffix}`];
|
|
@@ -314,7 +327,8 @@ function leafItems(row, suffix) {
|
|
|
314
327
|
function projectedTreeItems(project, row, prefix = '') {
|
|
315
328
|
const build = (node) => {
|
|
316
329
|
if (node.p === 'lit') return [node.value];
|
|
317
|
-
if (node.p === 'leaf') return leafItems(row, `${prefix}${node.index}
|
|
330
|
+
if (node.p === 'leaf') return leafItems(row, `${prefix}${node.index}`,
|
|
331
|
+
project.leaves[node.index].ref?.codecColumn);
|
|
318
332
|
if (node.p === 'object') {
|
|
319
333
|
/** @type {any} */
|
|
320
334
|
const out = {};
|
|
@@ -331,6 +345,47 @@ function projectedTreeItems(project, row, prefix = '') {
|
|
|
331
345
|
return build(project.tree);
|
|
332
346
|
}
|
|
333
347
|
|
|
348
|
+
/**
|
|
349
|
+
* The items a GENERAL grouping answers: one per group row, built from
|
|
350
|
+
* the group's keys and aggregates through the plan's own tree. A key
|
|
351
|
+
* comes back with its JSON type beside it, so an absent key leaves
|
|
352
|
+
* its member out exactly as the object constructor does; an aggregate
|
|
353
|
+
* over no values follows the mapping the plan recorded — `0` for a
|
|
354
|
+
* count or a sum, the empty sequence for the other three, which is
|
|
355
|
+
* the ENGINE's answer, not SQL's `NULL`.
|
|
356
|
+
* @param {any} entry
|
|
357
|
+
* @param {any[]} rows
|
|
358
|
+
* @returns {any[]}
|
|
359
|
+
*/
|
|
360
|
+
function groupItems(group, rows) {
|
|
361
|
+
const aggregateItems = (row, index) => {
|
|
362
|
+
const value = row[`a${index}`] ?? null;
|
|
363
|
+
const empty = group.aggregates[index].empty;
|
|
364
|
+
if (value === null) return empty === 'omit' ? [] : [empty === 'zero' ? 0 : null];
|
|
365
|
+
const aggregate = group.aggregates[index];
|
|
366
|
+
return [aggregate.ref?.codecColumn && ['min', 'max'].includes(aggregate.fn)
|
|
367
|
+
? columnCodec(aggregate.ref.codecColumn).decode(value) : value];
|
|
368
|
+
};
|
|
369
|
+
const build = (node, row) => {
|
|
370
|
+
if (node.p === 'lit') return [node.value];
|
|
371
|
+
if (node.p === 'key') return leafItems(row, `k${node.index}`, group.keys[node.index].ref.codecColumn);
|
|
372
|
+
if (node.p === 'agg') return aggregateItems(row, node.index);
|
|
373
|
+
if (node.p === 'object') {
|
|
374
|
+
/** @type {any} */
|
|
375
|
+
const out = {};
|
|
376
|
+
for (const member of node.members) {
|
|
377
|
+
const items = build(member.node, row);
|
|
378
|
+
if (items.length > 0) out[member.name] = items[0];
|
|
379
|
+
}
|
|
380
|
+
return [out];
|
|
381
|
+
}
|
|
382
|
+
const items = [];
|
|
383
|
+
for (const item of node.items) items.push(...build(item, row));
|
|
384
|
+
return [items];
|
|
385
|
+
};
|
|
386
|
+
return rows.flatMap((row) => build(group.tree, row));
|
|
387
|
+
}
|
|
388
|
+
|
|
334
389
|
/**
|
|
335
390
|
* The query engine for one collection.
|
|
336
391
|
* @param {{ connection: any, state: any, collection: any,
|
|
@@ -510,7 +565,6 @@ export function createQueryEngine(context) {
|
|
|
510
565
|
: { ...planned.series, mode: 'engine', index: null, prefix: [] },
|
|
511
566
|
};
|
|
512
567
|
}
|
|
513
|
-
|
|
514
568
|
if (profile !== null) {
|
|
515
569
|
// the member allow-list: what the caller may OBTAIN, checked
|
|
516
570
|
// against every member path the document references, before a
|
|
@@ -792,7 +846,15 @@ export function createQueryEngine(context) {
|
|
|
792
846
|
const divertingExternal = (entry, externals) =>
|
|
793
847
|
entry.externalNames.find((name) => {
|
|
794
848
|
const kind = entry.externalSlotKinds.get(name);
|
|
795
|
-
|
|
849
|
+
const invalidDerived = entry.slots.some((slot) => {
|
|
850
|
+
if (!('derived' in slot)) return false;
|
|
851
|
+
const inputs = slot.derived.kind === 'bboxAxis'
|
|
852
|
+
? [slot.derived] : [slot.derived.centre, slot.derived.radius];
|
|
853
|
+
return inputs.some((input) => 'external' in input && input.external === name)
|
|
854
|
+
&& !bindable(slotValue(slot, externals));
|
|
855
|
+
});
|
|
856
|
+
if (invalidDerived) return true;
|
|
857
|
+
if (kind === 'derived') return false;
|
|
796
858
|
// a probe binds when SOME declared width takes it; a width the
|
|
797
859
|
// model does not declare is the diversion it always was
|
|
798
860
|
if (kind === 'probe') return rankAlternativeFor(entry, externals[name]) === null;
|
|
@@ -834,46 +896,6 @@ export function createQueryEngine(context) {
|
|
|
834
896
|
return items;
|
|
835
897
|
};
|
|
836
898
|
|
|
837
|
-
/**
|
|
838
|
-
* The items a GENERAL grouping answers: one per group row, built from
|
|
839
|
-
* the group's keys and aggregates through the plan's own tree. A key
|
|
840
|
-
* comes back with its JSON type beside it, so an absent key leaves
|
|
841
|
-
* its member out exactly as the object constructor does; an aggregate
|
|
842
|
-
* over no values follows the mapping the plan recorded — `0` for a
|
|
843
|
-
* count or a sum, the empty sequence for the other three, which is
|
|
844
|
-
* the ENGINE's answer, not SQL's `NULL`.
|
|
845
|
-
* @param {any} entry
|
|
846
|
-
* @param {any[]} rows
|
|
847
|
-
* @returns {any[]}
|
|
848
|
-
*/
|
|
849
|
-
const groupItems = (entry, rows) => {
|
|
850
|
-
const group = entry.plan.group;
|
|
851
|
-
const aggregateItems = (row, index) => {
|
|
852
|
-
const value = row[`a${index}`] ?? null;
|
|
853
|
-
const empty = group.aggregates[index].empty;
|
|
854
|
-
if (value === null) return empty === 'omit' ? [] : [empty === 'zero' ? 0 : null];
|
|
855
|
-
return [value];
|
|
856
|
-
};
|
|
857
|
-
const build = (node, row) => {
|
|
858
|
-
if (node.p === 'lit') return [node.value];
|
|
859
|
-
if (node.p === 'key') return leafItems(row, `k${node.index}`);
|
|
860
|
-
if (node.p === 'agg') return aggregateItems(row, node.index);
|
|
861
|
-
if (node.p === 'object') {
|
|
862
|
-
/** @type {any} */
|
|
863
|
-
const out = {};
|
|
864
|
-
for (const member of node.members) {
|
|
865
|
-
const items = build(member.node, row);
|
|
866
|
-
if (items.length > 0) out[member.name] = items[0];
|
|
867
|
-
}
|
|
868
|
-
return [out];
|
|
869
|
-
}
|
|
870
|
-
const items = [];
|
|
871
|
-
for (const item of node.items) items.push(...build(item, row));
|
|
872
|
-
return [items];
|
|
873
|
-
};
|
|
874
|
-
return rows.flatMap((row) => build(group.tree, row));
|
|
875
|
-
};
|
|
876
|
-
|
|
877
899
|
/** How many ITEMS an engine result carries (its own shape rule). */
|
|
878
900
|
const itemCount = (answer) => (answer === undefined ? 0
|
|
879
901
|
: Array.isArray(answer) ? answer.length : 1);
|
|
@@ -1096,7 +1118,7 @@ export function createQueryEngine(context) {
|
|
|
1096
1118
|
}
|
|
1097
1119
|
if (entry.plan.group !== null) {
|
|
1098
1120
|
return chain(runAll(entry, externals, statement), (rows) =>
|
|
1099
|
-
answerOf(entry, groupItems(entry, checkRowBound(entry, rows))));
|
|
1121
|
+
answerOf(entry, groupItems(entry.plan.group, checkRowBound(entry, rows))));
|
|
1100
1122
|
}
|
|
1101
1123
|
if (entry.plan.bucket !== null) {
|
|
1102
1124
|
return chain(runAll(entry, externals, statement), (rows) => {
|
|
@@ -1193,12 +1215,12 @@ export function createQueryEngine(context) {
|
|
|
1193
1215
|
return items;
|
|
1194
1216
|
})) });
|
|
1195
1217
|
}
|
|
1196
|
-
if (entry.plan.group !== null) {
|
|
1218
|
+
if (entry.plan.group !== null && entry.plan.aggregate === null) {
|
|
1197
1219
|
// a native grouping is a barrier: the groups are the answer
|
|
1198
1220
|
return createCursor({ ...classified, signal, deadline, now: state.now, wrap: driverWrap,
|
|
1199
1221
|
materialize: () => chain(guardScan(entry), () => chain(statementOf(entry), (statement) =>
|
|
1200
1222
|
chain(runAll(entry, externals, statement), (rows) =>
|
|
1201
|
-
groupItems(entry, checkRowBound(entry, rows))))) });
|
|
1223
|
+
groupItems(entry.plan.group, checkRowBound(entry, rows))))) });
|
|
1202
1224
|
}
|
|
1203
1225
|
if (entry.plan.bucket !== null) {
|
|
1204
1226
|
// a native bucket is a barrier: the groups are the answer
|
|
@@ -1295,6 +1317,10 @@ export function createQueryEngine(context) {
|
|
|
1295
1317
|
else if (pred.p === 'bboxRtree') touchedVirtual.add(pred.table);
|
|
1296
1318
|
else if (pred.p === 'cellIn' || pred.p === 'cellPrefix')
|
|
1297
1319
|
touchedColumns.add(pred.column);
|
|
1320
|
+
else if (pred.p === 'refCmp') {
|
|
1321
|
+
if (pred.left.column) touchedColumns.add(pred.left.column);
|
|
1322
|
+
if (pred.right.column) touchedColumns.add(pred.right.column);
|
|
1323
|
+
}
|
|
1298
1324
|
else if ('ref' in pred && pred.ref?.column) touchedColumns.add(pred.ref.column);
|
|
1299
1325
|
};
|
|
1300
1326
|
collectColumns(entry.plan.filter);
|
|
@@ -1383,7 +1409,10 @@ export function createQueryEngine(context) {
|
|
|
1383
1409
|
fn: entry2.fn, path: entry2.ref === null ? null : segmentsOf(entry2.ref) })),
|
|
1384
1410
|
order: entry.plan.group.order === 'first-seen' ? 'first-seen'
|
|
1385
1411
|
: entry.plan.group.order.map((term) => ({
|
|
1386
|
-
key: entry.plan.group.keys[term.index].as
|
|
1412
|
+
...(term.aggregate === undefined ? { key: entry.plan.group.keys[term.index].as }
|
|
1413
|
+
: { aggregate: entry.plan.group.aggregates[term.aggregate].fn,
|
|
1414
|
+
path: entry.plan.group.aggregates[term.aggregate].ref?.segments ?? null }),
|
|
1415
|
+
desc: term.desc })),
|
|
1387
1416
|
},
|
|
1388
1417
|
// a chain's element window (`[<phrase>]`): the phrase planned as
|
|
1389
1418
|
// if bare, its rows answered as the one array item
|
|
@@ -1577,7 +1606,11 @@ export function createEntityQueryEngine(context) {
|
|
|
1577
1606
|
const q = dialect.quoteIdentifier;
|
|
1578
1607
|
const physicalOf = (name) => ({ table: mapping.entities[name].table,
|
|
1579
1608
|
// a join-table root has no document column of its own (§10.7)
|
|
1580
|
-
document: mapping.entities[name].document !== false
|
|
1609
|
+
document: mapping.entities[name].document !== false,
|
|
1610
|
+
...(entities.get(name)?.physical == null ? {} : {
|
|
1611
|
+
mapping: mapping.entities[name],
|
|
1612
|
+
keys: mapping.entities[name].keys.map((key) => mapping.entities[name].columns.find((c) => c.name === key).physical),
|
|
1613
|
+
}) });
|
|
1581
1614
|
// the relation tables of every root this engine serves (§10.1): the
|
|
1582
1615
|
// engine is the scope every entity set of the store shares, so a
|
|
1583
1616
|
// producer holding one set can follow a hop into another root
|
|
@@ -1620,6 +1653,10 @@ export function createEntityQueryEngine(context) {
|
|
|
1620
1653
|
planned = { ...planned, mode: 'set', plan: null,
|
|
1621
1654
|
reasons: [{ construct: 'pushdown', reason: BIND_REASONS.pushdown }] };
|
|
1622
1655
|
}
|
|
1656
|
+
if (planned.plan?.group && dialect.name !== 'sqlite') {
|
|
1657
|
+
planned = { ...planned, mode: 'set', plan: null,
|
|
1658
|
+
reasons: [{ construct: '$groupby', reason: 'entity grouping runtime guards are qualified for SQLite' }] };
|
|
1659
|
+
}
|
|
1623
1660
|
const docPath = entities.get(planned.referenced[0])?.docPath;
|
|
1624
1661
|
// the profile, applied exactly as on a collection (MODEL-FORMAT §8):
|
|
1625
1662
|
// the roots a document may read, the references it may make, the
|
|
@@ -1678,6 +1715,8 @@ export function createEntityQueryEngine(context) {
|
|
|
1678
1715
|
residualLimits: profile === null ? undefined : profile.limits,
|
|
1679
1716
|
needsScanCheck: profile !== null && profile.refuseFullScan === true,
|
|
1680
1717
|
scanChecked: false,
|
|
1718
|
+
admitted: null,
|
|
1719
|
+
runtimeReason: null,
|
|
1681
1720
|
};
|
|
1682
1721
|
if (planned.mode === 'native') {
|
|
1683
1722
|
let plan = planned.plan;
|
|
@@ -1687,6 +1726,9 @@ export function createEntityQueryEngine(context) {
|
|
|
1687
1726
|
const predicate = mandatory.get(binding.entity);
|
|
1688
1727
|
return predicate === undefined ? entry : { ...entry, filter: conjoin(entry.filter, predicate) };
|
|
1689
1728
|
}) };
|
|
1729
|
+
if (plan.project) plan = { ...plan, project: { ...plan.project, leaves: plan.project.leaves.map((leaf) =>
|
|
1730
|
+
leaf.count && mandatory.has(leaf.count.entity) ? { ...leaf, count: { ...leaf.count,
|
|
1731
|
+
filter: conjoin(leaf.count.filter, mandatory.get(leaf.count.entity)) } } : leaf) } };
|
|
1690
1732
|
}
|
|
1691
1733
|
if (entry.rowBound !== null && plan.aggregate === null) {
|
|
1692
1734
|
const cap = entry.rowBound + 1;
|
|
@@ -1712,6 +1754,13 @@ export function createEntityQueryEngine(context) {
|
|
|
1712
1754
|
}
|
|
1713
1755
|
return rows;
|
|
1714
1756
|
};
|
|
1757
|
+
const admittedRows = (entry, rows) => {
|
|
1758
|
+
entry.admitted ??= { statements: 0, rows: 0, bytes: 0 };
|
|
1759
|
+
entry.admitted.statements++;
|
|
1760
|
+
entry.admitted.rows += rows.length;
|
|
1761
|
+
entry.admitted.bytes += utf8Length(JSON.stringify(rows));
|
|
1762
|
+
return rows;
|
|
1763
|
+
};
|
|
1715
1764
|
/** One merged entity document against the profile's byte bound
|
|
1716
1765
|
* (JD2076): measured after the merge, because the mapped scalars live
|
|
1717
1766
|
* in columns and the row's own JSON text holds only the rest. */
|
|
@@ -1767,11 +1816,15 @@ export function createEntityQueryEngine(context) {
|
|
|
1767
1816
|
const limit = entry.rowBound === null ? '' : ` ${dialect.limitClause(entry.rowBound + 1, undefined)}`;
|
|
1768
1817
|
return {
|
|
1769
1818
|
name,
|
|
1770
|
-
sql: `SELECT ${
|
|
1771
|
-
? dialect.stringLiteral('{}')
|
|
1772
|
-
|
|
1819
|
+
sql: `SELECT ${entities.get(name)?.physical != null ? physicalSelection(mapping.entities[name], dialect, `${q('t')}.`)
|
|
1820
|
+
: `${q('t')}.*, ${mapping.entities[name].document === false ? dialect.stringLiteral('{}')
|
|
1821
|
+
: dialect.jsonText(`${q('t')}.${q('doc')}`)} AS ${q('__doc')}`} `
|
|
1773
1822
|
+ `FROM ${q(mapping.entities[name].table)} AS ${q('t')}${where} `
|
|
1774
|
-
+ `ORDER BY ${
|
|
1823
|
+
+ `ORDER BY ${entities.get(name)?.physical != null ? mapping.entities[name].keys.map((k) => {
|
|
1824
|
+
const column = mapping.entities[name].columns.find((c) => c.name === k);
|
|
1825
|
+
const value = `${q('t')}.${q(column.physical)}`;
|
|
1826
|
+
return column.codec === 'text' ? dialect.codepoint(value) : value;
|
|
1827
|
+
}).join(', ') : `${q('t')}.${dialect.rowIdentity()}`}${limit}`,
|
|
1775
1828
|
params: slots.map((slot) => slot.literal),
|
|
1776
1829
|
statement: null,
|
|
1777
1830
|
};
|
|
@@ -1785,6 +1838,7 @@ export function createEntityQueryEngine(context) {
|
|
|
1785
1838
|
if (fetcher.statement === null) fetcher.statement = connection.prepare(fetcher.sql, { readOnly: true });
|
|
1786
1839
|
return chain(fetcher.statement, (statement) =>
|
|
1787
1840
|
chain(statement.all(fetcher.params), (rows) => {
|
|
1841
|
+
admittedRows(entry, rows);
|
|
1788
1842
|
root[fetcher.name] = checkRows(entry, rows, fetcher.name).map((row) =>
|
|
1789
1843
|
checkBytes(entry, mergeEntityRow(mapping.entities[fetcher.name], row, '__doc'), fetcher.name));
|
|
1790
1844
|
return next(i + 1);
|
|
@@ -1803,6 +1857,8 @@ export function createEntityQueryEngine(context) {
|
|
|
1803
1857
|
requireCallable(options, state.now);
|
|
1804
1858
|
const { externals, strict, pushdown, profile } = callState(options);
|
|
1805
1859
|
const entry = entryFor(document, pushdown, profile);
|
|
1860
|
+
entry.admitted = { statements: 0, rows: 0, bytes: 0 };
|
|
1861
|
+
entry.runtimeReason = null;
|
|
1806
1862
|
if (entry.planned.mode !== 'native') {
|
|
1807
1863
|
if (strict) {
|
|
1808
1864
|
const forcing = entry.planned.reasons[0];
|
|
@@ -1822,12 +1878,30 @@ export function createEntityQueryEngine(context) {
|
|
|
1822
1878
|
if (entry.statement === null) entry.statement = connection.prepare(entry.sql, { readOnly: true });
|
|
1823
1879
|
return chain(guardEntityScan(entry), () => chain(entry.statement, (statement) => {
|
|
1824
1880
|
if (entry.planned.plan.aggregate === 'count')
|
|
1825
|
-
return chain(statement.get(params), (row) => wrapValue(entry, row?.value ?? 0));
|
|
1881
|
+
return chain(statement.get(params), (row) => { admittedRows(entry, row ? [row] : []); return wrapValue(entry, row?.value ?? 0); });
|
|
1826
1882
|
return chain(statement.all(params), (rows) => {
|
|
1883
|
+
admittedRows(entry, rows);
|
|
1884
|
+
checkRows(entry, rows, entry.planned.referenced[0]);
|
|
1885
|
+
if (entry.planned.plan.group) {
|
|
1886
|
+
const group = entry.planned.plan.group;
|
|
1887
|
+
if (rows.some((row) => row._valid === 0))
|
|
1888
|
+
throw new DbRuntimeError('JD2003', 'a grouped physical column refuses a lossy or invalid value');
|
|
1889
|
+
if (rows.some((row) => group.aggregates.some((aggregate, i) =>
|
|
1890
|
+
['sum', 'avg'].includes(aggregate.fn) && row[`_safe${i}`] !== 1))) {
|
|
1891
|
+
entry.runtimeReason = { construct: '$groupby', reason: 'integer accumulation exceeded its runtime exactness bound' };
|
|
1892
|
+
if (strict) throw new DbCompileError('JD0010', entry.runtimeReason.reason);
|
|
1893
|
+
if (profile?.refuseFullScan) throw profileEntityRefusal(
|
|
1894
|
+
'the profile refuses the decoded scan required by the integer accumulation bound', '/entities');
|
|
1895
|
+
return runResidual(entry, document, externals);
|
|
1896
|
+
}
|
|
1897
|
+
return answerOf(entry, groupItems(group, rows)
|
|
1898
|
+
.map((row) => checkBytes(entry, row, entry.planned.referenced[0])));
|
|
1899
|
+
}
|
|
1827
1900
|
const project = entry.planned.plan.project;
|
|
1828
1901
|
if (project != null) {
|
|
1829
1902
|
return answerOf(entry,
|
|
1830
|
-
rows.flatMap((row) => projectedTreeItems(project, row, 'p'))
|
|
1903
|
+
rows.flatMap((row) => projectedTreeItems(project, row, 'p'))
|
|
1904
|
+
.map((row) => checkBytes(entry, row, entry.planned.referenced[0])));
|
|
1831
1905
|
}
|
|
1832
1906
|
const retEntity = entry.planned.plan.bindings
|
|
1833
1907
|
.find((binding) => binding.name === entry.planned.plan.ret).entity;
|
|
@@ -1856,7 +1930,8 @@ export function createEntityQueryEngine(context) {
|
|
|
1856
1930
|
for (const slot of entry.slots) {
|
|
1857
1931
|
if (bindable(slotValue(slot, externals))) continue;
|
|
1858
1932
|
const name = 'external' in slot ? slot.external
|
|
1859
|
-
: 'derived' in slot ? slot.derived.
|
|
1933
|
+
: 'derived' in slot ? (slot.derived.kind === 'bboxAxis' ? slot.derived.external
|
|
1934
|
+
: [slot.derived.centre, slot.derived.radius].find((input) => 'external' in input)?.external) : null;
|
|
1860
1935
|
return { construct: 'external', reason: BIND_REASONS.external(name, 'root') };
|
|
1861
1936
|
}
|
|
1862
1937
|
return null;
|
|
@@ -1877,6 +1952,8 @@ export function createEntityQueryEngine(context) {
|
|
|
1877
1952
|
if (entry.planned.wrapped === true) {
|
|
1878
1953
|
return buffered({ construct: 'window', reason: BIND_REASONS.wrappedWindow });
|
|
1879
1954
|
}
|
|
1955
|
+
if (entry.planned.plan?.group)
|
|
1956
|
+
return buffered({ construct: '$groupby', reason: 'group results are validated and reconstructed together' });
|
|
1880
1957
|
if (entry.planned.mode !== 'native') {
|
|
1881
1958
|
const forcing = entry.planned.reasons[0]
|
|
1882
1959
|
?? { construct: 'residual', reason: BIND_REASONS.untranslated };
|
|
@@ -1924,14 +2001,17 @@ export function createEntityQueryEngine(context) {
|
|
|
1924
2001
|
refuseBuffered(options, classified, entities.get(entry.planned.retEntity ?? '')?.docPath);
|
|
1925
2002
|
const signal = options?.signal;
|
|
1926
2003
|
const deadline = options?.deadline;
|
|
1927
|
-
if (entry.planned.wrapped === true) {
|
|
2004
|
+
if (entry.planned.wrapped === true || entry.planned.plan?.group) {
|
|
1928
2005
|
return cursorFactory({ ...classified, signal, deadline, now: state.now, wrap: driverWrap,
|
|
1929
|
-
materialize: () => chain(execute(document, options), (value) =>
|
|
2006
|
+
materialize: () => chain(execute(document, options), (value) => entry.planned.wrapped === true
|
|
2007
|
+
? [value] : value === undefined ? [] : Array.isArray(value) ? value : [value]) });
|
|
1930
2008
|
}
|
|
1931
2009
|
if (classified.barrier !== null) {
|
|
1932
2010
|
return cursorFactory({ ...classified, signal, deadline, now: state.now, wrap: driverWrap,
|
|
1933
|
-
materialize: () =>
|
|
1934
|
-
|
|
2011
|
+
materialize: () => {
|
|
2012
|
+
entry.admitted = { statements: 0, rows: 0, bytes: 0 };
|
|
2013
|
+
return chain(fetchRoot(entry), (root) => packedResidualOf(entry, document)(root, externals).map(each));
|
|
2014
|
+
} });
|
|
1935
2015
|
}
|
|
1936
2016
|
const params = entry.slots.map((slot) => slotValue(slot, externals));
|
|
1937
2017
|
// the statement is prepared by the first PULL, not here: a root
|
|
@@ -1944,7 +2024,11 @@ export function createEntityQueryEngine(context) {
|
|
|
1944
2024
|
if (entry.planned.plan.aggregate === 'count') {
|
|
1945
2025
|
return cursorFactory({ ...classified, signal, deadline, now: state.now, wrap: driverWrap,
|
|
1946
2026
|
materialize: () => chain(guardEntityScan(entry), () => chain(prepared(), (statement) =>
|
|
1947
|
-
chain(statement.get(params), (row) =>
|
|
2027
|
+
chain(statement.get(params), (row) => {
|
|
2028
|
+
entry.admitted = { statements: 0, rows: 0, bytes: 0 };
|
|
2029
|
+
admittedRows(entry, row ? [row] : []);
|
|
2030
|
+
return [row?.value ?? 0];
|
|
2031
|
+
}))) });
|
|
1948
2032
|
}
|
|
1949
2033
|
const rowEntity = entry.planned.plan.ret === null ? null
|
|
1950
2034
|
: entry.planned.plan.bindings
|
|
@@ -1953,17 +2037,23 @@ export function createEntityQueryEngine(context) {
|
|
|
1953
2037
|
// a statement of its own per cursor: two live iterators over one
|
|
1954
2038
|
// cached statement invalidate each other at the driver
|
|
1955
2039
|
return cursorFactory({ ...classified, signal, deadline, now: state.now, wrap: driverWrap,
|
|
1956
|
-
open: () =>
|
|
1957
|
-
|
|
2040
|
+
open: () => {
|
|
2041
|
+
entry.admitted = { statements: 1, rows: 0, bytes: 2 };
|
|
2042
|
+
return chain(guardEntityScan(entry), () => chain(connection.prepare(entry.sql, { readOnly: true, ephemeral: true }),
|
|
2043
|
+
(statement) => statement.iterate(params)));
|
|
2044
|
+
},
|
|
1958
2045
|
items: (row) => {
|
|
1959
2046
|
pulledRows++;
|
|
2047
|
+
entry.admitted.bytes += utf8Length(JSON.stringify(row)) + (entry.admitted.rows ? 1 : 0);
|
|
2048
|
+
entry.admitted.rows++;
|
|
1960
2049
|
if (entry.rowBound !== null && pulledRows > entry.rowBound) {
|
|
1961
2050
|
throw new DbRuntimeError('JD2007',
|
|
1962
2051
|
`the fetch crossed the profile's maxRows bound of ${entry.rowBound}`,
|
|
1963
2052
|
{ docPath: entities.get(rowEntity)?.docPath, collection: rowEntity });
|
|
1964
2053
|
}
|
|
1965
2054
|
const project = entry.planned.plan.project;
|
|
1966
|
-
if (project != null) return projectedTreeItems(project, row, 'p')
|
|
2055
|
+
if (project != null) return projectedTreeItems(project, row, 'p')
|
|
2056
|
+
.map((item) => checkBytes(entry, item, entry.planned.referenced[0]));
|
|
1967
2057
|
return [each(checkBytes(entry, mergeEntityRow(mapping.entities[rowEntity], row, '__doc'), rowEntity))];
|
|
1968
2058
|
} });
|
|
1969
2059
|
};
|
|
@@ -1978,21 +2068,23 @@ export function createEntityQueryEngine(context) {
|
|
|
1978
2068
|
const classified = cursorClass(entry, options?.externals ?? null);
|
|
1979
2069
|
const diverted = entry.planned.mode === 'native' && classified.barrier !== null
|
|
1980
2070
|
&& classified.barrier.construct === 'external';
|
|
1981
|
-
const
|
|
1982
|
-
const
|
|
2071
|
+
const runtime = entry.runtimeReason;
|
|
2072
|
+
const mode = diverted || runtime ? 'set' : entry.planned.mode;
|
|
2073
|
+
const reasons = runtime ? [runtime, ...entry.planned.reasons] : diverted && classified.barrier !== null
|
|
1983
2074
|
? [classified.barrier, ...entry.planned.reasons] : entry.planned.reasons;
|
|
1984
2075
|
const base = {
|
|
1985
2076
|
mode,
|
|
1986
2077
|
streaming: classified.streaming,
|
|
1987
|
-
barrier: classified.barrier,
|
|
2078
|
+
barrier: runtime ?? classified.barrier,
|
|
1988
2079
|
budget: budgetOf(profile, profileSource, connection.capabilities),
|
|
1989
2080
|
wrapped: entry.planned.wrapped === true,
|
|
1990
2081
|
referenced: [...entry.planned.referenced],
|
|
2082
|
+
admitted: entry.admitted === null ? null : { ...entry.admitted },
|
|
1991
2083
|
reasons,
|
|
1992
2084
|
// the same effective-order vocabulary the collection engine and
|
|
1993
2085
|
// the graph loader report; `null` when no statement answers
|
|
1994
|
-
order:
|
|
1995
|
-
sql:
|
|
2086
|
+
order: mode === 'native' ? planOrder(entry.planned.plan) : null,
|
|
2087
|
+
sql: mode === 'native' ? entry.sql : null,
|
|
1996
2088
|
residual: mode === 'native'
|
|
1997
2089
|
? null
|
|
1998
2090
|
: { mode: 'set', reasons },
|
|
@@ -2426,6 +2518,8 @@ export function createLoadEngine(context, entityName) {
|
|
|
2426
2518
|
if (spec?.[member] !== undefined && !isWindowBound(spec[member]))
|
|
2427
2519
|
throw refuse(`${member} must be a non-negative integer`, []);
|
|
2428
2520
|
}
|
|
2521
|
+
if (entities.get(entityName)?.physical != null && (keyset || spec?.after !== undefined))
|
|
2522
|
+
throw refuse('column codecs require decoded identities; physical keyset continuation is not qualified', []);
|
|
2429
2523
|
const tree = buildTree(entityName, spec ?? {}, 0, maxDepth, [], new Set(), profile);
|
|
2430
2524
|
const rendered = render(tree, 'r', param, emitters);
|
|
2431
2525
|
|
|
@@ -2515,7 +2609,8 @@ export function createLoadEngine(context, entityName) {
|
|
|
2515
2609
|
pagination = 'offset';
|
|
2516
2610
|
}
|
|
2517
2611
|
|
|
2518
|
-
let sql = `SELECT ${
|
|
2612
|
+
let sql = `SELECT ${tree.entity.physical != null ? physicalSelection(tree.entityMapping, dialect, `${rendered.aliasSql}.`)
|
|
2613
|
+
: `${rendered.aliasSql}.*, ${tree.entityMapping.document === false ? dialect.stringLiteral('{}') : dialect.jsonText(rendered.docSql)} AS ${q('__doc')}`}`
|
|
2519
2614
|
+ includeSql
|
|
2520
2615
|
+ ` FROM ${q(tree.entityMapping.table)} AS ${rendered.aliasSql}`;
|
|
2521
2616
|
if (conditions.length > 0) sql += ` WHERE ${conditions.join(' AND ')}`;
|
|
@@ -2529,7 +2624,7 @@ export function createLoadEngine(context, entityName) {
|
|
|
2529
2624
|
const nullsFirst = term.emptyGreatest === term.desc;
|
|
2530
2625
|
return `${value} ${term.desc ? 'DESC' : 'ASC'}${dialect.orderNulls(nullsFirst)}`;
|
|
2531
2626
|
});
|
|
2532
|
-
if (identity === null) orderSql.push(`${rendered.aliasSql}.${dialect.rowIdentity()}`);
|
|
2627
|
+
if (identity === null) orderSql.push(...(tree.entity.physical != null ? tree.entityMapping.keys.map((k) => `${rendered.aliasSql}.${q(tree.entityMapping.columns.find((c) => c.name === k).physical)}`) : [`${rendered.aliasSql}.${dialect.rowIdentity()}`]));
|
|
2533
2628
|
sql += ` ORDER BY ${orderSql.join(', ')}`;
|
|
2534
2629
|
// the profile's row bound rides the root as LIMIT maxRows + 1, so
|
|
2535
2630
|
// a load past it is detected at the bound and refused (JD2007)
|
|
@@ -2681,7 +2776,7 @@ export function createLoadEngine(context, entityName) {
|
|
|
2681
2776
|
* the truth either way.
|
|
2682
2777
|
* @param {any} spec
|
|
2683
2778
|
* @param {{ limit?: number, after?: any, maxBytes?: number | null,
|
|
2684
|
-
* consistency?: 'live' | 'snapshot', signal?: AbortSignal }} [options]
|
|
2779
|
+
* consistency?: 'live' | 'snapshot', signal?: AbortSignal, lookahead?: boolean }} [options]
|
|
2685
2780
|
* @param {((tree: any, doc: any) => any) | undefined} [register]
|
|
2686
2781
|
*/
|
|
2687
2782
|
page(spec, options = undefined, register = undefined, cursorFactory = createCursor) {
|
|
@@ -2700,7 +2795,9 @@ export function createLoadEngine(context, entityName) {
|
|
|
2700
2795
|
if (spec?.take !== undefined || spec?.skip !== undefined)
|
|
2701
2796
|
throw refuse('page() windows by its limit and continuation — a take or skip in the spec is refused', []);
|
|
2702
2797
|
const after = options?.after ?? spec?.after ?? undefined;
|
|
2703
|
-
|
|
2798
|
+
if (options?.lookahead !== undefined && typeof options.lookahead !== 'boolean')
|
|
2799
|
+
throw refuse('page() lookahead must be boolean', []);
|
|
2800
|
+
const paged = { ...(spec ?? {}), take: limit + (options?.lookahead === false ? 0 : 1) };
|
|
2704
2801
|
if (after === undefined) delete paged.after;
|
|
2705
2802
|
else paged.after = after;
|
|
2706
2803
|
const entry = buildLoad(paged, true, profileOf(options));
|
|
@@ -2718,7 +2815,7 @@ export function createLoadEngine(context, entityName) {
|
|
|
2718
2815
|
// of work
|
|
2719
2816
|
const cursor = openCursor(entry, options?.signal, undefined, options?.deadline, cursorFactory);
|
|
2720
2817
|
return chain(drainPage(cursor, {
|
|
2721
|
-
limit, maxBytes, after: after ?? null,
|
|
2818
|
+
limit, maxBytes, after: after ?? null, lookahead: options?.lookahead,
|
|
2722
2819
|
sizeOf: (doc) => utf8Length(JSON.stringify(doc)),
|
|
2723
2820
|
continuationOf: (doc) => continuationOf(entry, doc),
|
|
2724
2821
|
}), (page) => ({
|