@minnowdb/core 0.2.1 → 0.3.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 +21 -32
- package/dist/engine/artifact-cache.d.ts +2 -0
- package/dist/engine/artifact-cache.d.ts.map +1 -1
- package/dist/engine/artifact-cache.js +5 -0
- package/dist/engine/artifact-cache.js.map +1 -1
- package/dist/engine/batch.d.ts +6 -1
- package/dist/engine/batch.d.ts.map +1 -1
- package/dist/engine/batch.js +17 -6
- package/dist/engine/batch.js.map +1 -1
- package/dist/engine/catalog.d.ts +22 -2
- package/dist/engine/catalog.d.ts.map +1 -1
- package/dist/engine/catalog.js +30 -3
- package/dist/engine/catalog.js.map +1 -1
- package/dist/engine/client.d.ts +2 -0
- package/dist/engine/client.d.ts.map +1 -1
- package/dist/engine/client.js +20 -10
- package/dist/engine/client.js.map +1 -1
- package/dist/engine/database.d.ts +67 -5
- package/dist/engine/database.d.ts.map +1 -1
- package/dist/engine/database.js +3508 -369
- package/dist/engine/database.js.map +1 -1
- package/dist/engine/defaults.d.ts +4 -7
- package/dist/engine/defaults.d.ts.map +1 -1
- package/dist/engine/defaults.js +47 -21
- package/dist/engine/defaults.js.map +1 -1
- package/dist/engine/fts.d.ts.map +1 -1
- package/dist/engine/fts.js +2 -0
- package/dist/engine/fts.js.map +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/live.d.ts.map +1 -1
- package/dist/engine/live.js +10 -2
- package/dist/engine/live.js.map +1 -1
- package/dist/engine/optimizer.d.ts.map +1 -1
- package/dist/engine/optimizer.js +258 -23
- package/dist/engine/optimizer.js.map +1 -1
- package/dist/engine/query-cache.d.ts +1 -1
- package/dist/engine/query-cache.d.ts.map +1 -1
- package/dist/engine/query-cache.js +3 -1
- package/dist/engine/query-cache.js.map +1 -1
- package/dist/engine/query.d.ts +130 -22
- package/dist/engine/query.d.ts.map +1 -1
- package/dist/engine/query.js +1344 -241
- package/dist/engine/query.js.map +1 -1
- package/dist/engine/schema-wire.d.ts +6 -2
- package/dist/engine/schema-wire.d.ts.map +1 -1
- package/dist/engine/schema-wire.js +40 -33
- package/dist/engine/schema-wire.js.map +1 -1
- package/dist/engine/schema.d.ts +157 -76
- package/dist/engine/schema.d.ts.map +1 -1
- package/dist/engine/schema.js +548 -103
- package/dist/engine/schema.js.map +1 -1
- package/dist/engine/sort-keys.d.ts +4 -3
- package/dist/engine/sort-keys.d.ts.map +1 -1
- package/dist/engine/sort-keys.js +42 -27
- package/dist/engine/sort-keys.js.map +1 -1
- package/dist/engine/sql-domains.d.ts +26 -0
- package/dist/engine/sql-domains.d.ts.map +1 -0
- package/dist/engine/sql-domains.js +421 -0
- package/dist/engine/sql-domains.js.map +1 -0
- package/dist/engine/sql-driver.d.ts +19 -0
- package/dist/engine/sql-driver.d.ts.map +1 -0
- package/dist/engine/sql-driver.js +2 -0
- package/dist/engine/sql-driver.js.map +1 -0
- package/dist/engine/sql-json.d.ts +7 -8
- package/dist/engine/sql-json.d.ts.map +1 -1
- package/dist/engine/sql-json.js +28 -14
- package/dist/engine/sql-json.js.map +1 -1
- package/dist/engine/sql-semantics.d.ts +2 -0
- package/dist/engine/sql-semantics.d.ts.map +1 -1
- package/dist/engine/sql-semantics.js +69 -3
- package/dist/engine/sql-semantics.js.map +1 -1
- package/dist/engine/vector.d.ts +5 -1
- package/dist/engine/vector.d.ts.map +1 -1
- package/dist/engine/vector.js +433 -61
- package/dist/engine/vector.js.map +1 -1
- package/dist/engine/worker-host.d.ts +1 -0
- package/dist/engine/worker-host.d.ts.map +1 -1
- package/dist/engine/worker-host.js +4 -0
- package/dist/engine/worker-host.js.map +1 -1
- package/dist/plan/index.d.ts +3 -3
- package/dist/plan/index.js +3 -3
- package/dist/storage/indexeddb.d.ts +44 -5
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/indexeddb.js +738 -175
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/storage/memory.d.ts +27 -10
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +67 -18
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/opfs/leader.d.ts +28 -6
- package/dist/storage/opfs/leader.d.ts.map +1 -1
- package/dist/storage/opfs/leader.js +301 -34
- package/dist/storage/opfs/leader.js.map +1 -1
- package/dist/storage/opfs/rpc.d.ts.map +1 -1
- package/dist/storage/opfs/rpc.js +2 -1
- package/dist/storage/opfs/rpc.js.map +1 -1
- package/dist/storage/opfs/store.d.ts +34 -5
- package/dist/storage/opfs/store.d.ts.map +1 -1
- package/dist/storage/opfs/store.js +41 -0
- package/dist/storage/opfs/store.js.map +1 -1
- package/dist/storage/snapshot.d.ts +14 -0
- package/dist/storage/snapshot.d.ts.map +1 -1
- package/dist/storage/snapshot.js +44 -0
- package/dist/storage/snapshot.js.map +1 -1
- package/dist/storage/toolkit/index.d.ts +1 -1
- package/dist/storage/toolkit/index.d.ts.map +1 -1
- package/dist/storage/toolkit/index.js +1 -1
- package/dist/storage/toolkit/index.js.map +1 -1
- package/dist/storage/toolkit/record-core.d.ts +19 -5
- package/dist/storage/toolkit/record-core.d.ts.map +1 -1
- package/dist/storage/toolkit/record-core.js +353 -78
- package/dist/storage/toolkit/record-core.js.map +1 -1
- package/dist/storage/toolkit/wire.d.ts +15 -0
- package/dist/storage/toolkit/wire.d.ts.map +1 -1
- package/dist/storage/toolkit/wire.js +134 -0
- package/dist/storage/toolkit/wire.js.map +1 -1
- package/dist/storage/types.d.ts +213 -31
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +291 -20
- package/dist/storage/types.js.map +1 -1
- package/dist/testing/block-store-conformance.d.ts.map +1 -1
- package/dist/testing/block-store-conformance.js +153 -2
- package/dist/testing/block-store-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +23 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/simulator.d.ts +98 -0
- package/dist/testing/simulator.d.ts.map +1 -0
- package/dist/testing/simulator.js +560 -0
- package/dist/testing/simulator.js.map +1 -0
- package/dist/testing/sqllogictest.d.ts +90 -0
- package/dist/testing/sqllogictest.d.ts.map +1 -0
- package/dist/testing/sqllogictest.js +435 -0
- package/dist/testing/sqllogictest.js.map +1 -0
- package/dist/transactions/index.d.ts +18 -3
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +64 -8
- package/dist/transactions/index.js.map +1 -1
- package/package.json +5 -3
- package/postgres-feature-profile.json +223 -0
- package/sql-feature-matrix.json +172 -252
package/dist/engine/vector.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { cachedListMembership, distinctFromComparison,
|
|
1
|
+
import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, } from "./query.js";
|
|
2
|
+
import { jsonValueOf } from "./sql-json.js";
|
|
2
3
|
import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize, } from "./fts.js";
|
|
3
4
|
import { ByteGroupIndex } from "./group-index.js";
|
|
4
5
|
import { ByteJoinIndex } from "./join-index.js";
|
|
5
6
|
import { QueryMemoryBudgetError, QueryMemoryContext, } from "./memory.js";
|
|
6
|
-
import { compareSqlStrings, defineSqlResultProperty } from "./sql-semantics.js";
|
|
7
|
+
import { compareSqlStrings, compileSimilarPattern, defineSqlResultProperty, } from "./sql-semantics.js";
|
|
8
|
+
import { exactNumericBinary, exactNumericCompare, collatedDomainCompare, enumDomainCompare, externalSqlDomainValue, isExactNumeric, protectedSqlTextValue, } from "./sql-domains.js";
|
|
7
9
|
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
8
10
|
const DEFAULT_BATCH_ROWS = 2_048;
|
|
9
11
|
/** Above this, locating each IN member separately costs more than scanning between them. */
|
|
@@ -59,7 +61,7 @@ export function createColumnarTable(name, columns, uniqueKey) {
|
|
|
59
61
|
...(uniqueKey === undefined ? {} : { uniqueKey }),
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
|
-
export function columnarTableFromRows(name, rows, projectedColumnNames) {
|
|
64
|
+
export function columnarTableFromRows(name, rows, projectedColumnNames, protectText = true) {
|
|
63
65
|
const columnNameSet = new Set(projectedColumnNames);
|
|
64
66
|
if (projectedColumnNames === undefined) {
|
|
65
67
|
for (const row of rows) {
|
|
@@ -72,7 +74,10 @@ export function columnarTableFromRows(name, rows, projectedColumnNames) {
|
|
|
72
74
|
return { name, rowCount: rows.length, columns: new Map() };
|
|
73
75
|
const columns = new Map();
|
|
74
76
|
for (const columnName of columnNames) {
|
|
75
|
-
const values = rows.map((row) =>
|
|
77
|
+
const values = rows.map((row) => {
|
|
78
|
+
const value = row[columnName] ?? null;
|
|
79
|
+
return protectText && typeof value === "string" ? protectedSqlTextValue(value) : value;
|
|
80
|
+
});
|
|
76
81
|
columns.set(columnName, { type: inferVectorType(values), values });
|
|
77
82
|
}
|
|
78
83
|
return createColumnarTable(name, columns);
|
|
@@ -105,7 +110,7 @@ export function prepareVectorQuery(plan, inputTables, options = {}) {
|
|
|
105
110
|
async executeAsync(executionOptions = {}) {
|
|
106
111
|
if (closed)
|
|
107
112
|
throw new Error("Prepared vector query is closed");
|
|
108
|
-
const canSpillSort = bound.orderBy.length > 0 && !bound.grouped;
|
|
113
|
+
const canSpillSort = bound.orderBy.length > 0 && !bound.grouped && !bound.sourceOrdered;
|
|
109
114
|
// An unordered grouped plan spills too: the empty ordering makes the pairwise merge a
|
|
110
115
|
// stable concatenation, and partition-wise accumulation bounds peak group state.
|
|
111
116
|
const canSpillHash = bound.grouped && bound.groupBy.length > 0;
|
|
@@ -255,7 +260,7 @@ export function vectorValue(vector, rowIndex) {
|
|
|
255
260
|
return code === NULL_STRING_CODE ? null : (vector.dictionary[code] ?? null);
|
|
256
261
|
}
|
|
257
262
|
function columnarTablePayloadBytes(table) {
|
|
258
|
-
let total = 0;
|
|
263
|
+
let total = table.scanOrder?.byteLength ?? 0;
|
|
259
264
|
for (const vector of table.columns.values()) {
|
|
260
265
|
total = safeMemorySum(total, vector.validity.byteLength, "Column vector payload");
|
|
261
266
|
if (vector.kind === "string") {
|
|
@@ -315,7 +320,178 @@ function disjunctiveNormalForm(predicate) {
|
|
|
315
320
|
};
|
|
316
321
|
return visit(predicate.left) && branches.length > 1 ? branches : undefined;
|
|
317
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Gives a pure comma/CROSS-join group a connected physical order once schemas can resolve bare
|
|
325
|
+
* column names. SQLLogicTest deliberately permutes FROM lists; preserving that written order can
|
|
326
|
+
* otherwise materialize several disconnected Cartesian products before their WHERE equalities
|
|
327
|
+
* become applicable. The streamed base stays fixed; explicit/outer joins and wildcard projection
|
|
328
|
+
* keep their complete written order.
|
|
329
|
+
*
|
|
330
|
+
* One equality that connects each new source becomes the inner hash-join key. The remaining
|
|
331
|
+
* conditions stay ordinary predicates, so null and residual-filter semantics do not change.
|
|
332
|
+
*/
|
|
333
|
+
function orderCartesianJoins(plan, tables) {
|
|
334
|
+
if (plan.joins.length < 2 ||
|
|
335
|
+
plan.select.some((item) => item.expression.kind === "wildcard") ||
|
|
336
|
+
plan.joins.some((join) => !isCartesianJoin(join))) {
|
|
337
|
+
return plan;
|
|
338
|
+
}
|
|
339
|
+
const entries = [
|
|
340
|
+
{ source: plan.base, originalIndex: 0 },
|
|
341
|
+
...plan.joins.map((join, index) => ({
|
|
342
|
+
source: tableSourceOf(join),
|
|
343
|
+
originalIndex: index + 1,
|
|
344
|
+
})),
|
|
345
|
+
];
|
|
346
|
+
const sourceTables = entries.map(({ source }) => tables.get(source.table));
|
|
347
|
+
if (sourceTables.some((table) => table === undefined))
|
|
348
|
+
return plan;
|
|
349
|
+
const predicateSources = plan.predicates.map((predicate) => ({
|
|
350
|
+
predicate,
|
|
351
|
+
sources: rawPredicateSources(predicate, entries, sourceTables),
|
|
352
|
+
}));
|
|
353
|
+
// The preparation layer may stream the written base table through a resident window. Keep
|
|
354
|
+
// that source fixed and order only the fully materialized join sides around it.
|
|
355
|
+
const remaining = new Set(entries.slice(1).map(({ originalIndex }) => originalIndex));
|
|
356
|
+
const ordered = [0];
|
|
357
|
+
while (remaining.size > 0) {
|
|
358
|
+
let best;
|
|
359
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
360
|
+
for (const candidate of remaining) {
|
|
361
|
+
const score = cartesianSourceScore(candidate, ordered, predicateSources);
|
|
362
|
+
if (score > bestScore || (score === bestScore && (best === undefined || candidate < best))) {
|
|
363
|
+
best = candidate;
|
|
364
|
+
bestScore = score;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (best === undefined)
|
|
368
|
+
return plan;
|
|
369
|
+
ordered.push(best);
|
|
370
|
+
remaining.delete(best);
|
|
371
|
+
}
|
|
372
|
+
const predicates = [...plan.predicates];
|
|
373
|
+
const available = new Set([ordered[0] ?? 0]);
|
|
374
|
+
const joins = [];
|
|
375
|
+
for (const sourceIndex of ordered.slice(1)) {
|
|
376
|
+
const source = entries[sourceIndex]?.source;
|
|
377
|
+
if (source === undefined)
|
|
378
|
+
return plan;
|
|
379
|
+
const keyIndex = predicates.findIndex((predicate) => predicateJoinsSource(predicate, sourceIndex, available, entries, sourceTables));
|
|
380
|
+
const key = keyIndex < 0 ? undefined : predicates.splice(keyIndex, 1)[0];
|
|
381
|
+
joins.push(key === undefined ? cartesianJoin(source) : keyedInnerJoin(source, key));
|
|
382
|
+
available.add(sourceIndex);
|
|
383
|
+
}
|
|
384
|
+
return { ...plan, base: plan.base, joins, predicates };
|
|
385
|
+
}
|
|
386
|
+
function isCartesianJoin(join) {
|
|
387
|
+
const condition = join.on;
|
|
388
|
+
return (join.kind === "inner" &&
|
|
389
|
+
condition?.kind === "condition" &&
|
|
390
|
+
condition.operator === "=" &&
|
|
391
|
+
condition.left.kind === "literal" &&
|
|
392
|
+
condition.left.value === 1 &&
|
|
393
|
+
condition.right.kind === "literal" &&
|
|
394
|
+
condition.right.value === 1);
|
|
395
|
+
}
|
|
396
|
+
function tableSourceOf(join) {
|
|
397
|
+
const { kind, left, right, on, full, natural, ...source } = join;
|
|
398
|
+
void kind;
|
|
399
|
+
void left;
|
|
400
|
+
void right;
|
|
401
|
+
void on;
|
|
402
|
+
void full;
|
|
403
|
+
void natural;
|
|
404
|
+
return source;
|
|
405
|
+
}
|
|
406
|
+
function cartesianJoin(source) {
|
|
407
|
+
return {
|
|
408
|
+
...source,
|
|
409
|
+
kind: "inner",
|
|
410
|
+
left: { kind: "literal", value: null },
|
|
411
|
+
right: { kind: "literal", value: null },
|
|
412
|
+
on: {
|
|
413
|
+
kind: "condition",
|
|
414
|
+
operator: "=",
|
|
415
|
+
left: { kind: "literal", value: 1 },
|
|
416
|
+
right: { kind: "literal", value: 1 },
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function keyedInnerJoin(source, predicate) {
|
|
421
|
+
return {
|
|
422
|
+
...source,
|
|
423
|
+
kind: "inner",
|
|
424
|
+
left: predicate.left,
|
|
425
|
+
right: predicate.right,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function cartesianSourceScore(candidate, ordered, predicates) {
|
|
429
|
+
const available = new Set(ordered);
|
|
430
|
+
let local = 0;
|
|
431
|
+
let connected = 0;
|
|
432
|
+
let equality = 0;
|
|
433
|
+
for (const entry of predicates) {
|
|
434
|
+
const sources = entry.sources;
|
|
435
|
+
if (sources?.has(candidate) !== true)
|
|
436
|
+
continue;
|
|
437
|
+
if (sources.size === 1) {
|
|
438
|
+
local++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if ([...sources].every((source) => source === candidate || available.has(source))) {
|
|
442
|
+
connected++;
|
|
443
|
+
if (entry.predicate.operator === "=")
|
|
444
|
+
equality++;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// A usable equality dominates a general connection, which dominates a local filter; original
|
|
448
|
+
// order breaks ties. The empty-order branch keeps the helper correct if base selection is ever
|
|
449
|
+
// made schema-aware too, though the streaming executor currently fixes that source.
|
|
450
|
+
return ordered.length === 0 ? local : equality * 100 + connected * 10 + local;
|
|
451
|
+
}
|
|
452
|
+
function rawPredicateSources(predicate, entries, tables) {
|
|
453
|
+
const sources = new Set();
|
|
454
|
+
return rawExpressionSources(predicate.left, entries, tables, sources) &&
|
|
455
|
+
rawExpressionSources(predicate.right, entries, tables, sources)
|
|
456
|
+
? sources
|
|
457
|
+
: undefined;
|
|
458
|
+
}
|
|
459
|
+
function rawExpressionSources(expression, entries, tables, into) {
|
|
460
|
+
if (expression.kind === "subquery" || expression.kind === "exists")
|
|
461
|
+
return false;
|
|
462
|
+
if (expression.kind === "column") {
|
|
463
|
+
const parts = expression.reference.split(".");
|
|
464
|
+
let matches;
|
|
465
|
+
if (parts.length === 2) {
|
|
466
|
+
const match = entries.findIndex(({ source }) => source.alias === parts[0]);
|
|
467
|
+
matches = match < 0 || tables[match]?.columns.has(parts[1] ?? "") !== true ? [] : [match];
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
const column = parts[0] ?? "";
|
|
471
|
+
matches = tables.flatMap((table, index) => table?.columns.has(column) === true ? [index] : []);
|
|
472
|
+
}
|
|
473
|
+
if (matches.length !== 1)
|
|
474
|
+
return false;
|
|
475
|
+
into.add(matches[0] ?? -1);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
return childExpressions(expression).every((child) => rawExpressionSources(child, entries, tables, into));
|
|
479
|
+
}
|
|
480
|
+
function predicateJoinsSource(predicate, candidate, available, entries, tables) {
|
|
481
|
+
if (predicate.operator !== "=")
|
|
482
|
+
return false;
|
|
483
|
+
const left = new Set();
|
|
484
|
+
const right = new Set();
|
|
485
|
+
if (!rawExpressionSources(predicate.left, entries, tables, left) ||
|
|
486
|
+
!rawExpressionSources(predicate.right, entries, tables, right)) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const candidateOnly = (sources) => sources.size === 1 && sources.has(candidate);
|
|
490
|
+
const availableOnly = (sources) => sources.size > 0 && [...sources].every((source) => available.has(source));
|
|
491
|
+
return ((candidateOnly(left) && availableOnly(right)) || (candidateOnly(right) && availableOnly(left)));
|
|
492
|
+
}
|
|
318
493
|
function bindPlan(plan, tables, memory, ftsStats) {
|
|
494
|
+
plan = orderCartesianJoins(plan, tables);
|
|
319
495
|
const sources = [plan.base, ...plan.joins];
|
|
320
496
|
const sourceTables = sources.map((source) => {
|
|
321
497
|
const table = tables.get(source.table);
|
|
@@ -422,6 +598,7 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
422
598
|
...(nulls === undefined ? {} : { nulls }),
|
|
423
599
|
}));
|
|
424
600
|
const grouped = groupBy.length > 0 || aggregateSpecs.length > 0;
|
|
601
|
+
const sourceOrdered = sourceTables[0]?.orderedForPlan === true && standardJoins.length === 0 && !grouped;
|
|
425
602
|
// A single bare string-column GROUP BY can group on dictionary codes: identical values share a
|
|
426
603
|
// code within one vector, so a code-indexed slot table replaces per-row hashing. Streamed
|
|
427
604
|
// vectors swap dictionaries per window; the accumulator remaps its slot table by value on
|
|
@@ -440,9 +617,11 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
440
617
|
groupBy,
|
|
441
618
|
groupIndexBySignature,
|
|
442
619
|
aggregates: aggregateSpecs,
|
|
620
|
+
hasListAggregate: aggregateSpecs.some((aggregate) => aggregate.name === "JSON_ARRAYAGG" || aggregate.name === "STRING_AGG"),
|
|
443
621
|
select,
|
|
444
622
|
orderBy,
|
|
445
623
|
grouped,
|
|
624
|
+
sourceOrdered,
|
|
446
625
|
...(codeGrouping === undefined ? {} : { codeGrouping }),
|
|
447
626
|
wildcard: plan.select[0]?.expression.kind === "wildcard",
|
|
448
627
|
...(plan.limit === undefined ? {} : { limit: plan.limit }),
|
|
@@ -513,7 +692,11 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
|
|
|
513
692
|
};
|
|
514
693
|
}
|
|
515
694
|
if (expression.kind === "literal" || expression.kind === "wildcard") {
|
|
516
|
-
return
|
|
695
|
+
return expression.kind === "literal" &&
|
|
696
|
+
typeof expression.value === "string" &&
|
|
697
|
+
expression.internalSqlValue !== true
|
|
698
|
+
? { ...expression, value: protectedSqlTextValue(expression.value), signature }
|
|
699
|
+
: { ...expression, signature };
|
|
517
700
|
}
|
|
518
701
|
if (expression.kind === "column") {
|
|
519
702
|
const parts = expression.reference.split(".");
|
|
@@ -599,12 +782,26 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
|
|
|
599
782
|
argument.vector.kind === "datetime"
|
|
600
783
|
? { source: argument.source, vector: argument.vector }
|
|
601
784
|
: undefined;
|
|
602
|
-
const rawNumber =
|
|
785
|
+
const rawNumber = expression.name !== "JSON_ARRAYAGG" &&
|
|
786
|
+
expression.name !== "STRING_AGG" &&
|
|
787
|
+
argument.kind === "column" &&
|
|
788
|
+
argument.vector.kind === "number"
|
|
603
789
|
? { source: argument.source, vector: argument.vector }
|
|
604
790
|
: undefined;
|
|
605
791
|
aggregateSpecs.push({
|
|
606
792
|
name: expression.name,
|
|
607
793
|
argument,
|
|
794
|
+
...(expression.name === "STRING_AGG"
|
|
795
|
+
? { delimiter: required(arguments_[1], "STRING_AGG delimiter is missing") }
|
|
796
|
+
: {}),
|
|
797
|
+
...(expression.aggregateOrderBy === undefined
|
|
798
|
+
? {}
|
|
799
|
+
: {
|
|
800
|
+
orderBy: expression.aggregateOrderBy.map((order) => ({
|
|
801
|
+
...order,
|
|
802
|
+
expression: bindExpression(order.expression, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
|
|
803
|
+
})),
|
|
804
|
+
}),
|
|
608
805
|
// The signature this slot was keyed by is the JSON of the compiled call, which carries the
|
|
609
806
|
// flag — so COUNT(x) and COUNT(DISTINCT x) in one select land in separate slots.
|
|
610
807
|
...(expression.distinct === true ? { distinct: true } : {}),
|
|
@@ -674,7 +871,8 @@ function ensureFtsDictionaryCache(expression, columnIndex, vector, scoring) {
|
|
|
674
871
|
const tokenCount = withScores ? new Uint32Array(vector.dictionary.length) : undefined;
|
|
675
872
|
const termTf = withScores ? new Uint32Array(vector.dictionary.length * termCount) : undefined;
|
|
676
873
|
for (let code = 0; code < vector.dictionary.length; code += 1) {
|
|
677
|
-
const
|
|
874
|
+
const rendered = externalSqlDomainValue(vector.dictionary[code] ?? "");
|
|
875
|
+
const tokens = tokenize(typeof rendered === "string" ? rendered : "");
|
|
678
876
|
masks[code] = termsMask(tokens, expression.terms);
|
|
679
877
|
if (tokenCount !== undefined)
|
|
680
878
|
tokenCount[code] = tokens.length;
|
|
@@ -922,8 +1120,10 @@ function runScanBatch(plan, start, length, groups, output, memory) {
|
|
|
922
1120
|
const scan = sourceRows[plan.scanSource];
|
|
923
1121
|
if (scan === undefined)
|
|
924
1122
|
return false;
|
|
925
|
-
|
|
926
|
-
|
|
1123
|
+
const order = plan.sourceTables[plan.scanSource]?.scanOrder;
|
|
1124
|
+
for (let index = 0; index < length; index += 1) {
|
|
1125
|
+
scan[index] = order?.[start + index] ?? start + index;
|
|
1126
|
+
}
|
|
927
1127
|
const batch = { length, rowsBySource: sourceRows, memory: batchMemory };
|
|
928
1128
|
return consumeJoinedBatches(plan, batch, 0, groups, output, memory);
|
|
929
1129
|
}
|
|
@@ -938,7 +1138,7 @@ function executeBoundPlan(plan, memory) {
|
|
|
938
1138
|
const groups = new GroupAccumulator(plan, memory);
|
|
939
1139
|
const output = new ResultSink(plan, memory, true);
|
|
940
1140
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
941
|
-
const narrowed = ascendingScanRange(plan, 0, scanRows);
|
|
1141
|
+
const narrowed = plan.sourceOrdered ? undefined : ascendingScanRange(plan, 0, scanRows);
|
|
942
1142
|
const ranges = narrowed?.ranges ?? [
|
|
943
1143
|
{ begin: narrowed?.begin ?? 0, end: narrowed?.end ?? scanRows },
|
|
944
1144
|
];
|
|
@@ -972,7 +1172,7 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
972
1172
|
// window cannot answer instead of stepping through them. `windowEnd` is where the loader
|
|
973
1173
|
// stopped, which is the block boundary — exactly the span the ordering check covers.
|
|
974
1174
|
const windowEnd = typeof residentEnd === "number" && residentEnd > start ? residentEnd : start + length;
|
|
975
|
-
const narrowed = ascendingScanRange(plan, start, windowEnd);
|
|
1175
|
+
const narrowed = plan.sourceOrdered ? undefined : ascendingScanRange(plan, start, windowEnd);
|
|
976
1176
|
if (narrowed === undefined) {
|
|
977
1177
|
if (runScanBatch(plan, start, length, groups, output, memory))
|
|
978
1178
|
break;
|
|
@@ -1161,8 +1361,18 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1161
1361
|
const raw = spec.argument.kind === "wildcard"
|
|
1162
1362
|
? 1
|
|
1163
1363
|
: evaluateBatchExpression(plan, spec.argument, batch, row);
|
|
1164
|
-
|
|
1165
|
-
|
|
1364
|
+
if (spec.name === "STRING_AGG" && raw !== null && raw !== undefined) {
|
|
1365
|
+
const delimiter = required(spec.delimiter, "STRING_AGG delimiter is missing");
|
|
1366
|
+
spillRow[`a${String(index)}`] = JSON.stringify([
|
|
1367
|
+
raw,
|
|
1368
|
+
evaluateBatchExpression(plan, delimiter, batch, row),
|
|
1369
|
+
(spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
|
|
1370
|
+
]);
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
spillRow[`a${String(index)}`] =
|
|
1374
|
+
raw === null || raw === undefined ? null : asQueryValue(raw);
|
|
1375
|
+
}
|
|
1166
1376
|
}
|
|
1167
1377
|
batchMemory.tally(queryRowPayloadBytes(spillRow), "Hash spill value row");
|
|
1168
1378
|
const partition = hashQueryValues(groupValues) & (partitionCount - 1);
|
|
@@ -1446,7 +1656,7 @@ function createSpillRunReader(store, ownerId, run, columns, memory) {
|
|
|
1446
1656
|
}
|
|
1447
1657
|
function compareOrderedRows(left, right, orderBy) {
|
|
1448
1658
|
for (const order of orderBy) {
|
|
1449
|
-
const placed =
|
|
1659
|
+
const placed = nullOrder(left[order.outputName], right[order.outputName], order.nulls, order.direction);
|
|
1450
1660
|
if (placed !== undefined && placed !== 0)
|
|
1451
1661
|
return placed;
|
|
1452
1662
|
const comparison = compareValues(left[order.outputName], right[order.outputName]);
|
|
@@ -1533,7 +1743,7 @@ function executeMetadataCount(plan, memory) {
|
|
|
1533
1743
|
}
|
|
1534
1744
|
function finishResult(plan, inputRows, memory) {
|
|
1535
1745
|
const rows = inputRows;
|
|
1536
|
-
if (plan.orderBy.length > 0) {
|
|
1746
|
+
if (plan.orderBy.length > 0 && !plan.sourceOrdered) {
|
|
1537
1747
|
const ordering = memory.reserve(safeMemoryProduct(rows.length, Uint32Array.BYTES_PER_ELEMENT * 2 +
|
|
1538
1748
|
Uint8Array.BYTES_PER_ELEMENT +
|
|
1539
1749
|
// Per extracted sort key: one slot, plus the null mask a numeric column adds beside
|
|
@@ -2110,7 +2320,7 @@ function consumeJoinedBatches(plan, batch, joinIndex, groups, output, memory, pr
|
|
|
2110
2320
|
let working = batch;
|
|
2111
2321
|
let complete = prefiltered;
|
|
2112
2322
|
let owned;
|
|
2113
|
-
if (
|
|
2323
|
+
if (!prefiltered) {
|
|
2114
2324
|
const filtered = prefilterBatch(plan, batch, join);
|
|
2115
2325
|
if (filtered !== undefined) {
|
|
2116
2326
|
complete = filtered.complete;
|
|
@@ -2144,7 +2354,7 @@ function consumeJoinedBatches(plan, batch, joinIndex, groups, output, memory, pr
|
|
|
2144
2354
|
function reachedEarlyLimit(plan, outputRows) {
|
|
2145
2355
|
// Early termination must still produce the rows the trailing OFFSET will discard.
|
|
2146
2356
|
return (!plan.grouped &&
|
|
2147
|
-
plan.orderBy.length === 0 &&
|
|
2357
|
+
(plan.orderBy.length === 0 || plan.sourceOrdered) &&
|
|
2148
2358
|
plan.limit !== undefined &&
|
|
2149
2359
|
outputRows >= plan.limit + (plan.offset ?? 0));
|
|
2150
2360
|
}
|
|
@@ -2186,11 +2396,18 @@ function* joinBatches(plan, input, join, memory) {
|
|
|
2186
2396
|
const probeKey = evaluateBatchExpression(plan, join.probe, input, row);
|
|
2187
2397
|
let buildRow = probeKey === null ? -1 : join.lookup.firstRow(probeKey);
|
|
2188
2398
|
if (buildRow < 0) {
|
|
2189
|
-
if (join.kind === "left") {
|
|
2399
|
+
if (join.kind === "left" || join.kind === "anti") {
|
|
2190
2400
|
appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, -1);
|
|
2191
2401
|
outputLength += 1;
|
|
2192
2402
|
}
|
|
2193
2403
|
}
|
|
2404
|
+
else if (join.kind === "semi") {
|
|
2405
|
+
appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
|
|
2406
|
+
outputLength += 1;
|
|
2407
|
+
}
|
|
2408
|
+
else if (join.kind === "anti") {
|
|
2409
|
+
// A match excludes the probe row.
|
|
2410
|
+
}
|
|
2194
2411
|
else {
|
|
2195
2412
|
while (buildRow >= 0) {
|
|
2196
2413
|
appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
|
|
@@ -2254,12 +2471,16 @@ function* loopJoinBatches(plan, input, join, loop, memory) {
|
|
|
2254
2471
|
if (!holds)
|
|
2255
2472
|
continue;
|
|
2256
2473
|
matched = true;
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2474
|
+
if (join.kind !== "anti") {
|
|
2475
|
+
appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
|
|
2476
|
+
outputLength += 1;
|
|
2477
|
+
if (outputLength === DEFAULT_BATCH_ROWS)
|
|
2478
|
+
yield* emit();
|
|
2479
|
+
}
|
|
2480
|
+
if (join.kind === "semi" || join.kind === "anti")
|
|
2481
|
+
break;
|
|
2261
2482
|
}
|
|
2262
|
-
if (!matched && join.kind === "left") {
|
|
2483
|
+
if (!matched && (join.kind === "left" || join.kind === "anti")) {
|
|
2263
2484
|
appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, -1);
|
|
2264
2485
|
outputLength += 1;
|
|
2265
2486
|
if (outputLength === DEFAULT_BATCH_ROWS)
|
|
@@ -2309,10 +2530,12 @@ function joinUniqueBatch(plan, input, join, memory) {
|
|
|
2309
2530
|
cache[code] = buildRow;
|
|
2310
2531
|
}
|
|
2311
2532
|
}
|
|
2312
|
-
if (buildRow < 0 && join.kind === "inner")
|
|
2533
|
+
if ((buildRow < 0 && (join.kind === "inner" || join.kind === "semi")) ||
|
|
2534
|
+
(buildRow >= 0 && join.kind === "anti")) {
|
|
2313
2535
|
continue;
|
|
2536
|
+
}
|
|
2314
2537
|
selectedRows[outputLength] = row;
|
|
2315
|
-
buildRows[outputLength] = buildRow;
|
|
2538
|
+
buildRows[outputLength] = join.kind === "anti" ? -1 : buildRow;
|
|
2316
2539
|
outputLength += 1;
|
|
2317
2540
|
}
|
|
2318
2541
|
}
|
|
@@ -2320,10 +2543,12 @@ function joinUniqueBatch(plan, input, join, memory) {
|
|
|
2320
2543
|
for (let row = 0; row < input.length; row += 1) {
|
|
2321
2544
|
const probeKey = evaluateBatchExpression(plan, join.probe, input, row);
|
|
2322
2545
|
const buildRow = probeKey === null ? -1 : join.lookup.firstRow(probeKey);
|
|
2323
|
-
if (buildRow < 0 && join.kind === "inner")
|
|
2546
|
+
if ((buildRow < 0 && (join.kind === "inner" || join.kind === "semi")) ||
|
|
2547
|
+
(buildRow >= 0 && join.kind === "anti")) {
|
|
2324
2548
|
continue;
|
|
2549
|
+
}
|
|
2325
2550
|
selectedRows[outputLength] = row;
|
|
2326
|
-
buildRows[outputLength] = buildRow;
|
|
2551
|
+
buildRows[outputLength] = join.kind === "anti" ? -1 : buildRow;
|
|
2327
2552
|
outputLength += 1;
|
|
2328
2553
|
}
|
|
2329
2554
|
}
|
|
@@ -2415,6 +2640,7 @@ class ResultSink {
|
|
|
2415
2640
|
const capacity = (plan.limit ?? 0) + (plan.offset ?? 0);
|
|
2416
2641
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
2417
2642
|
const bounded = !plan.grouped &&
|
|
2643
|
+
!plan.sourceOrdered &&
|
|
2418
2644
|
plan.orderBy.length > 0 &&
|
|
2419
2645
|
plan.limit !== undefined &&
|
|
2420
2646
|
(plan.joins.length > 0 || capacity < scanRows * BOUNDED_SINK_MAX_SHARE);
|
|
@@ -2692,7 +2918,7 @@ class ResultSink {
|
|
|
2692
2918
|
const orderBy = this.#plan.orderBy;
|
|
2693
2919
|
for (let index = 0; index < orderBy.length; index += 1) {
|
|
2694
2920
|
const order = required(orderBy[index], "Order term is missing");
|
|
2695
|
-
const placed =
|
|
2921
|
+
const placed = nullOrder(left[index], right[index], order.nulls, order.direction);
|
|
2696
2922
|
if (placed !== undefined && placed !== 0)
|
|
2697
2923
|
return placed;
|
|
2698
2924
|
const comparison = compareValues(left[index], right[index]);
|
|
@@ -2820,7 +3046,7 @@ class GroupAccumulator {
|
|
|
2820
3046
|
* COUNT/SUM/AVG aggregates: one pass with unboxed reads and no per-row dispatch. Returns false
|
|
2821
3047
|
* when the plan shape needs the generic per-row path.
|
|
2822
3048
|
*/
|
|
2823
|
-
consumeFast(batch
|
|
3049
|
+
consumeFast(batch) {
|
|
2824
3050
|
const plan = this.#plan;
|
|
2825
3051
|
const codeGrouping = plan.codeGrouping;
|
|
2826
3052
|
const globalGroup = plan.groupBy.length === 0;
|
|
@@ -2831,7 +3057,7 @@ class GroupAccumulator {
|
|
|
2831
3057
|
if (specs === undefined)
|
|
2832
3058
|
return false;
|
|
2833
3059
|
// The purest shape — global COUNT(*) with no predicates — needs no row loop at all.
|
|
2834
|
-
if (globalGroup &&
|
|
3060
|
+
if (globalGroup && specs.every((spec) => spec.kind === "star")) {
|
|
2835
3061
|
const state = required(this.#index.getEmpty(), "Grouped query state is missing");
|
|
2836
3062
|
for (let index = 0; index < specs.length; index += 1) {
|
|
2837
3063
|
state.counts[index] = (state.counts[index] ?? 0) + batch.length;
|
|
@@ -2881,8 +3107,6 @@ class GroupAccumulator {
|
|
|
2881
3107
|
slots: groupVector.codes.length,
|
|
2882
3108
|
};
|
|
2883
3109
|
for (let row = 0; row < batch.length; row += 1) {
|
|
2884
|
-
if (hasPredicates && !passes(row))
|
|
2885
|
-
continue;
|
|
2886
3110
|
let state = globalState;
|
|
2887
3111
|
if (state === undefined && grouping !== undefined && states !== undefined) {
|
|
2888
3112
|
const sourceRow = groupRows?.[row] ?? -1;
|
|
@@ -3101,7 +3325,7 @@ function consumeBatch(plan, batch, groups, output, memory, prefiltered = false)
|
|
|
3101
3325
|
}
|
|
3102
3326
|
return;
|
|
3103
3327
|
}
|
|
3104
|
-
if (plan.grouped && groups.consumeFast(batch
|
|
3328
|
+
if (plan.grouped && groups.consumeFast(batch))
|
|
3105
3329
|
return;
|
|
3106
3330
|
if (!plan.grouped && output.tryAddBatch(batch))
|
|
3107
3331
|
return;
|
|
@@ -3133,6 +3357,9 @@ function createGroupState(groupValues, plan, memory) {
|
|
|
3133
3357
|
payloadBytes = safeMemorySum(payloadBytes, queryValuePayloadBytes(value), "Group state");
|
|
3134
3358
|
}
|
|
3135
3359
|
payloadBytes = safeMemorySum(payloadBytes, safeMemoryProduct(plan.aggregates.length, AGGREGATE_ACCUMULATOR_BYTES, "Aggregate accumulator state"), "Group state");
|
|
3360
|
+
if (plan.hasListAggregate) {
|
|
3361
|
+
payloadBytes = safeMemorySum(payloadBytes, safeMemoryProduct(plan.aggregates.length, QUERY_REFERENCE_BYTES, "JSON aggregate list slots"), "Group state");
|
|
3362
|
+
}
|
|
3136
3363
|
// tally, not reserve: a group state lives until the context closes and is never released
|
|
3137
3364
|
// on its own, so a per-group QueryMemoryReservation object — retained in the context's Set
|
|
3138
3365
|
// for the whole query — is pure overhead at one per distinct group.
|
|
@@ -3162,6 +3389,9 @@ function createGroupState(groupValues, plan, memory) {
|
|
|
3162
3389
|
counts: new Float64Array(plan.aggregates.length),
|
|
3163
3390
|
sums: new Float64Array(plan.aggregates.length),
|
|
3164
3391
|
values: new Array(plan.aggregates.length),
|
|
3392
|
+
...(plan.hasListAggregate
|
|
3393
|
+
? { lists: new Array(plan.aggregates.length) }
|
|
3394
|
+
: {}),
|
|
3165
3395
|
valueReservations: new Array(plan.aggregates.length),
|
|
3166
3396
|
valueReservationBytes: new Float64Array(plan.aggregates.length),
|
|
3167
3397
|
distincts,
|
|
@@ -3236,7 +3466,11 @@ function updateAggregates(plan, state, batch, row, memory) {
|
|
|
3236
3466
|
const value = spec.argument.kind === "wildcard"
|
|
3237
3467
|
? 1
|
|
3238
3468
|
: evaluateBatchExpression(plan, spec.argument, batch, row);
|
|
3239
|
-
|
|
3469
|
+
const delimiter = spec.delimiter === undefined
|
|
3470
|
+
? undefined
|
|
3471
|
+
: evaluateBatchExpression(plan, spec.delimiter, batch, row);
|
|
3472
|
+
const orderValues = (spec.orderBy ?? []).map((order) => asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)));
|
|
3473
|
+
applyAggregateValue(spec, state, index, value, memory, delimiter, orderValues);
|
|
3240
3474
|
}
|
|
3241
3475
|
}
|
|
3242
3476
|
/** Reads a float64 slot (number value or raw epoch milliseconds) unboxed, or null when invalid. */
|
|
@@ -3258,10 +3492,63 @@ function rawFloat64Value(vector, rowIndex) {
|
|
|
3258
3492
|
function updateAggregatesFromValues(plan, state, values, memory) {
|
|
3259
3493
|
for (let index = 0; index < plan.aggregates.length; index += 1) {
|
|
3260
3494
|
const spec = required(plan.aggregates[index], "Aggregate specification is missing");
|
|
3261
|
-
|
|
3495
|
+
const value = values[index];
|
|
3496
|
+
if (spec.name === "STRING_AGG" && typeof value === "string") {
|
|
3497
|
+
const decoded = JSON.parse(value);
|
|
3498
|
+
if (!Array.isArray(decoded) || decoded.length !== 3) {
|
|
3499
|
+
throw new Error("Spilled STRING_AGG input is invalid");
|
|
3500
|
+
}
|
|
3501
|
+
const encodedOrder = decoded[2];
|
|
3502
|
+
if (!Array.isArray(encodedOrder))
|
|
3503
|
+
throw new Error("Spilled STRING_AGG order is invalid");
|
|
3504
|
+
applyAggregateValue(spec, state, index, decoded[0], memory, decoded[1], encodedOrder.map(decodeSpillValue));
|
|
3505
|
+
}
|
|
3506
|
+
else {
|
|
3507
|
+
applyAggregateValue(spec, state, index, value, memory);
|
|
3508
|
+
}
|
|
3262
3509
|
}
|
|
3263
3510
|
}
|
|
3264
|
-
function applyAggregateValue(spec, state, index, value, memory) {
|
|
3511
|
+
function applyAggregateValue(spec, state, index, value, memory, delimiter, orderValues = []) {
|
|
3512
|
+
if (spec.name === "JSON_ARRAYAGG") {
|
|
3513
|
+
const member = asQueryValue(value ?? null);
|
|
3514
|
+
if (spec.distinct === true && !firstOfItsKind(state, index, member, memory))
|
|
3515
|
+
return;
|
|
3516
|
+
state.counts[index] = (state.counts[index] ?? 0) + 1;
|
|
3517
|
+
const lists = required(state.lists, "JSON aggregate list state is missing");
|
|
3518
|
+
let list = lists[index];
|
|
3519
|
+
if (list === undefined) {
|
|
3520
|
+
list = [];
|
|
3521
|
+
lists[index] = list;
|
|
3522
|
+
}
|
|
3523
|
+
list.push(member);
|
|
3524
|
+
memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(member), "JSON_ARRAYAGG member"), "JSON_ARRAYAGG member");
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
if (spec.name === "STRING_AGG") {
|
|
3528
|
+
if (value === null || value === undefined)
|
|
3529
|
+
return;
|
|
3530
|
+
value = externalSqlDomainValue(value);
|
|
3531
|
+
delimiter = externalSqlDomainValue(delimiter);
|
|
3532
|
+
if (typeof value !== "string")
|
|
3533
|
+
throw new TypeError("STRING_AGG value must be a string");
|
|
3534
|
+
if (delimiter !== null && delimiter !== undefined && typeof delimiter !== "string") {
|
|
3535
|
+
throw new TypeError("STRING_AGG delimiter must be a string");
|
|
3536
|
+
}
|
|
3537
|
+
const distinctMember = JSON.stringify([value, delimiter ?? ""]);
|
|
3538
|
+
if (spec.distinct === true && !firstOfItsKind(state, index, distinctMember, memory))
|
|
3539
|
+
return;
|
|
3540
|
+
const member = JSON.stringify([value, delimiter ?? "", orderValues.map(encodeSpillValue)]);
|
|
3541
|
+
state.counts[index] = (state.counts[index] ?? 0) + 1;
|
|
3542
|
+
const lists = required(state.lists, "STRING_AGG list state is missing");
|
|
3543
|
+
let list = lists[index];
|
|
3544
|
+
if (list === undefined) {
|
|
3545
|
+
list = [];
|
|
3546
|
+
lists[index] = list;
|
|
3547
|
+
}
|
|
3548
|
+
list.push(member);
|
|
3549
|
+
memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(member), "STRING_AGG member"), "STRING_AGG member");
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3265
3552
|
if (value === null || value === undefined)
|
|
3266
3553
|
return;
|
|
3267
3554
|
// One gate for every path that accumulates: the generic per-row path, the raw datetime path,
|
|
@@ -3270,7 +3557,16 @@ function applyAggregateValue(spec, state, index, value, memory) {
|
|
|
3270
3557
|
return;
|
|
3271
3558
|
state.counts[index] = (state.counts[index] ?? 0) + 1;
|
|
3272
3559
|
if (spec.name === "SUM" || spec.name === "AVG") {
|
|
3273
|
-
|
|
3560
|
+
if (isExactNumeric(value)) {
|
|
3561
|
+
const current = state.values[index];
|
|
3562
|
+
const sum = current === undefined ? value : exactNumericBinary("+", current, value);
|
|
3563
|
+
if (sum === null || sum === undefined)
|
|
3564
|
+
throw new Error("Exact NUMERIC sum disappeared");
|
|
3565
|
+
replaceAggregateValue(state, index, sum, `${spec.name} aggregate value`, memory);
|
|
3566
|
+
}
|
|
3567
|
+
else {
|
|
3568
|
+
state.sums[index] = (state.sums[index] ?? 0) + numeric(value);
|
|
3569
|
+
}
|
|
3274
3570
|
}
|
|
3275
3571
|
else if (spec.name === "MIN" &&
|
|
3276
3572
|
(state.values[index] === undefined || compareValues(value, state.values[index]) < 0)) {
|
|
@@ -3364,12 +3660,55 @@ function evaluateFinalExpression(plan, expression, group) {
|
|
|
3364
3660
|
return count;
|
|
3365
3661
|
if (count === 0)
|
|
3366
3662
|
return null;
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
if (expression.name === "
|
|
3371
|
-
|
|
3663
|
+
if (expression.name === "JSON_ARRAYAGG") {
|
|
3664
|
+
return JSON.stringify((required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? []).map(jsonValueOf));
|
|
3665
|
+
}
|
|
3666
|
+
if (expression.name === "STRING_AGG") {
|
|
3667
|
+
const members = required(group.lists, "STRING_AGG list state is missing")[aggregateIndex] ?? [];
|
|
3668
|
+
const decoded = members.map((encoded) => {
|
|
3669
|
+
if (typeof encoded !== "string")
|
|
3670
|
+
throw new Error("STRING_AGG member is invalid");
|
|
3671
|
+
const pair = JSON.parse(encoded);
|
|
3672
|
+
if (!Array.isArray(pair) ||
|
|
3673
|
+
typeof pair[0] !== "string" ||
|
|
3674
|
+
typeof pair[1] !== "string" ||
|
|
3675
|
+
!Array.isArray(pair[2])) {
|
|
3676
|
+
throw new Error("STRING_AGG member is invalid");
|
|
3677
|
+
}
|
|
3678
|
+
return {
|
|
3679
|
+
value: pair[0],
|
|
3680
|
+
delimiter: pair[1],
|
|
3681
|
+
order: pair[2].map(decodeSpillValue),
|
|
3682
|
+
};
|
|
3683
|
+
});
|
|
3684
|
+
const orderBy = plan.aggregates[aggregateIndex]?.orderBy ?? [];
|
|
3685
|
+
if (orderBy.length > 0) {
|
|
3686
|
+
decoded.sort((left, right) => {
|
|
3687
|
+
for (const [index, order] of orderBy.entries()) {
|
|
3688
|
+
const a = left.order[index];
|
|
3689
|
+
const b = right.order[index];
|
|
3690
|
+
const placed = nullOrder(a, b, order.nulls, order.direction);
|
|
3691
|
+
if (placed !== undefined && placed !== 0)
|
|
3692
|
+
return placed;
|
|
3693
|
+
const compared = compareValues(a, b);
|
|
3694
|
+
if (compared !== 0)
|
|
3695
|
+
return order.direction === "desc" ? -compared : compared;
|
|
3696
|
+
}
|
|
3697
|
+
return 0;
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
return protectedSqlTextValue(decoded
|
|
3701
|
+
.map((member, index) => (index === 0 ? member.value : member.delimiter + member.value))
|
|
3702
|
+
.join(""));
|
|
3703
|
+
}
|
|
3372
3704
|
const value = group.values[aggregateIndex] ?? null;
|
|
3705
|
+
if (expression.name === "SUM")
|
|
3706
|
+
return isExactNumeric(value) ? value : (group.sums[aggregateIndex] ?? 0);
|
|
3707
|
+
if (expression.name === "AVG") {
|
|
3708
|
+
if (isExactNumeric(value))
|
|
3709
|
+
return exactNumericBinary("/", value, count);
|
|
3710
|
+
return (group.sums[aggregateIndex] ?? 0) / count;
|
|
3711
|
+
}
|
|
3373
3712
|
// Raw-millisecond datetime extremes re-box into a Date only here, once per surviving group.
|
|
3374
3713
|
if (typeof value === "number" && plan.aggregates[aggregateIndex]?.rawDatetime !== undefined) {
|
|
3375
3714
|
return new Date(value);
|
|
@@ -3598,19 +3937,24 @@ function detectDictionaryLike(predicate) {
|
|
|
3598
3937
|
*/
|
|
3599
3938
|
const dictionaryLikeCache = new WeakMap();
|
|
3600
3939
|
function dictionaryLikeMatches(dictionary, pattern, caseInsensitive, escape) {
|
|
3940
|
+
const externalPattern = externalSqlDomainValue(pattern);
|
|
3941
|
+
if (typeof externalPattern !== "string")
|
|
3942
|
+
throw new TypeError("LIKE requires string operands");
|
|
3601
3943
|
let patterns = dictionaryLikeCache.get(dictionary);
|
|
3602
3944
|
if (patterns === undefined) {
|
|
3603
3945
|
patterns = new Map();
|
|
3604
3946
|
dictionaryLikeCache.set(dictionary, patterns);
|
|
3605
3947
|
}
|
|
3606
|
-
const key = `${caseInsensitive ? "i" : "s"}${escape ?? ""} ${
|
|
3948
|
+
const key = `${caseInsensitive ? "i" : "s"}${escape ?? ""} ${externalPattern}`;
|
|
3607
3949
|
let matches = patterns.get(key);
|
|
3608
3950
|
if (matches === undefined) {
|
|
3609
3951
|
matches = new Uint8Array(dictionary.length);
|
|
3610
3952
|
for (let index = 0; index < dictionary.length; index += 1) {
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3953
|
+
const value = externalSqlDomainValue(dictionary[index] ?? "");
|
|
3954
|
+
matches[index] =
|
|
3955
|
+
typeof value === "string" && likeMatches(externalPattern, value, caseInsensitive, escape)
|
|
3956
|
+
? 1
|
|
3957
|
+
: 0;
|
|
3614
3958
|
}
|
|
3615
3959
|
if (patterns.size >= 32)
|
|
3616
3960
|
patterns.clear();
|
|
@@ -3709,17 +4053,24 @@ function booleanTruth(expression, evaluateValue) {
|
|
|
3709
4053
|
if (operator === "LIKE" ||
|
|
3710
4054
|
operator === "NOT LIKE" ||
|
|
3711
4055
|
operator === "ILIKE" ||
|
|
3712
|
-
operator === "NOT ILIKE"
|
|
3713
|
-
|
|
3714
|
-
|
|
4056
|
+
operator === "NOT ILIKE" ||
|
|
4057
|
+
operator === "SIMILAR TO" ||
|
|
4058
|
+
operator === "NOT SIMILAR TO") {
|
|
4059
|
+
const value = externalSqlDomainValue(evaluateValue(expression.left));
|
|
4060
|
+
const pattern = externalSqlDomainValue(evaluateValue(expression.right));
|
|
3715
4061
|
if (value === null || value === undefined || pattern === null || pattern === undefined) {
|
|
3716
4062
|
return null;
|
|
3717
4063
|
}
|
|
3718
4064
|
if (typeof value !== "string" || typeof pattern !== "string") {
|
|
3719
4065
|
throw new TypeError("LIKE requires string operands");
|
|
3720
4066
|
}
|
|
3721
|
-
const
|
|
3722
|
-
|
|
4067
|
+
const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
|
|
4068
|
+
const matched = similar
|
|
4069
|
+
? compileSimilarPattern(pattern, expression.escape ?? "\\").test(value)
|
|
4070
|
+
: likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
|
|
4071
|
+
return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
|
|
4072
|
+
? matched
|
|
4073
|
+
: !matched;
|
|
3723
4074
|
}
|
|
3724
4075
|
if (operator === "IS DISTINCT FROM" || operator === "IS NOT DISTINCT FROM") {
|
|
3725
4076
|
const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
|
|
@@ -3732,9 +4083,9 @@ function booleanTruth(expression, evaluateValue) {
|
|
|
3732
4083
|
const a = comparable(left);
|
|
3733
4084
|
const b = comparable(right);
|
|
3734
4085
|
if (operator === "=")
|
|
3735
|
-
return a ===
|
|
4086
|
+
return compareValues(a, b) === 0;
|
|
3736
4087
|
if (operator === "!=" || operator === "<>")
|
|
3737
|
-
return a !==
|
|
4088
|
+
return compareValues(a, b) !== 0;
|
|
3738
4089
|
const comparison = compareValues(a, b);
|
|
3739
4090
|
if (operator === ">")
|
|
3740
4091
|
return comparison > 0;
|
|
@@ -3933,8 +4284,11 @@ function binaryValue(operator, left, right) {
|
|
|
3933
4284
|
if (typeof left !== "string" || typeof right !== "string") {
|
|
3934
4285
|
throw new TypeError("|| requires string operands");
|
|
3935
4286
|
}
|
|
3936
|
-
return left + right;
|
|
4287
|
+
return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
|
|
3937
4288
|
}
|
|
4289
|
+
const exact = exactNumericBinary(operator, left, right);
|
|
4290
|
+
if (exact !== undefined)
|
|
4291
|
+
return exact;
|
|
3938
4292
|
const a = numeric(left);
|
|
3939
4293
|
const b = numeric(right);
|
|
3940
4294
|
if (operator === "+")
|
|
@@ -3963,7 +4317,11 @@ function comparisonValue(operator, leftValue, rightValue) {
|
|
|
3963
4317
|
if (operator === "LIKE" ||
|
|
3964
4318
|
operator === "NOT LIKE" ||
|
|
3965
4319
|
operator === "ILIKE" ||
|
|
3966
|
-
operator === "NOT ILIKE"
|
|
4320
|
+
operator === "NOT ILIKE" ||
|
|
4321
|
+
operator === "SIMILAR TO" ||
|
|
4322
|
+
operator === "NOT SIMILAR TO") {
|
|
4323
|
+
leftValue = externalSqlDomainValue(leftValue);
|
|
4324
|
+
rightValue = externalSqlDomainValue(rightValue);
|
|
3967
4325
|
if (leftValue === null ||
|
|
3968
4326
|
leftValue === undefined ||
|
|
3969
4327
|
rightValue === null ||
|
|
@@ -3973,8 +4331,13 @@ function comparisonValue(operator, leftValue, rightValue) {
|
|
|
3973
4331
|
if (typeof leftValue !== "string" || typeof rightValue !== "string") {
|
|
3974
4332
|
throw new TypeError("LIKE requires string operands");
|
|
3975
4333
|
}
|
|
3976
|
-
const
|
|
3977
|
-
|
|
4334
|
+
const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
|
|
4335
|
+
const matched = similar
|
|
4336
|
+
? compileSimilarPattern(rightValue).test(leftValue)
|
|
4337
|
+
: likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
|
|
4338
|
+
return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
|
|
4339
|
+
? matched
|
|
4340
|
+
: !matched;
|
|
3978
4341
|
}
|
|
3979
4342
|
if (leftValue === null ||
|
|
3980
4343
|
leftValue === undefined ||
|
|
@@ -3985,9 +4348,9 @@ function comparisonValue(operator, leftValue, rightValue) {
|
|
|
3985
4348
|
const left = comparable(leftValue);
|
|
3986
4349
|
const right = comparable(rightValue);
|
|
3987
4350
|
if (operator === "=")
|
|
3988
|
-
return left ===
|
|
4351
|
+
return compareValues(left, right) === 0;
|
|
3989
4352
|
if (operator === "!=" || operator === "<>")
|
|
3990
|
-
return left !==
|
|
4353
|
+
return compareValues(left, right) !== 0;
|
|
3991
4354
|
const comparison = compareValues(left, right);
|
|
3992
4355
|
if (operator === ">")
|
|
3993
4356
|
return comparison > 0;
|
|
@@ -4039,6 +4402,15 @@ function stableSortRows(rows, orderBy) {
|
|
|
4039
4402
|
}
|
|
4040
4403
|
}
|
|
4041
4404
|
function compareValues(left, right) {
|
|
4405
|
+
const collated = collatedDomainCompare(left, right);
|
|
4406
|
+
if (collated !== undefined)
|
|
4407
|
+
return collated;
|
|
4408
|
+
const enumOrder = enumDomainCompare(left, right);
|
|
4409
|
+
if (enumOrder !== undefined)
|
|
4410
|
+
return enumOrder;
|
|
4411
|
+
const exact = exactNumericCompare(left, right);
|
|
4412
|
+
if (exact !== undefined)
|
|
4413
|
+
return exact;
|
|
4042
4414
|
const a = left instanceof Date ? left.getTime() : left;
|
|
4043
4415
|
const b = right instanceof Date ? right.getTime() : right;
|
|
4044
4416
|
if (a === b)
|