@minnowdb/core 0.6.8 → 0.7.0
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/README.md +3 -2
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +41 -0
- package/dist/engine/database.js +1085 -81
- package/dist/engine/optimizer.js +446 -18
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +16 -6
- package/dist/engine/query.d.ts +53 -1
- package/dist/engine/query.js +1160 -239
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +38 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1186 -0
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +134 -1
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +718 -124
- package/dist/plan/model.d.ts +21 -1
- package/dist/storage/types.js +44 -18
- package/package.json +1 -1
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +277 -6
package/dist/engine/vector.js
CHANGED
|
@@ -2,14 +2,14 @@ import { dateMilliseconds } from "../date-value.js";
|
|
|
2
2
|
import { crossJoinPlan, isCrossJoinPlan } from "../plan/model.js";
|
|
3
3
|
import { MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH, } from "../storage/types.js";
|
|
4
4
|
import { throwIfAborted } from "./cancellation.js";
|
|
5
|
-
import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, unknownColumnDomains, } from "./query.js";
|
|
5
|
+
import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionEvaluator, scalarFunctionValue, unknownColumnDomains, } from "./query.js";
|
|
6
6
|
import { jsonConstructor } from "./sql-json.js";
|
|
7
7
|
import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize, } from "./fts.js";
|
|
8
8
|
import { ByteGroupIndex } from "./group-index.js";
|
|
9
9
|
import { ByteJoinIndex } from "./join-index.js";
|
|
10
10
|
import { UnknownTableError } from "./errors.js";
|
|
11
11
|
import { QueryMemoryBudgetError, QueryMemoryContext, } from "./memory.js";
|
|
12
|
-
import { compareSqlValues, compileSimilarPattern, defineSqlResultProperty, } from "./sql-semantics.js";
|
|
12
|
+
import { coerceComparisonOperands, compareSqlValues, compileSimilarPattern, defineSqlResultProperty, } from "./sql-semantics.js";
|
|
13
13
|
import { concatenatedSqlValue, exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, preservedJsonDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
|
|
14
14
|
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
15
15
|
const DEFAULT_BATCH_ROWS = 2_048;
|
|
@@ -97,6 +97,8 @@ async function writeSpillRowPages(store, ownerId, runId, startPageIndex, columns
|
|
|
97
97
|
await writeSpillPages(store, pending, signal);
|
|
98
98
|
return pageCount;
|
|
99
99
|
}
|
|
100
|
+
/** Distinct raw values a packed number key column may carry before the generic index takes over. */
|
|
101
|
+
const NUMBER_KEY_CODE_CAP = 1 << 20;
|
|
100
102
|
export function createColumnarTable(name, columns, uniqueKey) {
|
|
101
103
|
const first = columns.values().next().value;
|
|
102
104
|
const rowCount = first?.values.length ?? 0;
|
|
@@ -585,7 +587,10 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
585
587
|
if (primitive !== undefined)
|
|
586
588
|
return { ...bound, primitive };
|
|
587
589
|
const primitiveIn = detectPrimitiveInList(bound);
|
|
588
|
-
|
|
590
|
+
if (primitiveIn !== undefined)
|
|
591
|
+
return { ...bound, primitiveIn };
|
|
592
|
+
const dictionaryPredicate = detectDictionaryPredicate(bound);
|
|
593
|
+
return dictionaryPredicate === undefined ? bound : { ...bound, dictionaryPredicate };
|
|
589
594
|
};
|
|
590
595
|
const predicates = plan.predicates.map((predicate) => {
|
|
591
596
|
const bound = bindPredicate(predicate);
|
|
@@ -593,7 +598,8 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
593
598
|
return bound;
|
|
594
599
|
if (bound.dictionaryEquality !== undefined ||
|
|
595
600
|
bound.dictionaryLike !== undefined ||
|
|
596
|
-
bound.dictionaryNumeric !== undefined
|
|
601
|
+
bound.dictionaryNumeric !== undefined ||
|
|
602
|
+
bound.dictionaryPredicate !== undefined) {
|
|
597
603
|
return bound;
|
|
598
604
|
}
|
|
599
605
|
const branches = disjunctiveNormalForm(predicate);
|
|
@@ -663,7 +669,10 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
663
669
|
// vectors swap dictionaries per window; the accumulator remaps its slot table by value on
|
|
664
670
|
// each swap, so windowed vectors qualify too.
|
|
665
671
|
const groupColumn = groupBy.length === 1 ? groupBy[0] : undefined;
|
|
666
|
-
const codeGrouping = grouped
|
|
672
|
+
const codeGrouping = !grouped || groupColumn === undefined ? undefined : codeGroupingFor(groupColumn);
|
|
673
|
+
const numberGrouping = grouped &&
|
|
674
|
+
groupColumn?.kind === "column" &&
|
|
675
|
+
(groupColumn.vector.kind === "number" || groupColumn.vector.kind === "datetime")
|
|
667
676
|
? { source: groupColumn.source, vector: groupColumn.vector }
|
|
668
677
|
: undefined;
|
|
669
678
|
return {
|
|
@@ -682,11 +691,82 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
682
691
|
grouped,
|
|
683
692
|
sourceOrdered,
|
|
684
693
|
...(codeGrouping === undefined ? {} : { codeGrouping }),
|
|
694
|
+
...(numberGrouping === undefined ? {} : { numberGrouping }),
|
|
685
695
|
wildcard: plan.select[0]?.expression.kind === "wildcard",
|
|
686
696
|
...(plan.limit === undefined ? {} : { limit: plan.limit }),
|
|
687
697
|
...(plan.offset === undefined ? {} : { offset: plan.offset }),
|
|
688
698
|
};
|
|
689
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* A single GROUP BY key groups by dictionary code when it is a bare string column, or any
|
|
702
|
+
* expression whose only column is one string column: the value is then a function of the code.
|
|
703
|
+
*/
|
|
704
|
+
function codeGroupingFor(key) {
|
|
705
|
+
if (key.kind === "column") {
|
|
706
|
+
return key.vector.kind === "string" ? { source: key.source, vector: key.vector } : undefined;
|
|
707
|
+
}
|
|
708
|
+
const column = singleStringColumn(key);
|
|
709
|
+
return column === undefined ? undefined : { ...column, expression: key };
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* A CASE the batch aggregate kernel can decide per dictionary code: every WHEN reads exactly one
|
|
713
|
+
* string column (the same one), and every THEN/ELSE is a bare number column or a numeric literal.
|
|
714
|
+
*/
|
|
715
|
+
function conditionalAggregate(argument) {
|
|
716
|
+
if (argument.kind !== "case" || argument.branches.length === 0)
|
|
717
|
+
return undefined;
|
|
718
|
+
const whens = argument.branches.map((branch) => branch.when);
|
|
719
|
+
const first = whens[0];
|
|
720
|
+
if (first === undefined)
|
|
721
|
+
return undefined;
|
|
722
|
+
const conjunction = whens
|
|
723
|
+
.slice(1)
|
|
724
|
+
.reduce((left, right) => ({ kind: "logical", operator: "and", left, right, signature: "" }), first);
|
|
725
|
+
const column = singleStringColumn(conjunction);
|
|
726
|
+
if (column === undefined)
|
|
727
|
+
return undefined;
|
|
728
|
+
const branchValue = (expression) => {
|
|
729
|
+
if (expression === undefined)
|
|
730
|
+
return { kind: "literal", value: null };
|
|
731
|
+
if (expression.kind === "column" && expression.vector.kind === "number") {
|
|
732
|
+
return { kind: "column", source: expression.source, vector: expression.vector };
|
|
733
|
+
}
|
|
734
|
+
if (expression.kind === "literal" &&
|
|
735
|
+
(expression.value === null || typeof expression.value === "number")) {
|
|
736
|
+
return { kind: "literal", value: expression.value };
|
|
737
|
+
}
|
|
738
|
+
return undefined;
|
|
739
|
+
};
|
|
740
|
+
const branches = [];
|
|
741
|
+
for (const branch of argument.branches) {
|
|
742
|
+
const value = branchValue(branch.then);
|
|
743
|
+
if (value === undefined)
|
|
744
|
+
return undefined;
|
|
745
|
+
branches.push({ when: branch.when, value });
|
|
746
|
+
}
|
|
747
|
+
const otherwise = branchValue(argument.otherwise);
|
|
748
|
+
if (otherwise === undefined)
|
|
749
|
+
return undefined;
|
|
750
|
+
return {
|
|
751
|
+
...column,
|
|
752
|
+
branches,
|
|
753
|
+
otherwise,
|
|
754
|
+
cache: { dictionary: undefined, branch: new Int32Array(0) },
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/** The one string column an expression reads, when it reads exactly one column and no FTS. */
|
|
758
|
+
function singleStringColumn(expression) {
|
|
759
|
+
if (boundContainsFts(expression))
|
|
760
|
+
return undefined;
|
|
761
|
+
const columns = boundColumnNodes(expression);
|
|
762
|
+
const first = columns[0];
|
|
763
|
+
if (first?.vector.kind !== "string")
|
|
764
|
+
return undefined;
|
|
765
|
+
if (columns.some((column) => column.source !== first.source || column.column !== first.column)) {
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
return { source: first.source, vector: first.vector };
|
|
769
|
+
}
|
|
690
770
|
function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats) {
|
|
691
771
|
const signature = JSON.stringify(expression);
|
|
692
772
|
if (expression.kind === "subquery") {
|
|
@@ -850,9 +930,14 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
|
|
|
850
930
|
argument.vector.kind === "number"
|
|
851
931
|
? { source: argument.source, vector: argument.vector }
|
|
852
932
|
: undefined;
|
|
933
|
+
const conditional = (expression.name === "COUNT" || expression.name === "SUM" || expression.name === "AVG") &&
|
|
934
|
+
expression.distinct !== true
|
|
935
|
+
? conditionalAggregate(argument)
|
|
936
|
+
: undefined;
|
|
853
937
|
aggregateSpecs.push({
|
|
854
938
|
name: expression.name,
|
|
855
939
|
argument,
|
|
940
|
+
...(conditional === undefined ? {} : { conditional }),
|
|
856
941
|
...(expression.name === "STRING_AGG"
|
|
857
942
|
? { delimiter: required(arguments_[1], "STRING_AGG delimiter is missing") }
|
|
858
943
|
: {}),
|
|
@@ -1225,7 +1310,10 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1225
1310
|
const groups = new GroupAccumulator(plan, memory);
|
|
1226
1311
|
const output = new ResultSink(plan, memory, options.loadScanWindow === undefined);
|
|
1227
1312
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
1228
|
-
|
|
1313
|
+
if (options.scanRows !== undefined && options.loadScanWindow !== undefined) {
|
|
1314
|
+
await scanSelectedRows(plan, options.scanRows, groups, output, memory, options);
|
|
1315
|
+
}
|
|
1316
|
+
for (let start = 0; start < scanRows && options.scanRows === undefined;) {
|
|
1229
1317
|
throwIfAborted(options.signal);
|
|
1230
1318
|
let length = Math.min(DEFAULT_BATCH_ROWS, scanRows - start);
|
|
1231
1319
|
// The loader answers synchronously when the batch is already resident — the common case,
|
|
@@ -1272,6 +1360,49 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1272
1360
|
const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
|
|
1273
1361
|
return finishResult(plan, rows, memory);
|
|
1274
1362
|
}
|
|
1363
|
+
/** Selected rows closer than this run in one batch; the predicates discard the rows between. */
|
|
1364
|
+
const SELECTED_ROW_COALESCE_GAP = 32;
|
|
1365
|
+
/**
|
|
1366
|
+
* Visits only the rows of an index-provided selection. Rows are ascending, so each streamed
|
|
1367
|
+
* window is loaded once; neighbouring selected rows inside a window run as one contiguous
|
|
1368
|
+
* batch, so a clustered hit costs one batch and a scattered one costs a batch per row, never
|
|
1369
|
+
* a batch per unselected row in between.
|
|
1370
|
+
*/
|
|
1371
|
+
async function scanSelectedRows(plan, selection, groups, output, memory, options) {
|
|
1372
|
+
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
1373
|
+
let index = 0;
|
|
1374
|
+
while (index < selection.length) {
|
|
1375
|
+
throwIfAborted(options.signal);
|
|
1376
|
+
const first = selection[index] ?? 0;
|
|
1377
|
+
if (first >= scanRows)
|
|
1378
|
+
break;
|
|
1379
|
+
const loaded = options.loadScanWindow?.(first, 1);
|
|
1380
|
+
const residentEnd = typeof loaded === "number" || loaded === undefined ? loaded : await loaded;
|
|
1381
|
+
throwIfAborted(options.signal);
|
|
1382
|
+
const windowEnd = typeof residentEnd === "number" && residentEnd > first
|
|
1383
|
+
? Math.min(residentEnd, scanRows)
|
|
1384
|
+
: first + 1;
|
|
1385
|
+
while (index < selection.length) {
|
|
1386
|
+
const begin = selection[index] ?? 0;
|
|
1387
|
+
if (begin >= windowEnd)
|
|
1388
|
+
break;
|
|
1389
|
+
let end = begin + 1;
|
|
1390
|
+
index += 1;
|
|
1391
|
+
while (index < selection.length) {
|
|
1392
|
+
const next = selection[index] ?? 0;
|
|
1393
|
+
if (next >= windowEnd ||
|
|
1394
|
+
next - end > SELECTED_ROW_COALESCE_GAP ||
|
|
1395
|
+
next >= begin + DEFAULT_BATCH_ROWS) {
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
end = next + 1;
|
|
1399
|
+
index += 1;
|
|
1400
|
+
}
|
|
1401
|
+
if (runScanBatch(plan, begin, end - begin, groups, output, memory))
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1275
1406
|
/**
|
|
1276
1407
|
* Pull-driven scan execution for cursor-safe plans. A result page owns a child memory context
|
|
1277
1408
|
* that closes after the consumer settles, so both modeled memory and live row objects are
|
|
@@ -1307,6 +1438,7 @@ async function executeBoundPlanBatches(plan, memory, options, consume) {
|
|
|
1307
1438
|
return values.length;
|
|
1308
1439
|
},
|
|
1309
1440
|
tryAddBatch: () => false,
|
|
1441
|
+
tryAddSelection: () => false,
|
|
1310
1442
|
add: (batch, row) => {
|
|
1311
1443
|
values.push(asQueryValue(evaluateBatchExpression(scanPlan, firstExpression, batch, row)));
|
|
1312
1444
|
},
|
|
@@ -2391,8 +2523,91 @@ function applyPredicateKernel(plan, predicate, batch, selection, survivors) {
|
|
|
2391
2523
|
if (predicate.disjunction !== undefined) {
|
|
2392
2524
|
return filterDisjunction(plan, predicate.disjunction, batch, selection, survivors);
|
|
2393
2525
|
}
|
|
2526
|
+
if (predicate.dictionaryPredicate !== undefined) {
|
|
2527
|
+
return filterDictionaryPredicate(plan, predicate, predicate.dictionaryPredicate, batch, selection, survivors);
|
|
2528
|
+
}
|
|
2394
2529
|
return undefined;
|
|
2395
2530
|
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Filters by a predicate that depends on one dictionary column only. The truth for a code is
|
|
2533
|
+
* learned the first time a row carrying that code appears, by running the ordinary per-row
|
|
2534
|
+
* evaluator on that row — so LOWER(status) = 'paid', a CASE over region, or STARTS_WITH on a
|
|
2535
|
+
* category cost one generic evaluation per distinct value per dictionary, then one code read
|
|
2536
|
+
* per row. A NULL code is never a match, exactly as the generic path treats an unknown result.
|
|
2537
|
+
*/
|
|
2538
|
+
function filterDictionaryPredicate(plan, predicate, fast, batch, selection, survivors) {
|
|
2539
|
+
const vector = fast.vector;
|
|
2540
|
+
if (fast.cache.dictionary !== vector.dictionary) {
|
|
2541
|
+
fast.cache.dictionary = vector.dictionary;
|
|
2542
|
+
fast.cache.truth = new Uint8Array(vector.dictionary.length + 1);
|
|
2543
|
+
}
|
|
2544
|
+
const truth = fast.cache.truth;
|
|
2545
|
+
const nullSlot = vector.dictionary.length;
|
|
2546
|
+
const rows = batch.rowsBySource[fast.source];
|
|
2547
|
+
const codes = vector.codes;
|
|
2548
|
+
const validity = vector.validity;
|
|
2549
|
+
const windowStart = vector.window?.start ?? 0;
|
|
2550
|
+
const slots = codes.length;
|
|
2551
|
+
const vectorLength = vector.length;
|
|
2552
|
+
let kept = 0;
|
|
2553
|
+
for (let index = 0; index < survivors; index += 1) {
|
|
2554
|
+
const row = selection[index] ?? 0;
|
|
2555
|
+
const sourceRow = rows?.[row] ?? -1;
|
|
2556
|
+
let code = nullSlot;
|
|
2557
|
+
if (sourceRow >= 0 && sourceRow < vectorLength) {
|
|
2558
|
+
const slot = sourceRow - windowStart;
|
|
2559
|
+
if (slot < 0 || slot >= slots) {
|
|
2560
|
+
throw new RangeError("Streamed vector row is outside the resident window");
|
|
2561
|
+
}
|
|
2562
|
+
if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) !== 0) {
|
|
2563
|
+
const raw = codes[slot] ?? NULL_STRING_CODE;
|
|
2564
|
+
if (raw !== NULL_STRING_CODE)
|
|
2565
|
+
code = raw;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
let known = truth[code] ?? 0;
|
|
2569
|
+
if (known === 0) {
|
|
2570
|
+
known = evaluateBatchPredicate(plan, predicate, batch, row) ? 1 : 2;
|
|
2571
|
+
truth[code] = known;
|
|
2572
|
+
}
|
|
2573
|
+
if (known !== 1)
|
|
2574
|
+
continue;
|
|
2575
|
+
selection[kept] = row;
|
|
2576
|
+
kept += 1;
|
|
2577
|
+
}
|
|
2578
|
+
return kept;
|
|
2579
|
+
}
|
|
2580
|
+
/** Slots one packed key column occupies: dictionary size plus NULL, or the number-code cap. */
|
|
2581
|
+
function multiCodeSpan(column) {
|
|
2582
|
+
return column.codes === undefined ? column.vector.dictionary.length + 1 : NUMBER_KEY_CODE_CAP;
|
|
2583
|
+
}
|
|
2584
|
+
/** Every column node under a bound expression. */
|
|
2585
|
+
function boundColumnNodes(expression, into = []) {
|
|
2586
|
+
if (expression.kind === "column")
|
|
2587
|
+
into.push(expression);
|
|
2588
|
+
else if (expression.kind !== "fts") {
|
|
2589
|
+
for (const child of boundChildren(expression))
|
|
2590
|
+
boundColumnNodes(child, into);
|
|
2591
|
+
}
|
|
2592
|
+
return into;
|
|
2593
|
+
}
|
|
2594
|
+
function boundContainsFts(expression) {
|
|
2595
|
+
if (expression.kind === "fts")
|
|
2596
|
+
return true;
|
|
2597
|
+
return boundChildren(expression).some(boundContainsFts);
|
|
2598
|
+
}
|
|
2599
|
+
function detectDictionaryPredicate(predicate) {
|
|
2600
|
+
const column = singleStringColumn({
|
|
2601
|
+
kind: "logical",
|
|
2602
|
+
operator: "and",
|
|
2603
|
+
left: predicate.left,
|
|
2604
|
+
right: predicate.right,
|
|
2605
|
+
signature: "",
|
|
2606
|
+
});
|
|
2607
|
+
return column === undefined
|
|
2608
|
+
? undefined
|
|
2609
|
+
: { ...column, cache: { dictionary: undefined, truth: new Uint8Array(0) } };
|
|
2610
|
+
}
|
|
2396
2611
|
// Scratch for the disjunction kernel. Branch predicates are always plain conditions, so a
|
|
2397
2612
|
// branch never carries a disjunction of its own and these buffers are never reentered.
|
|
2398
2613
|
let disjunctionCandidates = new Uint32Array(DEFAULT_BATCH_ROWS);
|
|
@@ -2938,6 +3153,12 @@ class ResultSink {
|
|
|
2938
3153
|
* Returns false when the sink shape needs the per-row path.
|
|
2939
3154
|
*/
|
|
2940
3155
|
tryAddBatch(batch) {
|
|
3156
|
+
return this.#tryAddRows(batch, undefined, batch.length);
|
|
3157
|
+
}
|
|
3158
|
+
tryAddSelection(batch, selection, survivors) {
|
|
3159
|
+
return this.#tryAddRows(batch, selection, survivors);
|
|
3160
|
+
}
|
|
3161
|
+
#tryAddRows(batch, selection, count) {
|
|
2941
3162
|
const fast = this.#fastFirstKey;
|
|
2942
3163
|
if (this.#capacity === undefined || fast === undefined)
|
|
2943
3164
|
return false;
|
|
@@ -2950,7 +3171,19 @@ class ResultSink {
|
|
|
2950
3171
|
const windowStart = window?.start ?? 0;
|
|
2951
3172
|
const windowLength = window?.length ?? vector.length;
|
|
2952
3173
|
const desc = fast.desc;
|
|
2953
|
-
|
|
3174
|
+
// A key that arrives sorted against the requested direction — `ORDER BY id DESC` over a
|
|
3175
|
+
// table stored in key order, the newest-first page — would otherwise improve on the cut
|
|
3176
|
+
// line on every row and rebuild the selection 200,000 times. Walking such a batch from
|
|
3177
|
+
// its last row establishes the cut line within `capacity` rows and rejects the rest on the
|
|
3178
|
+
// float comparison. Row order within a batch never affects the answer: ties keep the
|
|
3179
|
+
// earlier `seq`, and seq numbers are assigned in scan order below either way.
|
|
3180
|
+
const reversed = desc && this.#batchKeyIsAscending(rows, vector, selection, count);
|
|
3181
|
+
const firstSeq = this.#seq;
|
|
3182
|
+
for (let step = 0; step < count; step += 1) {
|
|
3183
|
+
const position = reversed ? count - 1 - step : step;
|
|
3184
|
+
const row = selection === undefined ? position : (selection[position] ?? 0);
|
|
3185
|
+
if (reversed)
|
|
3186
|
+
this.#seq = firstSeq + position;
|
|
2954
3187
|
const threshold = this.#thresholdFirst;
|
|
2955
3188
|
if (threshold === undefined || threshold === null) {
|
|
2956
3189
|
this.#addCandidate(batch, row);
|
|
@@ -2969,6 +3202,31 @@ class ResultSink {
|
|
|
2969
3202
|
}
|
|
2970
3203
|
this.#addCandidate(batch, row);
|
|
2971
3204
|
}
|
|
3205
|
+
if (reversed)
|
|
3206
|
+
this.#seq = firstSeq + count;
|
|
3207
|
+
return true;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Whether the rows to add read the first order key in ascending order, checked directly on
|
|
3211
|
+
* the resident values: one unboxed pass, cheaper than a single candidate insertion.
|
|
3212
|
+
*/
|
|
3213
|
+
#batchKeyIsAscending(rows, vector, selection, count) {
|
|
3214
|
+
if (rows === undefined || count < 2)
|
|
3215
|
+
return false;
|
|
3216
|
+
const windowStart = vector.window?.start ?? 0;
|
|
3217
|
+
const values = vector.values;
|
|
3218
|
+
const validity = vector.validity;
|
|
3219
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
3220
|
+
for (let position = 0; position < count; position += 1) {
|
|
3221
|
+
const row = selection === undefined ? position : (selection[position] ?? 0);
|
|
3222
|
+
const slot = (rows[row] ?? -1) - windowStart;
|
|
3223
|
+
if (slot < 0 || slot >= values.length || !isValid(validity, slot))
|
|
3224
|
+
return false;
|
|
3225
|
+
const value = values[slot] ?? 0;
|
|
3226
|
+
if (!(value >= previous))
|
|
3227
|
+
return false;
|
|
3228
|
+
previous = value;
|
|
3229
|
+
}
|
|
2972
3230
|
return true;
|
|
2973
3231
|
}
|
|
2974
3232
|
add(batch, row) {
|
|
@@ -3198,7 +3456,13 @@ class GroupAccumulator {
|
|
|
3198
3456
|
#codeColumns;
|
|
3199
3457
|
#multiCodeColumns;
|
|
3200
3458
|
#multiCodeStates;
|
|
3459
|
+
/** The dictionaries the packed slot table was laid out for; any swap re-lays it. */
|
|
3460
|
+
#multiCodeDictionaries = [];
|
|
3461
|
+
#multiCodeSlotsReserved = 0;
|
|
3201
3462
|
#multiCodeScratch = [];
|
|
3463
|
+
/** Raw-number grouping: state per finite value, plus the NULL group. */
|
|
3464
|
+
#numberStates;
|
|
3465
|
+
#numberNullState;
|
|
3202
3466
|
#keyScratch = [];
|
|
3203
3467
|
// The miss factories live on the accumulator and read the pending row through these fields, so
|
|
3204
3468
|
// the per-row lookup never allocates a capturing closure; execution is synchronous, so the
|
|
@@ -3229,42 +3493,93 @@ class GroupAccumulator {
|
|
|
3229
3493
|
// are stable and value-unique within one execution, so the key encodes a fixed-width number
|
|
3230
3494
|
// instead of re-encoding the string's UTF-8 on every row. Types are stable per position, so
|
|
3231
3495
|
// a code can never collide with a genuine number from the same expression.
|
|
3232
|
-
|
|
3233
|
-
plan.groupBy.
|
|
3234
|
-
?
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
//
|
|
3241
|
-
// group
|
|
3242
|
-
//
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3496
|
+
const codeColumns = plan.groupBy.length > 1
|
|
3497
|
+
? plan.groupBy.map((expression) => expression.kind === "column" && expression.vector.kind === "string"
|
|
3498
|
+
? { source: expression.source, vector: expression.vector }
|
|
3499
|
+
: undefined)
|
|
3500
|
+
: [];
|
|
3501
|
+
// When every compound key column is a bare string, number, or datetime column, group lookup
|
|
3502
|
+
// packs one code per column into one integer slot and byte-encodes a key only on the first
|
|
3503
|
+
// row of each combination per window. The slot table is laid out lazily by
|
|
3504
|
+
// #ensureMultiCodeStates, which re-lays it whenever a streamed window swaps a dictionary;
|
|
3505
|
+
// the group states themselves live in the value-keyed index, so a combination seen in two
|
|
3506
|
+
// windows is one group.
|
|
3507
|
+
const packed = plan.groupBy.length > 1
|
|
3508
|
+
? plan.groupBy.map((expression) => {
|
|
3509
|
+
if (expression.kind !== "column")
|
|
3510
|
+
return undefined;
|
|
3511
|
+
if (expression.vector.kind === "string") {
|
|
3512
|
+
return { source: expression.source, vector: expression.vector };
|
|
3513
|
+
}
|
|
3514
|
+
if (expression.vector.kind === "number" || expression.vector.kind === "datetime") {
|
|
3515
|
+
return {
|
|
3516
|
+
source: expression.source,
|
|
3517
|
+
vector: expression.vector,
|
|
3518
|
+
codes: new Map(),
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
return undefined;
|
|
3522
|
+
})
|
|
3523
|
+
: [];
|
|
3524
|
+
if (packed.length > 1 && packed.every((column) => column !== undefined)) {
|
|
3525
|
+
this.#multiCodeColumns = packed;
|
|
3526
|
+
this.#codeColumns = [];
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
// Otherwise the compound key substitutes the dictionary code for each bare unwindowed string
|
|
3530
|
+
// column: codes are stable and value-unique within one execution, so the key encodes a
|
|
3531
|
+
// fixed-width number instead of re-encoding the string's UTF-8 on every row. Types are
|
|
3532
|
+
// stable per position, so a code can never collide with a genuine number from the same
|
|
3533
|
+
// expression.
|
|
3534
|
+
this.#codeColumns = codeColumns.map((column) => column?.vector.window === undefined ? column : undefined);
|
|
3535
|
+
if (plan.numberGrouping !== undefined)
|
|
3536
|
+
this.#numberStates = new Map();
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Lays out the packed slot table for the current window dictionaries. Small combined domains
|
|
3540
|
+
* use a direct array (reserved against the budget); large ones a numeric Map. Returns false
|
|
3541
|
+
* when the budget refuses the array, which sends the query to the generic per-row index.
|
|
3542
|
+
*/
|
|
3543
|
+
#ensureMultiCodeStates(columns) {
|
|
3544
|
+
let current = true;
|
|
3545
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
3546
|
+
const column = columns[index];
|
|
3547
|
+
if (column?.codes !== undefined)
|
|
3548
|
+
continue;
|
|
3549
|
+
if (this.#multiCodeDictionaries[index] !== column?.vector.dictionary) {
|
|
3550
|
+
current = false;
|
|
3551
|
+
break;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
if (current && (this.#codeStates !== undefined || this.#multiCodeStates !== undefined)) {
|
|
3555
|
+
return true;
|
|
3556
|
+
}
|
|
3557
|
+
let slots = 1;
|
|
3558
|
+
for (const column of columns)
|
|
3559
|
+
slots *= multiCodeSpan(column);
|
|
3560
|
+
if (!Number.isSafeInteger(slots))
|
|
3561
|
+
return false;
|
|
3562
|
+
if (slots <= MULTI_CODE_GROUP_SLOT_CAP) {
|
|
3563
|
+
if (slots > this.#multiCodeSlotsReserved) {
|
|
3250
3564
|
try {
|
|
3251
|
-
memory.reserve(safeMemoryProduct(slots, QUERY_REFERENCE_BYTES, "Group code slots"), "Group code slots");
|
|
3565
|
+
this.#memory.reserve(safeMemoryProduct(slots - this.#multiCodeSlotsReserved, QUERY_REFERENCE_BYTES, "Group code slots"), "Group code slots");
|
|
3252
3566
|
}
|
|
3253
3567
|
catch (error) {
|
|
3254
3568
|
if (!(error instanceof QueryMemoryBudgetError))
|
|
3255
3569
|
throw error;
|
|
3256
|
-
return;
|
|
3570
|
+
return false;
|
|
3257
3571
|
}
|
|
3258
|
-
this.#
|
|
3259
|
-
this.#codeStates = new Array(slots).fill(undefined);
|
|
3260
|
-
this.#ordered = [];
|
|
3261
|
-
}
|
|
3262
|
-
else if (Number.isSafeInteger(slots)) {
|
|
3263
|
-
this.#multiCodeColumns = columns;
|
|
3264
|
-
this.#multiCodeStates = new Map();
|
|
3265
|
-
this.#ordered = [];
|
|
3572
|
+
this.#multiCodeSlotsReserved = slots;
|
|
3266
3573
|
}
|
|
3574
|
+
this.#codeStates = new Array(slots).fill(undefined);
|
|
3575
|
+
this.#multiCodeStates = undefined;
|
|
3267
3576
|
}
|
|
3577
|
+
else {
|
|
3578
|
+
this.#codeStates = undefined;
|
|
3579
|
+
this.#multiCodeStates = new Map();
|
|
3580
|
+
}
|
|
3581
|
+
this.#multiCodeDictionaries = columns.map((column) => column.codes === undefined ? column.vector.dictionary : undefined);
|
|
3582
|
+
return true;
|
|
3268
3583
|
}
|
|
3269
3584
|
/**
|
|
3270
3585
|
* Fast aggregate specs for the batch kernel: every aggregate is COUNT(*) or a bare numeric
|
|
@@ -3286,6 +3601,10 @@ class GroupAccumulator {
|
|
|
3286
3601
|
specs.push({ kind: "column", sums: spec.name !== "COUNT" });
|
|
3287
3602
|
continue;
|
|
3288
3603
|
}
|
|
3604
|
+
if (spec.conditional !== undefined) {
|
|
3605
|
+
specs.push({ kind: "conditional", sums: spec.name !== "COUNT" });
|
|
3606
|
+
continue;
|
|
3607
|
+
}
|
|
3289
3608
|
return undefined;
|
|
3290
3609
|
}
|
|
3291
3610
|
return specs;
|
|
@@ -3299,7 +3618,10 @@ class GroupAccumulator {
|
|
|
3299
3618
|
const plan = this.#plan;
|
|
3300
3619
|
const codeGrouping = plan.codeGrouping;
|
|
3301
3620
|
const globalGroup = plan.groupBy.length === 0;
|
|
3302
|
-
|
|
3621
|
+
const numberGrouped = plan.numberGrouping !== undefined && this.#numberStates !== undefined;
|
|
3622
|
+
if (!globalGroup &&
|
|
3623
|
+
!numberGrouped &&
|
|
3624
|
+
(codeGrouping === undefined || this.#codeStates === undefined)) {
|
|
3303
3625
|
return false;
|
|
3304
3626
|
}
|
|
3305
3627
|
const specs = this.#fastAggregatesCache;
|
|
@@ -3324,6 +3646,10 @@ class GroupAccumulator {
|
|
|
3324
3646
|
: undefined;
|
|
3325
3647
|
// Hoisted per-column reads: the row loop touches only local typed arrays and numbers.
|
|
3326
3648
|
const columns = [];
|
|
3649
|
+
// Conditional aggregates: the branch for a dictionary code is learned from the first row
|
|
3650
|
+
// carrying it (the generic truth evaluator on that row), then every later row with the code
|
|
3651
|
+
// reads its branch's number column or literal directly.
|
|
3652
|
+
const conditionals = [];
|
|
3327
3653
|
let stars = 0;
|
|
3328
3654
|
for (let index = 0; index < specs.length; index += 1) {
|
|
3329
3655
|
const spec = specs[index];
|
|
@@ -3331,6 +3657,26 @@ class GroupAccumulator {
|
|
|
3331
3657
|
stars += 1;
|
|
3332
3658
|
continue;
|
|
3333
3659
|
}
|
|
3660
|
+
if (spec.kind === "conditional") {
|
|
3661
|
+
const conditional = plan.aggregates[index]?.conditional;
|
|
3662
|
+
if (conditional === undefined)
|
|
3663
|
+
return false;
|
|
3664
|
+
const vector = conditional.vector;
|
|
3665
|
+
if (conditional.cache.dictionary !== vector.dictionary) {
|
|
3666
|
+
conditional.cache.dictionary = vector.dictionary;
|
|
3667
|
+
conditional.cache.branch = new Int32Array(vector.dictionary.length + 1).fill(-2);
|
|
3668
|
+
}
|
|
3669
|
+
conditionals.push({
|
|
3670
|
+
index,
|
|
3671
|
+
sums: spec.sums,
|
|
3672
|
+
spec: conditional,
|
|
3673
|
+
rows: batch.rowsBySource[conditional.source],
|
|
3674
|
+
branch: conditional.cache.branch,
|
|
3675
|
+
nullSlot: vector.dictionary.length,
|
|
3676
|
+
truths: conditional.branches.map((branch) => compiledTruth(plan, branch.when)),
|
|
3677
|
+
});
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3334
3680
|
const raw = plan.aggregates[index]?.rawNumber;
|
|
3335
3681
|
if (raw === undefined)
|
|
3336
3682
|
return false;
|
|
@@ -3371,14 +3717,10 @@ class GroupAccumulator {
|
|
|
3371
3717
|
code = rawCode;
|
|
3372
3718
|
}
|
|
3373
3719
|
}
|
|
3374
|
-
state = states[code];
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
states[code] = state;
|
|
3379
|
-
this.#registerCodeState(value, state);
|
|
3380
|
-
this.#ordered?.push(state);
|
|
3381
|
-
}
|
|
3720
|
+
state = states[code] ?? this.#createCodeState(batch, row, code, grouping.dictionary);
|
|
3721
|
+
}
|
|
3722
|
+
else if (state === undefined && numberGrouped) {
|
|
3723
|
+
state = this.stateFor(batch, row);
|
|
3382
3724
|
}
|
|
3383
3725
|
if (state === undefined)
|
|
3384
3726
|
return false;
|
|
@@ -3404,6 +3746,36 @@ class GroupAccumulator {
|
|
|
3404
3746
|
state.sums[column.index] = (state.sums[column.index] ?? 0) + (column.values[slot] ?? 0);
|
|
3405
3747
|
}
|
|
3406
3748
|
}
|
|
3749
|
+
for (const conditional of conditionals) {
|
|
3750
|
+
const code = stringCodeAt(conditional.spec.vector, conditional.rows?.[row] ?? -1) ??
|
|
3751
|
+
conditional.nullSlot;
|
|
3752
|
+
let chosen = conditional.branch[code] ?? -2;
|
|
3753
|
+
if (chosen === -2) {
|
|
3754
|
+
chosen = -1;
|
|
3755
|
+
for (let index = 0; index < conditional.truths.length; index += 1) {
|
|
3756
|
+
if (conditional.truths[index]?.(batch, row) === true) {
|
|
3757
|
+
chosen = index;
|
|
3758
|
+
break;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
conditional.branch[code] = chosen;
|
|
3762
|
+
}
|
|
3763
|
+
const branchValue = chosen < 0 ? conditional.spec.otherwise : conditional.spec.branches[chosen]?.value;
|
|
3764
|
+
if (branchValue === undefined)
|
|
3765
|
+
return false;
|
|
3766
|
+
let value;
|
|
3767
|
+
if (branchValue.kind === "literal")
|
|
3768
|
+
value = branchValue.value;
|
|
3769
|
+
else {
|
|
3770
|
+
value = rawFloat64Value(branchValue.vector, batch.rowsBySource[branchValue.source]?.[row] ?? -1);
|
|
3771
|
+
}
|
|
3772
|
+
if (value === null)
|
|
3773
|
+
continue;
|
|
3774
|
+
counts[conditional.index] = (counts[conditional.index] ?? 0) + 1;
|
|
3775
|
+
if (conditional.sums) {
|
|
3776
|
+
state.sums[conditional.index] = (state.sums[conditional.index] ?? 0) + value;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3407
3779
|
}
|
|
3408
3780
|
return true;
|
|
3409
3781
|
}
|
|
@@ -3425,12 +3797,14 @@ class GroupAccumulator {
|
|
|
3425
3797
|
this.#codeSlotsReserved = slots;
|
|
3426
3798
|
}
|
|
3427
3799
|
const next = new Array(slots).fill(undefined);
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3800
|
+
if (this.#plan.codeGrouping?.expression === undefined) {
|
|
3801
|
+
for (let code = 0; code < dictionary.length; code += 1) {
|
|
3802
|
+
const state = this.#codeStateByValue.get(dictionary[code] ?? "");
|
|
3803
|
+
if (state !== undefined)
|
|
3804
|
+
next[code] = state;
|
|
3805
|
+
}
|
|
3806
|
+
next[dictionary.length] = this.#nullCodeState;
|
|
3432
3807
|
}
|
|
3433
|
-
next[dictionary.length] = this.#nullCodeState;
|
|
3434
3808
|
this.#codeStates = next;
|
|
3435
3809
|
this.#codeDictionary = dictionary;
|
|
3436
3810
|
}
|
|
@@ -3441,6 +3815,33 @@ class GroupAccumulator {
|
|
|
3441
3815
|
else
|
|
3442
3816
|
this.#codeStateByValue.set(value, state);
|
|
3443
3817
|
}
|
|
3818
|
+
/**
|
|
3819
|
+
* Fills the slot for a dictionary code seen for the first time. A bare column's slot is a new
|
|
3820
|
+
* state keyed by the dictionary value; an expression key evaluates the expression on this row
|
|
3821
|
+
* (the representative of its code) and shares the state of any earlier code that produced the
|
|
3822
|
+
* same value, so UPPER('a') and UPPER('A') land in one group.
|
|
3823
|
+
*/
|
|
3824
|
+
#createCodeState(batch, row, code, dictionary) {
|
|
3825
|
+
const states = required(this.#codeStates, "Group code slots are missing");
|
|
3826
|
+
const expression = this.#plan.codeGrouping?.expression;
|
|
3827
|
+
let state;
|
|
3828
|
+
if (expression === undefined) {
|
|
3829
|
+
const value = code === dictionary.length ? null : (dictionary[code] ?? null);
|
|
3830
|
+
state = createGroupState([value], this.#plan, this.#memory);
|
|
3831
|
+
this.#registerCodeState(value, state);
|
|
3832
|
+
this.#ordered?.push(state);
|
|
3833
|
+
}
|
|
3834
|
+
else {
|
|
3835
|
+
const value = asQueryValue(evaluateBatchExpression(this.#plan, expression, batch, row));
|
|
3836
|
+
this.#pendingSingleValue = value;
|
|
3837
|
+
const before = this.#index.size;
|
|
3838
|
+
state = this.#index.getOrInsertOne(groupKey(value), this.#createPendingSingle);
|
|
3839
|
+
if (this.#index.size !== before)
|
|
3840
|
+
this.#ordered?.push(state);
|
|
3841
|
+
}
|
|
3842
|
+
states[code] = state;
|
|
3843
|
+
return state;
|
|
3844
|
+
}
|
|
3444
3845
|
/** Resolves the group state for one row, creating it on first touch. */
|
|
3445
3846
|
stateFor(batch, row) {
|
|
3446
3847
|
const plan = this.#plan;
|
|
@@ -3463,55 +3864,115 @@ class GroupAccumulator {
|
|
|
3463
3864
|
code = rawCode;
|
|
3464
3865
|
}
|
|
3465
3866
|
}
|
|
3466
|
-
|
|
3467
|
-
if (state === undefined) {
|
|
3468
|
-
const value = code === vector.dictionary.length ? null : (vector.dictionary[code] ?? null);
|
|
3469
|
-
state = createGroupState([value], plan, this.#memory);
|
|
3470
|
-
states[code] = state;
|
|
3471
|
-
this.#registerCodeState(value, state);
|
|
3472
|
-
this.#ordered?.push(state);
|
|
3473
|
-
}
|
|
3474
|
-
return state;
|
|
3867
|
+
return states[code] ?? this.#createCodeState(batch, row, code, vector.dictionary);
|
|
3475
3868
|
}
|
|
3476
3869
|
if (plan.groupBy.length === 0) {
|
|
3477
3870
|
return required(this.#index.getEmpty(), "Grouped query state is missing");
|
|
3478
3871
|
}
|
|
3479
3872
|
const multiCode = this.#multiCodeColumns;
|
|
3480
|
-
if (multiCode !== undefined
|
|
3481
|
-
(this.#
|
|
3873
|
+
if (multiCode !== undefined) {
|
|
3874
|
+
if (!this.#ensureMultiCodeStates(multiCode)) {
|
|
3875
|
+
this.#multiCodeColumns = undefined;
|
|
3876
|
+
this.#codeStates = undefined;
|
|
3877
|
+
this.#multiCodeStates = undefined;
|
|
3878
|
+
return this.stateFor(batch, row);
|
|
3879
|
+
}
|
|
3482
3880
|
let slot = 0;
|
|
3483
3881
|
for (let index = 0; index < multiCode.length; index += 1) {
|
|
3484
3882
|
const column = required(multiCode[index], "Group code column is missing");
|
|
3485
|
-
const vector = column.vector;
|
|
3486
3883
|
const sourceRow = batch.rowsBySource[column.source]?.[row] ?? -1;
|
|
3487
|
-
let code
|
|
3488
|
-
if (
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3884
|
+
let code;
|
|
3885
|
+
if (column.codes === undefined) {
|
|
3886
|
+
code = stringCodeAt(column.vector, sourceRow) ?? column.vector.dictionary.length;
|
|
3887
|
+
}
|
|
3888
|
+
else {
|
|
3889
|
+
// Code 0 is NULL and every other finite value gets the next dense code; a key column
|
|
3890
|
+
// with too many distinct values, or a non-finite one, hands the query to the index.
|
|
3891
|
+
const raw = rawFloat64Value(column.vector, sourceRow);
|
|
3892
|
+
if (raw === null)
|
|
3893
|
+
code = 0;
|
|
3894
|
+
else if (!Number.isFinite(raw))
|
|
3895
|
+
code = -1;
|
|
3896
|
+
else {
|
|
3897
|
+
const known = column.codes.get(raw);
|
|
3898
|
+
if (known !== undefined)
|
|
3899
|
+
code = known;
|
|
3900
|
+
else if (column.codes.size + 1 >= NUMBER_KEY_CODE_CAP)
|
|
3901
|
+
code = -1;
|
|
3902
|
+
else {
|
|
3903
|
+
code = column.codes.size + 1;
|
|
3904
|
+
this.#memory.tally(PACKED_GROUP_ENTRY_BYTES, "Packed group key code");
|
|
3905
|
+
column.codes.set(raw, code);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
if (code < 0) {
|
|
3909
|
+
this.#multiCodeColumns = undefined;
|
|
3910
|
+
this.#codeStates = undefined;
|
|
3911
|
+
this.#multiCodeStates = undefined;
|
|
3912
|
+
return this.stateFor(batch, row);
|
|
3913
|
+
}
|
|
3492
3914
|
}
|
|
3493
3915
|
this.#multiCodeScratch[index] = code;
|
|
3494
|
-
slot = slot * (
|
|
3916
|
+
slot = slot * multiCodeSpan(column) + code;
|
|
3495
3917
|
}
|
|
3496
3918
|
let state = this.#codeStates?.[slot] ?? this.#multiCodeStates?.get(slot);
|
|
3497
3919
|
if (state === undefined) {
|
|
3498
|
-
const groupValues = [];
|
|
3499
3920
|
for (let index = 0; index < multiCode.length; index += 1) {
|
|
3500
|
-
const
|
|
3501
|
-
const code = this.#multiCodeScratch[index] ??
|
|
3502
|
-
|
|
3921
|
+
const column = required(multiCode[index], "Group code column is missing");
|
|
3922
|
+
const code = this.#multiCodeScratch[index] ?? 0;
|
|
3923
|
+
if (column.codes === undefined) {
|
|
3924
|
+
const vector = column.vector;
|
|
3925
|
+
this.#keyScratch[index] =
|
|
3926
|
+
code === vector.dictionary.length ? null : (vector.dictionary[code] ?? null);
|
|
3927
|
+
}
|
|
3928
|
+
else {
|
|
3929
|
+
const raw = rawFloat64Value(column.vector, batch.rowsBySource[column.source]?.[row] ?? -1);
|
|
3930
|
+
this.#keyScratch[index] =
|
|
3931
|
+
raw === null
|
|
3932
|
+
? null
|
|
3933
|
+
: groupKey(column.vector.kind === "datetime" ? new Date(raw) : raw);
|
|
3934
|
+
}
|
|
3503
3935
|
}
|
|
3504
|
-
|
|
3936
|
+
this.#keyScratch.length = multiCode.length;
|
|
3937
|
+
this.#pendingBatch = batch;
|
|
3938
|
+
this.#pendingRow = row;
|
|
3939
|
+
state = this.#index.getOrInsert(this.#keyScratch, this.#createPendingCompound);
|
|
3505
3940
|
if (this.#codeStates !== undefined)
|
|
3506
3941
|
this.#codeStates[slot] = state;
|
|
3507
3942
|
else {
|
|
3508
3943
|
this.#memory.tally(PACKED_GROUP_ENTRY_BYTES, "Packed group index entry");
|
|
3509
3944
|
this.#multiCodeStates?.set(slot, state);
|
|
3510
3945
|
}
|
|
3511
|
-
this.#ordered?.push(state);
|
|
3512
3946
|
}
|
|
3513
3947
|
return state;
|
|
3514
3948
|
}
|
|
3949
|
+
const numberGrouping = plan.numberGrouping;
|
|
3950
|
+
const numberStates = this.#numberStates;
|
|
3951
|
+
if (numberGrouping !== undefined && numberStates !== undefined) {
|
|
3952
|
+
const raw = rawFloat64Value(numberGrouping.vector, batch.rowsBySource[numberGrouping.source]?.[row] ?? -1);
|
|
3953
|
+
if (raw === null) {
|
|
3954
|
+
let state = this.#numberNullState;
|
|
3955
|
+
if (state === undefined) {
|
|
3956
|
+
this.#pendingSingleValue = null;
|
|
3957
|
+
state = this.#index.getOrInsertOne(null, this.#createPendingSingle);
|
|
3958
|
+
this.#numberNullState = state;
|
|
3959
|
+
}
|
|
3960
|
+
return state;
|
|
3961
|
+
}
|
|
3962
|
+
// Non-finite values keep the generic key rules (they share the NULL key), so only finite
|
|
3963
|
+
// values are cached by raw value.
|
|
3964
|
+
if (Number.isFinite(raw)) {
|
|
3965
|
+
let state = numberStates.get(raw);
|
|
3966
|
+
if (state === undefined) {
|
|
3967
|
+
const value = numberGrouping.vector.kind === "datetime" ? new Date(raw) : raw;
|
|
3968
|
+
this.#pendingSingleValue = value;
|
|
3969
|
+
state = this.#index.getOrInsertOne(groupKey(value), this.#createPendingSingle);
|
|
3970
|
+
this.#memory.tally(PACKED_GROUP_ENTRY_BYTES, "Packed group index entry");
|
|
3971
|
+
numberStates.set(raw, state);
|
|
3972
|
+
}
|
|
3973
|
+
return state;
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3515
3976
|
if (plan.groupBy.length === 1) {
|
|
3516
3977
|
const groupValue = asQueryValue(evaluateBatchExpression(plan, required(plan.groupBy[0], "Group expression is missing"), batch, row));
|
|
3517
3978
|
this.#pendingSingleValue = groupValue;
|
|
@@ -3567,6 +4028,8 @@ function consumeBatch(plan, batch, groups, output, memory, prefiltered = false)
|
|
|
3567
4028
|
}
|
|
3568
4029
|
return;
|
|
3569
4030
|
}
|
|
4031
|
+
if (output.tryAddSelection(batch, selection, survivors))
|
|
4032
|
+
return;
|
|
3570
4033
|
for (let index = 0; index < survivors; index += 1) {
|
|
3571
4034
|
output.add(batch, selection[index] ?? 0);
|
|
3572
4035
|
if (reachedEarlyLimit(plan, output.size))
|
|
@@ -3680,7 +4143,21 @@ function updateAggregates(plan, state, batch, row, memory) {
|
|
|
3680
4143
|
// the single surviving value.
|
|
3681
4144
|
if (spec.rawDatetime !== undefined) {
|
|
3682
4145
|
const sourceRow = batch.rowsBySource[spec.rawDatetime.source]?.[row] ?? -1;
|
|
3683
|
-
|
|
4146
|
+
const value = rawFloat64Value(spec.rawDatetime.vector, sourceRow);
|
|
4147
|
+
if (value !== null &&
|
|
4148
|
+
spec.distinct !== true &&
|
|
4149
|
+
(spec.name === "MIN" || spec.name === "MAX")) {
|
|
4150
|
+
// Epoch milliseconds compare as plain numbers; the generic comparator would classify
|
|
4151
|
+
// both operands on every row of the scan.
|
|
4152
|
+
state.counts[index] = (state.counts[index] ?? 0) + 1;
|
|
4153
|
+
const current = state.values[index];
|
|
4154
|
+
if (typeof current !== "number" ||
|
|
4155
|
+
(spec.name === "MIN" ? value < current : value > current)) {
|
|
4156
|
+
replaceAggregateValue(state, index, value, `${spec.name} aggregate value`, memory);
|
|
4157
|
+
}
|
|
4158
|
+
continue;
|
|
4159
|
+
}
|
|
4160
|
+
applyAggregateValue(spec, state, index, value, memory);
|
|
3684
4161
|
continue;
|
|
3685
4162
|
}
|
|
3686
4163
|
// A bare number column reads its Float64Array slot directly and accumulates SUM/AVG into
|
|
@@ -3697,16 +4174,21 @@ function updateAggregates(plan, state, batch, row, memory) {
|
|
|
3697
4174
|
if (spec.name === "SUM" || spec.name === "AVG") {
|
|
3698
4175
|
state.sums[index] = (state.sums[index] ?? 0) + value;
|
|
3699
4176
|
}
|
|
3700
|
-
else if (spec.name === "MIN") {
|
|
4177
|
+
else if (spec.name === "MIN" || spec.name === "MAX") {
|
|
4178
|
+
// A raw number compares as a number. NaN keeps compareValues's place, after every
|
|
4179
|
+
// finite value: it is never the minimum and always the maximum once seen.
|
|
3701
4180
|
const current = state.values[index];
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
4181
|
+
const better = typeof current !== "number"
|
|
4182
|
+
? true
|
|
4183
|
+
: Number.isNaN(value)
|
|
4184
|
+
? spec.name === "MAX" && !Number.isNaN(current)
|
|
4185
|
+
: Number.isNaN(current)
|
|
4186
|
+
? spec.name === "MIN"
|
|
4187
|
+
: spec.name === "MIN"
|
|
4188
|
+
? value < current
|
|
4189
|
+
: value > current;
|
|
4190
|
+
if (better) {
|
|
4191
|
+
replaceAggregateValue(state, index, value, `${spec.name} aggregate value`, memory);
|
|
3710
4192
|
}
|
|
3711
4193
|
}
|
|
3712
4194
|
}
|
|
@@ -4648,50 +5130,134 @@ function inListHolds(operator, value, items) {
|
|
|
4648
5130
|
}
|
|
4649
5131
|
return operator === "NOT IN" && !hasNull;
|
|
4650
5132
|
}
|
|
5133
|
+
const compiledBatchExpressions = new WeakMap();
|
|
5134
|
+
/**
|
|
5135
|
+
* Compiles a bound expression to a closure once per plan, so a scan does not re-dispatch on the
|
|
5136
|
+
* node kind, re-check the function name, or allocate an argument array on every row. The
|
|
5137
|
+
* closures call the same value functions the interpreter does — binaryValue,
|
|
5138
|
+
* scalarFunctionValue, booleanTruth — so semantics are shared, only the walk is hoisted.
|
|
5139
|
+
*/
|
|
5140
|
+
function compiledBatchExpression(plan, expression) {
|
|
5141
|
+
const cached = compiledBatchExpressions.get(expression);
|
|
5142
|
+
if (cached !== undefined)
|
|
5143
|
+
return cached;
|
|
5144
|
+
let compiled;
|
|
5145
|
+
switch (expression.kind) {
|
|
5146
|
+
case "literal": {
|
|
5147
|
+
const value = expression.value;
|
|
5148
|
+
compiled = () => value;
|
|
5149
|
+
break;
|
|
5150
|
+
}
|
|
5151
|
+
case "wildcard":
|
|
5152
|
+
compiled = () => 1;
|
|
5153
|
+
break;
|
|
5154
|
+
case "column": {
|
|
5155
|
+
const vector = expression.vector;
|
|
5156
|
+
const source = expression.source;
|
|
5157
|
+
compiled = (batch, row) => vectorValue(vector, batch.rowsBySource[source]?.[row] ?? -1);
|
|
5158
|
+
break;
|
|
5159
|
+
}
|
|
5160
|
+
case "binary": {
|
|
5161
|
+
const operator = expression.operator;
|
|
5162
|
+
const left = compiledBatchExpression(plan, expression.left);
|
|
5163
|
+
const right = compiledBatchExpression(plan, expression.right);
|
|
5164
|
+
compiled = (batch, row) => binaryValue(operator, left(batch, row), right(batch, row));
|
|
5165
|
+
break;
|
|
5166
|
+
}
|
|
5167
|
+
case "case": {
|
|
5168
|
+
const branches = expression.branches.map((branch) => ({
|
|
5169
|
+
when: compiledTruth(plan, branch.when),
|
|
5170
|
+
then: compiledBatchExpression(plan, branch.then),
|
|
5171
|
+
}));
|
|
5172
|
+
const otherwise = expression.otherwise === undefined
|
|
5173
|
+
? undefined
|
|
5174
|
+
: compiledBatchExpression(plan, expression.otherwise);
|
|
5175
|
+
compiled = (batch, row) => {
|
|
5176
|
+
for (const branch of branches) {
|
|
5177
|
+
if (branch.when(batch, row) === true)
|
|
5178
|
+
return branch.then(batch, row);
|
|
5179
|
+
}
|
|
5180
|
+
return otherwise === undefined ? null : otherwise(batch, row);
|
|
5181
|
+
};
|
|
5182
|
+
break;
|
|
5183
|
+
}
|
|
5184
|
+
case "condition":
|
|
5185
|
+
case "logical":
|
|
5186
|
+
case "not": {
|
|
5187
|
+
const truth = compiledTruth(plan, expression);
|
|
5188
|
+
compiled = truth;
|
|
5189
|
+
break;
|
|
5190
|
+
}
|
|
5191
|
+
case "fts":
|
|
5192
|
+
compiled = (batch, row) => expression.op === "match"
|
|
5193
|
+
? ftsBatchTruth(expression, batch, null, row)
|
|
5194
|
+
: ftsBm25BatchValue(expression, batch, null, row);
|
|
5195
|
+
break;
|
|
5196
|
+
case "list":
|
|
5197
|
+
compiled = () => {
|
|
5198
|
+
throw new TypeError("Value lists are only supported with IN");
|
|
5199
|
+
};
|
|
5200
|
+
break;
|
|
5201
|
+
default: {
|
|
5202
|
+
if (expression.name === "COALESCE") {
|
|
5203
|
+
const parts = expression.arguments.map((argument) => compiledBatchExpression(plan, argument));
|
|
5204
|
+
compiled = (batch, row) => {
|
|
5205
|
+
for (const part of parts) {
|
|
5206
|
+
const candidate = part(batch, row);
|
|
5207
|
+
if (candidate !== null && candidate !== undefined)
|
|
5208
|
+
return candidate;
|
|
5209
|
+
}
|
|
5210
|
+
return null;
|
|
5211
|
+
};
|
|
5212
|
+
break;
|
|
5213
|
+
}
|
|
5214
|
+
const name = expression.name;
|
|
5215
|
+
if (!isScalarFunctionName(name)) {
|
|
5216
|
+
compiled = () => {
|
|
5217
|
+
throw new TypeError(`${name} requires grouped execution`);
|
|
5218
|
+
};
|
|
5219
|
+
break;
|
|
5220
|
+
}
|
|
5221
|
+
const parts = expression.arguments.map((argument) => compiledBatchExpression(plan, argument));
|
|
5222
|
+
// One argument array per call site, refilled per row: a function never re-enters its own
|
|
5223
|
+
// evaluation, so the slots are free again before the next row reads them.
|
|
5224
|
+
const values = new Array(parts.length);
|
|
5225
|
+
const evaluate = scalarFunctionEvaluator(name);
|
|
5226
|
+
compiled = (batch, row) => {
|
|
5227
|
+
let index = 0;
|
|
5228
|
+
for (const part of parts) {
|
|
5229
|
+
values[index] = part(batch, row);
|
|
5230
|
+
index += 1;
|
|
5231
|
+
}
|
|
5232
|
+
return evaluate(values);
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
compiledBatchExpressions.set(expression, compiled);
|
|
5237
|
+
return compiled;
|
|
5238
|
+
}
|
|
5239
|
+
/** A boolean node compiled over its children, keeping booleanTruth's three-valued rules. */
|
|
5240
|
+
function compiledTruth(plan, expression) {
|
|
5241
|
+
// booleanTruth reads operands through a callback; binding the current row into one closure
|
|
5242
|
+
// per node avoids allocating that callback per row. A node is never re-entered while its
|
|
5243
|
+
// own operands evaluate, so the shared slots are safe.
|
|
5244
|
+
const state = { batch: undefined, row: 0 };
|
|
5245
|
+
const evaluateValue = (nested) => compiledBatchExpression(plan, nested)(state.batch, state.row);
|
|
5246
|
+
return (batch, row) => {
|
|
5247
|
+
state.batch = batch;
|
|
5248
|
+
state.row = row;
|
|
5249
|
+
return booleanTruth(expression, evaluateValue);
|
|
5250
|
+
};
|
|
5251
|
+
}
|
|
4651
5252
|
function evaluateBatchExpression(plan, expression, batch, row) {
|
|
4652
5253
|
if (expression.kind === "literal")
|
|
4653
5254
|
return expression.value;
|
|
4654
5255
|
if (expression.kind === "wildcard")
|
|
4655
5256
|
return 1;
|
|
4656
|
-
if (expression.kind === "list")
|
|
4657
|
-
throw new TypeError("Value lists are only supported with IN");
|
|
4658
|
-
if (expression.kind === "condition" ||
|
|
4659
|
-
expression.kind === "logical" ||
|
|
4660
|
-
expression.kind === "not") {
|
|
4661
|
-
return booleanTruth(expression, (nested) => evaluateBatchExpression(plan, nested, batch, row));
|
|
4662
|
-
}
|
|
4663
|
-
if (expression.kind === "case") {
|
|
4664
|
-
for (const branch of expression.branches) {
|
|
4665
|
-
const matched = booleanTruth(branch.when, (nested) => evaluateBatchExpression(plan, nested, batch, row));
|
|
4666
|
-
if (matched === true)
|
|
4667
|
-
return evaluateBatchExpression(plan, branch.then, batch, row);
|
|
4668
|
-
}
|
|
4669
|
-
return expression.otherwise === undefined
|
|
4670
|
-
? null
|
|
4671
|
-
: evaluateBatchExpression(plan, expression.otherwise, batch, row);
|
|
4672
|
-
}
|
|
4673
5257
|
if (expression.kind === "column") {
|
|
4674
5258
|
return vectorValue(expression.vector, batch.rowsBySource[expression.source]?.[row] ?? -1);
|
|
4675
5259
|
}
|
|
4676
|
-
|
|
4677
|
-
return expression.op === "match"
|
|
4678
|
-
? ftsBatchTruth(expression, batch, null, row)
|
|
4679
|
-
: ftsBm25BatchValue(expression, batch, null, row);
|
|
4680
|
-
}
|
|
4681
|
-
if (expression.kind === "binary") {
|
|
4682
|
-
return binaryValue(expression.operator, evaluateBatchExpression(plan, expression.left, batch, row), evaluateBatchExpression(plan, expression.right, batch, row));
|
|
4683
|
-
}
|
|
4684
|
-
if (expression.name === "COALESCE") {
|
|
4685
|
-
for (const argument of expression.arguments) {
|
|
4686
|
-
const candidate = evaluateBatchExpression(plan, argument, batch, row);
|
|
4687
|
-
if (candidate !== null && candidate !== undefined)
|
|
4688
|
-
return candidate;
|
|
4689
|
-
}
|
|
4690
|
-
return null;
|
|
4691
|
-
}
|
|
4692
|
-
if (!isScalarFunctionName(expression.name))
|
|
4693
|
-
throw new TypeError(`${expression.name} requires grouped execution`);
|
|
4694
|
-
return scalarFunctionValue(expression.name, expression.arguments.map((argument) => evaluateBatchExpression(plan, argument, batch, row)));
|
|
5260
|
+
return compiledBatchExpression(plan, expression)(batch, row);
|
|
4695
5261
|
}
|
|
4696
5262
|
function evaluateExpression(expression, rowsBySource) {
|
|
4697
5263
|
if (expression.kind === "literal")
|
|
@@ -4741,12 +5307,20 @@ function evaluateExpression(expression, rowsBySource) {
|
|
|
4741
5307
|
function binaryValue(operator, left, right) {
|
|
4742
5308
|
if (left === null || left === undefined || right === null || right === undefined)
|
|
4743
5309
|
return null;
|
|
4744
|
-
if (
|
|
4745
|
-
if (
|
|
4746
|
-
|
|
4747
|
-
|
|
5310
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
5311
|
+
if (operator === "+")
|
|
5312
|
+
return left + right;
|
|
5313
|
+
if (operator === "-")
|
|
5314
|
+
return left - right;
|
|
5315
|
+
if (operator === "*")
|
|
5316
|
+
return left * right;
|
|
5317
|
+
if (operator === "/")
|
|
5318
|
+
return right === 0 ? null : left / right;
|
|
5319
|
+
if (operator === "%")
|
|
5320
|
+
return right === 0 ? null : left % right;
|
|
5321
|
+
}
|
|
5322
|
+
if (operator === "||")
|
|
4748
5323
|
return concatenatedSqlValue(left, right);
|
|
4749
|
-
}
|
|
4750
5324
|
const exact = exactNumericBinary(operator, left, right);
|
|
4751
5325
|
if (exact !== undefined)
|
|
4752
5326
|
return exact;
|
|
@@ -4806,6 +5380,26 @@ function comparisonValue(operator, leftValue, rightValue) {
|
|
|
4806
5380
|
rightValue === undefined) {
|
|
4807
5381
|
return false;
|
|
4808
5382
|
}
|
|
5383
|
+
// NaN keeps the general path: SQL treats it as one value equal to itself.
|
|
5384
|
+
if (typeof leftValue === "number" &&
|
|
5385
|
+
typeof rightValue === "number" &&
|
|
5386
|
+
leftValue === leftValue &&
|
|
5387
|
+
rightValue === rightValue) {
|
|
5388
|
+
if (operator === "=")
|
|
5389
|
+
return leftValue === rightValue;
|
|
5390
|
+
if (operator === "!=" || operator === "<>")
|
|
5391
|
+
return leftValue !== rightValue;
|
|
5392
|
+
if (operator === ">")
|
|
5393
|
+
return leftValue > rightValue;
|
|
5394
|
+
if (operator === ">=")
|
|
5395
|
+
return leftValue >= rightValue;
|
|
5396
|
+
if (operator === "<")
|
|
5397
|
+
return leftValue < rightValue;
|
|
5398
|
+
return leftValue <= rightValue;
|
|
5399
|
+
}
|
|
5400
|
+
// An untyped string beside a datetime, number, or boolean reads in that value's type, as
|
|
5401
|
+
// PostgreSQL types an unknown-typed literal by its context; the row executor does the same.
|
|
5402
|
+
[leftValue, rightValue] = coerceComparisonOperands(leftValue, rightValue);
|
|
4809
5403
|
const left = comparable(leftValue);
|
|
4810
5404
|
const right = comparable(rightValue);
|
|
4811
5405
|
if (operator === "=")
|