@minnowdb/core 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +3 -1
- package/dist/engine/catalog.js +1 -0
- package/dist/engine/client.d.ts +32 -4
- package/dist/engine/client.js +82 -15
- package/dist/engine/database.d.ts +23 -14
- package/dist/engine/database.js +528 -79
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +13 -0
- package/dist/engine/errors.js +22 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +2 -12
- package/dist/engine/optimizer.d.ts +7 -0
- package/dist/engine/optimizer.js +1349 -76
- package/dist/engine/query.d.ts +11 -278
- package/dist/engine/query.js +178 -49
- package/dist/engine/schema-wire.d.ts +7 -1
- package/dist/engine/schema-wire.js +4 -0
- package/dist/engine/schema.d.ts +67 -33
- package/dist/engine/schema.js +138 -7
- package/dist/engine/sql-domains.d.ts +8 -0
- package/dist/engine/sql-domains.js +25 -0
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +369 -43
- package/dist/engine/worker-host.js +119 -44
- package/dist/plan/index.d.ts +5 -4
- package/dist/plan/index.js +3 -3
- package/dist/plan/model.d.ts +224 -0
- package/dist/plan/model.js +1 -0
- package/dist/storage/types.d.ts +7 -0
- package/dist/storage/types.js +16 -0
- package/dist/transactions/index.d.ts +5 -3
- package/dist/transactions/index.js +58 -8
- package/dist/worker-protocol/index.d.ts +6 -1
- package/dist/worker-protocol/index.js +5 -2
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +89 -21
package/dist/engine/vector.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { dateMilliseconds } from "../date-value.js";
|
|
2
2
|
import { MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH, } from "../storage/types.js";
|
|
3
|
+
import { throwIfAborted } from "./cancellation.js";
|
|
3
4
|
import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, unknownColumnDomains, } from "./query.js";
|
|
4
|
-
import {
|
|
5
|
+
import { jsonConstructor } from "./sql-json.js";
|
|
5
6
|
import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize, } from "./fts.js";
|
|
6
7
|
import { ByteGroupIndex } from "./group-index.js";
|
|
7
8
|
import { ByteJoinIndex } from "./join-index.js";
|
|
8
9
|
import { UnknownTableError } from "./errors.js";
|
|
9
10
|
import { QueryMemoryBudgetError, QueryMemoryContext, } from "./memory.js";
|
|
10
11
|
import { compareSqlValues, compileSimilarPattern, defineSqlResultProperty, } from "./sql-semantics.js";
|
|
11
|
-
import { exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, protectedSqlTextValue, } from "./sql-domains.js";
|
|
12
|
+
import { exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, preservedJsonDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
|
|
12
13
|
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
13
14
|
const DEFAULT_BATCH_ROWS = 2_048;
|
|
14
15
|
/** Above this, locating each IN member separately costs more than scanning between them. */
|
|
@@ -42,11 +43,14 @@ function boundedSpillPageRows(value) {
|
|
|
42
43
|
return Math.min(rows, DEFAULT_BATCH_ROWS);
|
|
43
44
|
}
|
|
44
45
|
/** Writes already-materialized pages through the batch method when the store offers one. */
|
|
45
|
-
async function writeSpillPages(store, pages) {
|
|
46
|
+
async function writeSpillPages(store, pages, signal) {
|
|
47
|
+
throwIfAborted(signal);
|
|
46
48
|
const batched = store.putPages?.bind(store);
|
|
47
49
|
if (batched === undefined) {
|
|
48
50
|
for (const page of pages) {
|
|
51
|
+
throwIfAborted(signal);
|
|
49
52
|
await store.putPage(page.ownerId, page.runId, page.pageIndex, page.bytes);
|
|
53
|
+
throwIfAborted(signal);
|
|
50
54
|
}
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
@@ -60,24 +64,28 @@ async function writeSpillPages(store, pages) {
|
|
|
60
64
|
(batch.length === SPILL_WRITE_BATCH_PAGES ||
|
|
61
65
|
batchBytes + page.bytes.byteLength > MAX_TEMP_RUN_BATCH_BYTES)) {
|
|
62
66
|
await batched(batch);
|
|
67
|
+
throwIfAborted(signal);
|
|
63
68
|
batch = [];
|
|
64
69
|
batchBytes = 0;
|
|
65
70
|
}
|
|
66
71
|
batch.push(page);
|
|
67
72
|
batchBytes += page.bytes.byteLength;
|
|
68
73
|
}
|
|
69
|
-
if (batch.length > 0)
|
|
74
|
+
if (batch.length > 0) {
|
|
70
75
|
await batched(batch);
|
|
76
|
+
throwIfAborted(signal);
|
|
77
|
+
}
|
|
71
78
|
}
|
|
72
|
-
async function writeSpillRowPages(store, ownerId, runId, startPageIndex, columns, rows, pageRows) {
|
|
79
|
+
async function writeSpillRowPages(store, ownerId, runId, startPageIndex, columns, rows, pageRows, signal) {
|
|
73
80
|
let pending = [];
|
|
74
81
|
let pendingBytes = 0;
|
|
75
82
|
let pageCount = 0;
|
|
76
83
|
for (const bytes of encodeSpillRowPages(columns, rows, pageRows)) {
|
|
84
|
+
throwIfAborted(signal);
|
|
77
85
|
if (pending.length > 0 &&
|
|
78
86
|
(pending.length === SPILL_WRITE_BATCH_PAGES ||
|
|
79
87
|
pendingBytes + bytes.byteLength > MAX_TEMP_RUN_BATCH_BYTES)) {
|
|
80
|
-
await writeSpillPages(store, pending);
|
|
88
|
+
await writeSpillPages(store, pending, signal);
|
|
81
89
|
pending = [];
|
|
82
90
|
pendingBytes = 0;
|
|
83
91
|
}
|
|
@@ -85,7 +93,7 @@ async function writeSpillRowPages(store, ownerId, runId, startPageIndex, columns
|
|
|
85
93
|
pendingBytes += bytes.byteLength;
|
|
86
94
|
pageCount += 1;
|
|
87
95
|
}
|
|
88
|
-
await writeSpillPages(store, pending);
|
|
96
|
+
await writeSpillPages(store, pending, signal);
|
|
89
97
|
return pageCount;
|
|
90
98
|
}
|
|
91
99
|
export function createColumnarTable(name, columns, uniqueKey) {
|
|
@@ -157,13 +165,17 @@ export function prepareVectorQuery(plan, inputTables, options = {}) {
|
|
|
157
165
|
async executeAsync(executionOptions = {}) {
|
|
158
166
|
if (closed)
|
|
159
167
|
throw new Error("Prepared vector query is closed");
|
|
168
|
+
throwIfAborted(executionOptions.signal);
|
|
160
169
|
const canSpillSort = bound.orderBy.length > 0 && !bound.grouped && !bound.sourceOrdered;
|
|
161
170
|
// An unordered grouped plan spills too: the empty ordering makes the pairwise merge a
|
|
162
171
|
// stable concatenation, and partition-wise accumulation bounds peak group state.
|
|
163
172
|
const canSpillHash = bound.grouped && bound.groupBy.length > 0;
|
|
164
173
|
if (executionOptions.spillStore === undefined || (!canSpillSort && !canSpillHash)) {
|
|
165
|
-
if (executionOptions.loadScanWindow === undefined)
|
|
166
|
-
|
|
174
|
+
if (executionOptions.loadScanWindow === undefined) {
|
|
175
|
+
const result = this.execute();
|
|
176
|
+
throwIfAborted(executionOptions.signal);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
167
179
|
const executionMemory = retainedMemory.createChild();
|
|
168
180
|
try {
|
|
169
181
|
return await executeBoundPlanAsync(bound, executionMemory, executionOptions);
|
|
@@ -589,6 +601,9 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
589
601
|
const dictionaryLike = detectDictionaryLike(bound);
|
|
590
602
|
if (dictionaryLike !== undefined)
|
|
591
603
|
return { ...bound, dictionaryLike };
|
|
604
|
+
const dictionaryNumeric = detectDictionaryNumericComparison(bound);
|
|
605
|
+
if (dictionaryNumeric !== undefined)
|
|
606
|
+
return { ...bound, dictionaryNumeric };
|
|
592
607
|
const primitive = detectPrimitiveComparison(bound);
|
|
593
608
|
if (primitive !== undefined)
|
|
594
609
|
return { ...bound, primitive };
|
|
@@ -599,8 +614,11 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
599
614
|
const bound = bindPredicate(predicate);
|
|
600
615
|
if (bound.primitive !== undefined || bound.primitiveIn !== undefined)
|
|
601
616
|
return bound;
|
|
602
|
-
if (bound.dictionaryEquality !== undefined ||
|
|
617
|
+
if (bound.dictionaryEquality !== undefined ||
|
|
618
|
+
bound.dictionaryLike !== undefined ||
|
|
619
|
+
bound.dictionaryNumeric !== undefined) {
|
|
603
620
|
return bound;
|
|
621
|
+
}
|
|
604
622
|
const branches = disjunctiveNormalForm(predicate);
|
|
605
623
|
if (branches === undefined)
|
|
606
624
|
return bound;
|
|
@@ -652,10 +670,10 @@ function bindPlan(plan, tables, memory, ftsStats) {
|
|
|
652
670
|
});
|
|
653
671
|
// A wildcard select projects the materialized columns of each source, so ORDER BY resolves
|
|
654
672
|
// against those same names.
|
|
655
|
-
const orderSources = sources.
|
|
656
|
-
|
|
657
|
-
columns: [
|
|
658
|
-
})
|
|
673
|
+
const orderSources = sources.flatMap((source, index) => {
|
|
674
|
+
const columns = [...(sourceTables[index]?.columns.keys() ?? [])].filter((name) => !name.startsWith("\0"));
|
|
675
|
+
return columns.length === 0 ? [] : [{ alias: source.alias, columns }];
|
|
676
|
+
});
|
|
659
677
|
const orderBy = plan.orderBy.map(({ expression, direction, nulls }) => ({
|
|
660
678
|
outputName: orderOutputName(expression, plan.select, orderSources),
|
|
661
679
|
direction,
|
|
@@ -846,8 +864,11 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
|
|
|
846
864
|
argument.vector.kind === "datetime"
|
|
847
865
|
? { source: argument.source, vector: argument.vector }
|
|
848
866
|
: undefined;
|
|
849
|
-
const rawNumber = expression.name
|
|
850
|
-
expression.name
|
|
867
|
+
const rawNumber = (expression.name === "COUNT" ||
|
|
868
|
+
expression.name === "SUM" ||
|
|
869
|
+
expression.name === "AVG" ||
|
|
870
|
+
expression.name === "MIN" ||
|
|
871
|
+
expression.name === "MAX") &&
|
|
851
872
|
argument.kind === "column" &&
|
|
852
873
|
argument.vector.kind === "number"
|
|
853
874
|
? { source: argument.source, vector: argument.vector }
|
|
@@ -1217,6 +1238,7 @@ function executeBoundPlan(plan, memory) {
|
|
|
1217
1238
|
return finishResult(plan, rows, memory);
|
|
1218
1239
|
}
|
|
1219
1240
|
async function executeBoundPlanAsync(plan, memory, options) {
|
|
1241
|
+
throwIfAborted(options.signal);
|
|
1220
1242
|
const metadataCount = executeMetadataCount(plan, memory);
|
|
1221
1243
|
if (metadataCount !== undefined)
|
|
1222
1244
|
return metadataCount;
|
|
@@ -1224,11 +1246,13 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1224
1246
|
const output = new ResultSink(plan, memory, options.loadScanWindow === undefined);
|
|
1225
1247
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
1226
1248
|
for (let start = 0; start < scanRows;) {
|
|
1249
|
+
throwIfAborted(options.signal);
|
|
1227
1250
|
let length = Math.min(DEFAULT_BATCH_ROWS, scanRows - start);
|
|
1228
1251
|
// The loader answers synchronously when the batch is already resident — the common case,
|
|
1229
1252
|
// every batch but the first per block — so the scan loop only pays await on real slides.
|
|
1230
1253
|
const loaded = options.loadScanWindow?.(start, length);
|
|
1231
1254
|
const residentEnd = typeof loaded === "number" || loaded === undefined ? loaded : await loaded;
|
|
1255
|
+
throwIfAborted(options.signal);
|
|
1232
1256
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1233
1257
|
length = Math.min(length, residentEnd - start);
|
|
1234
1258
|
}
|
|
@@ -1250,6 +1274,7 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1250
1274
|
const ranges = narrowed.ranges ?? [{ begin: narrowed.begin, end: narrowed.end }];
|
|
1251
1275
|
for (const range of ranges) {
|
|
1252
1276
|
for (let row = range.begin; row < range.end; row += DEFAULT_BATCH_ROWS) {
|
|
1277
|
+
throwIfAborted(options.signal);
|
|
1253
1278
|
const rows = Math.min(DEFAULT_BATCH_ROWS, range.end - row);
|
|
1254
1279
|
if (runScanBatch(plan, row, rows, groups, output, memory)) {
|
|
1255
1280
|
stopped = true;
|
|
@@ -1263,6 +1288,7 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1263
1288
|
break;
|
|
1264
1289
|
start = windowEnd;
|
|
1265
1290
|
}
|
|
1291
|
+
throwIfAborted(options.signal);
|
|
1266
1292
|
const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
|
|
1267
1293
|
return finishResult(plan, rows, memory);
|
|
1268
1294
|
}
|
|
@@ -1275,24 +1301,44 @@ async function executeBoundPlanBatches(plan, memory, options, consume) {
|
|
|
1275
1301
|
const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
|
|
1276
1302
|
const { limit, offset, ...unbounded } = plan;
|
|
1277
1303
|
const scanPlan = unbounded;
|
|
1304
|
+
const consumeFirstColumn = options.consumeFirstColumn;
|
|
1305
|
+
const firstExpression = consumeFirstColumn === undefined ? undefined : scanPlan.select[0]?.expression;
|
|
1278
1306
|
let skipped = 0;
|
|
1279
1307
|
let emitted = 0;
|
|
1280
1308
|
const scanRows = scanPlan.sourceTables[scanPlan.scanSource]?.rowCount ?? 0;
|
|
1281
1309
|
const step = Math.min(DEFAULT_BATCH_ROWS, options.batchRows);
|
|
1282
1310
|
for (let start = 0; start < scanRows && (limit === undefined || emitted < limit);) {
|
|
1283
|
-
options.signal
|
|
1311
|
+
throwIfAborted(options.signal);
|
|
1284
1312
|
let length = Math.min(step, scanRows - start);
|
|
1285
1313
|
const loaded = options.loadScanWindow?.(start, length);
|
|
1286
1314
|
const residentEnd = typeof loaded === "number" || loaded === undefined ? loaded : await loaded;
|
|
1287
|
-
options.signal
|
|
1315
|
+
throwIfAborted(options.signal);
|
|
1288
1316
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1289
1317
|
length = Math.min(length, residentEnd - start);
|
|
1290
1318
|
}
|
|
1291
1319
|
const pageMemory = memory.createChild();
|
|
1292
1320
|
try {
|
|
1293
1321
|
const groups = new GroupAccumulator(scanPlan, pageMemory);
|
|
1294
|
-
const
|
|
1322
|
+
const values = [];
|
|
1323
|
+
const output = firstExpression === undefined
|
|
1324
|
+
? new ResultSink(scanPlan, pageMemory, options.loadScanWindow === undefined)
|
|
1325
|
+
: {
|
|
1326
|
+
get size() {
|
|
1327
|
+
return values.length;
|
|
1328
|
+
},
|
|
1329
|
+
tryAddBatch: () => false,
|
|
1330
|
+
add: (batch, row) => {
|
|
1331
|
+
values.push(asQueryValue(evaluateBatchExpression(scanPlan, firstExpression, batch, row)));
|
|
1332
|
+
},
|
|
1333
|
+
finish: () => [],
|
|
1334
|
+
};
|
|
1295
1335
|
runScanBatch(scanPlan, start, length, groups, output, pageMemory);
|
|
1336
|
+
if (firstExpression !== undefined && consumeFirstColumn !== undefined) {
|
|
1337
|
+
if (values.length > 0)
|
|
1338
|
+
await consumeFirstColumn(values);
|
|
1339
|
+
start += length;
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1296
1342
|
let rows = output.finish();
|
|
1297
1343
|
const remainingOffset = Math.max(0, (offset ?? 0) - skipped);
|
|
1298
1344
|
if (remainingOffset > 0) {
|
|
@@ -1323,6 +1369,7 @@ function createSpillOwnerId() {
|
|
|
1323
1369
|
return `query-${globalThis.crypto.randomUUID()}`;
|
|
1324
1370
|
}
|
|
1325
1371
|
async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
1372
|
+
throwIfAborted(options.signal);
|
|
1326
1373
|
const store = required(options.spillStore, "Query spill store is missing");
|
|
1327
1374
|
const pageRows = boundedSpillPageRows(options.spillPageRows);
|
|
1328
1375
|
const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
|
|
@@ -1333,9 +1380,11 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1333
1380
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
1334
1381
|
const scanBatchRows = Math.min(DEFAULT_BATCH_ROWS, pageRows);
|
|
1335
1382
|
for (let start = 0; start < scanRows;) {
|
|
1383
|
+
throwIfAborted(options.signal);
|
|
1336
1384
|
let length = Math.min(scanBatchRows, scanRows - start);
|
|
1337
1385
|
const loadedSort = options.loadScanWindow?.(start, length);
|
|
1338
1386
|
const residentEnd = typeof loadedSort === "number" || loadedSort === undefined ? loadedSort : await loadedSort;
|
|
1387
|
+
throwIfAborted(options.signal);
|
|
1339
1388
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1340
1389
|
length = Math.min(length, residentEnd - start);
|
|
1341
1390
|
}
|
|
@@ -1351,6 +1400,7 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1351
1400
|
for (let index = 0; index < length; index += 1)
|
|
1352
1401
|
scan[index] = start + index;
|
|
1353
1402
|
await spillJoinedBatches(plan, { length, rowsBySource: sourceRows, memory: batchMemory }, 0, memory, async (batch) => {
|
|
1403
|
+
throwIfAborted(options.signal);
|
|
1354
1404
|
const outputMemory = memory.createChild();
|
|
1355
1405
|
try {
|
|
1356
1406
|
const rows = [];
|
|
@@ -1365,13 +1415,13 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1365
1415
|
ordering.release();
|
|
1366
1416
|
}
|
|
1367
1417
|
const runId = `run-${String(runSequence++)}`;
|
|
1368
|
-
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows);
|
|
1418
|
+
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows, options.signal);
|
|
1369
1419
|
runs.push({ id: runId, pageCount });
|
|
1370
1420
|
}
|
|
1371
1421
|
finally {
|
|
1372
1422
|
outputMemory.close();
|
|
1373
1423
|
}
|
|
1374
|
-
});
|
|
1424
|
+
}, options.signal);
|
|
1375
1425
|
}
|
|
1376
1426
|
finally {
|
|
1377
1427
|
batchMemory.close();
|
|
@@ -1382,8 +1432,10 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1382
1432
|
return { columns, columnDomains: unknownColumnDomains(columns), rows: [] };
|
|
1383
1433
|
let active = runs;
|
|
1384
1434
|
while (active.length > 1) {
|
|
1435
|
+
throwIfAborted(options.signal);
|
|
1385
1436
|
const merged = [];
|
|
1386
1437
|
for (let index = 0; index < active.length; index += 2) {
|
|
1438
|
+
throwIfAborted(options.signal);
|
|
1387
1439
|
const left = required(active[index], "Left spill run is missing");
|
|
1388
1440
|
const right = active[index + 1];
|
|
1389
1441
|
if (right === undefined) {
|
|
@@ -1391,9 +1443,10 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1391
1443
|
continue;
|
|
1392
1444
|
}
|
|
1393
1445
|
const outputId = `merge-${String(runSequence++)}`;
|
|
1394
|
-
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, plan.orderBy, pageRows, memory));
|
|
1446
|
+
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, plan.orderBy, pageRows, memory, options.signal));
|
|
1395
1447
|
await store.removeRun(ownerId, left.id);
|
|
1396
1448
|
await store.removeRun(ownerId, right.id);
|
|
1449
|
+
throwIfAborted(options.signal);
|
|
1397
1450
|
}
|
|
1398
1451
|
active = merged;
|
|
1399
1452
|
}
|
|
@@ -1402,7 +1455,9 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1402
1455
|
const offset = plan.offset ?? 0;
|
|
1403
1456
|
const limit = plan.limit === undefined ? Number.MAX_SAFE_INTEGER : plan.limit + offset;
|
|
1404
1457
|
for (let pageIndex = 0; pageIndex < finalRun.pageCount && rows.length < limit; pageIndex += 1) {
|
|
1458
|
+
throwIfAborted(options.signal);
|
|
1405
1459
|
const bytes = await store.getPage(ownerId, finalRun.id, pageIndex);
|
|
1460
|
+
throwIfAborted(options.signal);
|
|
1406
1461
|
if (bytes === undefined)
|
|
1407
1462
|
throw new Error("Query spill page is missing");
|
|
1408
1463
|
for (const row of decodeSpillRows(columns, bytes)) {
|
|
@@ -1420,6 +1475,7 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1420
1475
|
}
|
|
1421
1476
|
}
|
|
1422
1477
|
async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
1478
|
+
throwIfAborted(options.signal);
|
|
1423
1479
|
const store = required(options.spillStore, "Query spill store is missing");
|
|
1424
1480
|
const pageRows = boundedSpillPageRows(options.spillPageRows);
|
|
1425
1481
|
const partitionCount = 64;
|
|
@@ -1436,9 +1492,11 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1436
1492
|
// page size while staying coarse enough to amortize partition-page write transactions.
|
|
1437
1493
|
const scanChunkRows = Math.min(DEFAULT_BATCH_ROWS, HASH_SPILL_SCAN_CHUNK_ROWS);
|
|
1438
1494
|
for (let start = 0; start < scanRows;) {
|
|
1495
|
+
throwIfAborted(options.signal);
|
|
1439
1496
|
let length = Math.min(scanChunkRows, scanRows - start);
|
|
1440
1497
|
const loadedHash = options.loadScanWindow?.(start, length);
|
|
1441
1498
|
const residentEnd = typeof loadedHash === "number" || loadedHash === undefined ? loadedHash : await loadedHash;
|
|
1499
|
+
throwIfAborted(options.signal);
|
|
1442
1500
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1443
1501
|
length = Math.min(length, residentEnd - start);
|
|
1444
1502
|
}
|
|
@@ -1458,6 +1516,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1458
1516
|
// Each surviving row spills its evaluated group keys and aggregate arguments, so the
|
|
1459
1517
|
// partition phase never re-reads source vectors and the scan source may be windowed.
|
|
1460
1518
|
async (batch) => {
|
|
1519
|
+
throwIfAborted(options.signal);
|
|
1461
1520
|
for (let row = 0; row < batch.length; row += 1) {
|
|
1462
1521
|
if (!plan.predicates.every((predicate) => evaluateBatchPredicate(plan, predicate, batch, row))) {
|
|
1463
1522
|
continue;
|
|
@@ -1480,6 +1539,12 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1480
1539
|
(spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
|
|
1481
1540
|
]);
|
|
1482
1541
|
}
|
|
1542
|
+
else if (spec.name === "JSON_ARRAYAGG" && (spec.orderBy?.length ?? 0) > 0) {
|
|
1543
|
+
spillRow[`a${String(index)}`] = JSON.stringify([
|
|
1544
|
+
encodeSpillValue(asQueryValue(raw ?? null)),
|
|
1545
|
+
(spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
|
|
1546
|
+
]);
|
|
1547
|
+
}
|
|
1483
1548
|
else {
|
|
1484
1549
|
spillRow[`a${String(index)}`] =
|
|
1485
1550
|
raw === null || raw === undefined ? null : asQueryValue(raw);
|
|
@@ -1491,7 +1556,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1491
1556
|
rows.push(spillRow);
|
|
1492
1557
|
partitionBuffers.set(partition, rows);
|
|
1493
1558
|
}
|
|
1494
|
-
});
|
|
1559
|
+
}, options.signal);
|
|
1495
1560
|
const flush = [];
|
|
1496
1561
|
let flushBytes = 0;
|
|
1497
1562
|
for (const [partition, rows] of partitionBuffers) {
|
|
@@ -1499,7 +1564,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1499
1564
|
if (flush.length > 0 &&
|
|
1500
1565
|
(flush.length === SPILL_WRITE_BATCH_PAGES ||
|
|
1501
1566
|
flushBytes + bytes.byteLength > MAX_TEMP_RUN_BATCH_BYTES)) {
|
|
1502
|
-
await writeSpillPages(store, flush);
|
|
1567
|
+
await writeSpillPages(store, flush, options.signal);
|
|
1503
1568
|
flush.length = 0;
|
|
1504
1569
|
flushBytes = 0;
|
|
1505
1570
|
}
|
|
@@ -1514,7 +1579,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1514
1579
|
partitionPages[partition] = pageIndex + 1;
|
|
1515
1580
|
}
|
|
1516
1581
|
}
|
|
1517
|
-
await writeSpillPages(store, flush);
|
|
1582
|
+
await writeSpillPages(store, flush, options.signal);
|
|
1518
1583
|
}
|
|
1519
1584
|
finally {
|
|
1520
1585
|
batchMemory.close();
|
|
@@ -1523,6 +1588,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1523
1588
|
}
|
|
1524
1589
|
const runs = [];
|
|
1525
1590
|
for (let partition = 0; partition < partitionCount; partition += 1) {
|
|
1591
|
+
throwIfAborted(options.signal);
|
|
1526
1592
|
const sourcePageCount = partitionPages[partition] ?? 0;
|
|
1527
1593
|
if (sourcePageCount === 0)
|
|
1528
1594
|
continue;
|
|
@@ -1530,7 +1596,9 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1530
1596
|
try {
|
|
1531
1597
|
const groups = new ByteGroupIndex(partitionMemory);
|
|
1532
1598
|
for (let pageIndex = 0; pageIndex < sourcePageCount; pageIndex += 1) {
|
|
1599
|
+
throwIfAborted(options.signal);
|
|
1533
1600
|
const bytes = await store.getPage(ownerId, `partition-${String(partition)}`, pageIndex);
|
|
1601
|
+
throwIfAborted(options.signal);
|
|
1534
1602
|
if (bytes === undefined)
|
|
1535
1603
|
throw new Error("Query hash spill page is missing");
|
|
1536
1604
|
const pageMemory = partitionMemory.createChild();
|
|
@@ -1559,7 +1627,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1559
1627
|
}
|
|
1560
1628
|
}
|
|
1561
1629
|
const runId = `group-${String(runSequence++)}`;
|
|
1562
|
-
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows);
|
|
1630
|
+
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows, options.signal);
|
|
1563
1631
|
runs.push({ id: runId, pageCount });
|
|
1564
1632
|
}
|
|
1565
1633
|
finally {
|
|
@@ -1569,9 +1637,9 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1569
1637
|
}
|
|
1570
1638
|
if (runs.length === 0)
|
|
1571
1639
|
return { columns, columnDomains: unknownColumnDomains(columns), rows: [] };
|
|
1572
|
-
const finalRun = await mergeAllSpillRuns(store, ownerId, runs, columns, plan.orderBy, pageRows, () => `merge-${String(runSequence++)}`, memory);
|
|
1640
|
+
const finalRun = await mergeAllSpillRuns(store, ownerId, runs, columns, plan.orderBy, pageRows, () => `merge-${String(runSequence++)}`, memory, options.signal);
|
|
1573
1641
|
const spillOffset = plan.offset ?? 0;
|
|
1574
|
-
const result = await readFinalSpillRun(store, ownerId, finalRun, columns, plan.limit === undefined ? undefined : plan.limit + spillOffset);
|
|
1642
|
+
const result = await readFinalSpillRun(store, ownerId, finalRun, columns, plan.limit === undefined ? undefined : plan.limit + spillOffset, options.signal);
|
|
1575
1643
|
if (spillOffset > 0)
|
|
1576
1644
|
result.rows.splice(0, Math.min(spillOffset, result.rows.length));
|
|
1577
1645
|
return result;
|
|
@@ -1580,11 +1648,13 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1580
1648
|
await store.removeOwner(ownerId);
|
|
1581
1649
|
}
|
|
1582
1650
|
}
|
|
1583
|
-
async function mergeAllSpillRuns(store, ownerId, runs, columns, orderBy, pageRows, nextRunId, memory) {
|
|
1651
|
+
async function mergeAllSpillRuns(store, ownerId, runs, columns, orderBy, pageRows, nextRunId, memory, signal) {
|
|
1584
1652
|
let active = [...runs];
|
|
1585
1653
|
while (active.length > 1) {
|
|
1654
|
+
throwIfAborted(signal);
|
|
1586
1655
|
const merged = [];
|
|
1587
1656
|
for (let index = 0; index < active.length; index += 2) {
|
|
1657
|
+
throwIfAborted(signal);
|
|
1588
1658
|
const left = required(active[index], "Left spill run is missing");
|
|
1589
1659
|
const right = active[index + 1];
|
|
1590
1660
|
if (right === undefined) {
|
|
@@ -1592,19 +1662,22 @@ async function mergeAllSpillRuns(store, ownerId, runs, columns, orderBy, pageRow
|
|
|
1592
1662
|
continue;
|
|
1593
1663
|
}
|
|
1594
1664
|
const outputId = nextRunId();
|
|
1595
|
-
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory));
|
|
1665
|
+
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory, signal));
|
|
1596
1666
|
await store.removeRun(ownerId, left.id);
|
|
1597
1667
|
await store.removeRun(ownerId, right.id);
|
|
1668
|
+
throwIfAborted(signal);
|
|
1598
1669
|
}
|
|
1599
1670
|
active = merged;
|
|
1600
1671
|
}
|
|
1601
1672
|
return required(active[0], "Final spill run is missing");
|
|
1602
1673
|
}
|
|
1603
|
-
async function readFinalSpillRun(store, ownerId, run, columns, requestedLimit) {
|
|
1674
|
+
async function readFinalSpillRun(store, ownerId, run, columns, requestedLimit, signal) {
|
|
1604
1675
|
const rows = [];
|
|
1605
1676
|
const limit = requestedLimit ?? Number.MAX_SAFE_INTEGER;
|
|
1606
1677
|
for (let pageIndex = 0; pageIndex < run.pageCount && rows.length < limit; pageIndex += 1) {
|
|
1678
|
+
throwIfAborted(signal);
|
|
1607
1679
|
const bytes = await store.getPage(ownerId, run.id, pageIndex);
|
|
1680
|
+
throwIfAborted(signal);
|
|
1608
1681
|
if (bytes === undefined) {
|
|
1609
1682
|
throw new Error(`Query spill page is missing: ${run.id}/${String(pageIndex)}`);
|
|
1610
1683
|
}
|
|
@@ -1651,15 +1724,17 @@ function hashQueryValues(values) {
|
|
|
1651
1724
|
}
|
|
1652
1725
|
return hash;
|
|
1653
1726
|
}
|
|
1654
|
-
async function spillJoinedBatches(plan, batch, joinIndex, memory, consume) {
|
|
1727
|
+
async function spillJoinedBatches(plan, batch, joinIndex, memory, consume, signal) {
|
|
1728
|
+
throwIfAborted(signal);
|
|
1655
1729
|
const join = plan.joins[joinIndex];
|
|
1656
1730
|
if (join === undefined) {
|
|
1657
1731
|
await consume(batch);
|
|
1658
1732
|
return;
|
|
1659
1733
|
}
|
|
1660
1734
|
for (const joined of joinBatches(plan, batch, join, memory)) {
|
|
1735
|
+
throwIfAborted(signal);
|
|
1661
1736
|
try {
|
|
1662
|
-
await spillJoinedBatches(plan, joined, joinIndex + 1, memory, consume);
|
|
1737
|
+
await spillJoinedBatches(plan, joined, joinIndex + 1, memory, consume, signal);
|
|
1663
1738
|
}
|
|
1664
1739
|
finally {
|
|
1665
1740
|
joined.memory?.close();
|
|
@@ -1686,25 +1761,27 @@ function passesPredicates(plan, batch, row) {
|
|
|
1686
1761
|
}
|
|
1687
1762
|
return true;
|
|
1688
1763
|
}
|
|
1689
|
-
async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory) {
|
|
1764
|
+
async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory, signal) {
|
|
1690
1765
|
const mergeMemory = memory.createChild();
|
|
1691
|
-
const leftReader = createSpillRunReader(store, ownerId, left, columns, mergeMemory);
|
|
1692
|
-
const rightReader = createSpillRunReader(store, ownerId, right, columns, mergeMemory);
|
|
1766
|
+
const leftReader = createSpillRunReader(store, ownerId, left, columns, mergeMemory, signal);
|
|
1767
|
+
const rightReader = createSpillRunReader(store, ownerId, right, columns, mergeMemory, signal);
|
|
1693
1768
|
let outputPage = [];
|
|
1694
1769
|
let outputMemory = mergeMemory.createChild();
|
|
1695
1770
|
let pageIndex = 0;
|
|
1696
1771
|
const flush = async () => {
|
|
1697
1772
|
if (outputPage.length === 0)
|
|
1698
1773
|
return;
|
|
1699
|
-
pageIndex += await writeSpillRowPages(store, ownerId, outputId, pageIndex, columns, outputPage, pageRows);
|
|
1774
|
+
pageIndex += await writeSpillRowPages(store, ownerId, outputId, pageIndex, columns, outputPage, pageRows, signal);
|
|
1700
1775
|
outputPage = [];
|
|
1701
1776
|
outputMemory.close();
|
|
1702
1777
|
outputMemory = mergeMemory.createChild();
|
|
1703
1778
|
};
|
|
1704
1779
|
try {
|
|
1780
|
+
throwIfAborted(signal);
|
|
1705
1781
|
let leftRow = await leftReader.next();
|
|
1706
1782
|
let rightRow = await rightReader.next();
|
|
1707
1783
|
while (leftRow !== undefined || rightRow !== undefined) {
|
|
1784
|
+
throwIfAborted(signal);
|
|
1708
1785
|
if (rightRow === undefined ||
|
|
1709
1786
|
(leftRow !== undefined && compareOrderedRows(leftRow, rightRow, orderBy) <= 0)) {
|
|
1710
1787
|
const row = required(leftRow, "Left spill row is missing");
|
|
@@ -1730,17 +1807,19 @@ async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, or
|
|
|
1730
1807
|
mergeMemory.close();
|
|
1731
1808
|
}
|
|
1732
1809
|
}
|
|
1733
|
-
function createSpillRunReader(store, ownerId, run, columns, memory) {
|
|
1810
|
+
function createSpillRunReader(store, ownerId, run, columns, memory, signal) {
|
|
1734
1811
|
let pageIndex = 0;
|
|
1735
1812
|
let rows = [];
|
|
1736
1813
|
let rowIndex = 0;
|
|
1737
1814
|
let pageReservation;
|
|
1738
1815
|
return {
|
|
1739
1816
|
async next() {
|
|
1817
|
+
throwIfAborted(signal);
|
|
1740
1818
|
while (rowIndex >= rows.length) {
|
|
1741
1819
|
if (pageIndex >= run.pageCount)
|
|
1742
1820
|
return undefined;
|
|
1743
1821
|
const bytes = await store.getPage(ownerId, run.id, pageIndex);
|
|
1822
|
+
throwIfAborted(signal);
|
|
1744
1823
|
if (bytes === undefined)
|
|
1745
1824
|
throw new Error("Query spill page is missing");
|
|
1746
1825
|
pageReservation?.release();
|
|
@@ -2269,6 +2348,43 @@ function filterDictionaryLike(fast, batch, selection, survivors) {
|
|
|
2269
2348
|
}
|
|
2270
2349
|
return kept;
|
|
2271
2350
|
}
|
|
2351
|
+
/** Compacts the selection using a match table computed once per exact-NUMERIC dictionary. */
|
|
2352
|
+
function filterDictionaryNumeric(fast, batch, selection, survivors) {
|
|
2353
|
+
const vector = fast.expression.vector;
|
|
2354
|
+
if (fast.cache.dictionary !== vector.dictionary) {
|
|
2355
|
+
const matches = dictionaryNumericMatches(fast, vector.dictionary);
|
|
2356
|
+
if (matches === undefined)
|
|
2357
|
+
return undefined;
|
|
2358
|
+
fast.cache.dictionary = vector.dictionary;
|
|
2359
|
+
fast.cache.matches = matches;
|
|
2360
|
+
}
|
|
2361
|
+
const matches = fast.cache.matches;
|
|
2362
|
+
const rows = batch.rowsBySource[fast.expression.source];
|
|
2363
|
+
const codes = vector.codes;
|
|
2364
|
+
const validity = vector.validity;
|
|
2365
|
+
const windowStart = vector.window?.start ?? 0;
|
|
2366
|
+
const slots = codes.length;
|
|
2367
|
+
const vectorLength = vector.length;
|
|
2368
|
+
let kept = 0;
|
|
2369
|
+
for (let index = 0; index < survivors; index += 1) {
|
|
2370
|
+
const row = selection[index] ?? 0;
|
|
2371
|
+
const sourceRow = rows?.[row] ?? -1;
|
|
2372
|
+
if (sourceRow < 0 || sourceRow >= vectorLength)
|
|
2373
|
+
continue;
|
|
2374
|
+
const slot = sourceRow - windowStart;
|
|
2375
|
+
if (slot < 0 || slot >= slots) {
|
|
2376
|
+
throw new RangeError("Streamed vector row is outside the resident window");
|
|
2377
|
+
}
|
|
2378
|
+
if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) === 0)
|
|
2379
|
+
continue;
|
|
2380
|
+
const code = codes[slot] ?? NULL_STRING_CODE;
|
|
2381
|
+
if (code === NULL_STRING_CODE || matches[code] !== 1)
|
|
2382
|
+
continue;
|
|
2383
|
+
selection[kept] = row;
|
|
2384
|
+
kept += 1;
|
|
2385
|
+
}
|
|
2386
|
+
return kept;
|
|
2387
|
+
}
|
|
2272
2388
|
// The per-batch selection scratch: batches are bounded by DEFAULT_BATCH_ROWS, spills may use
|
|
2273
2389
|
// larger pages, so the scratch grows to the largest batch seen and is trivially small.
|
|
2274
2390
|
let selectionScratch = new Uint32Array(DEFAULT_BATCH_ROWS);
|
|
@@ -2289,6 +2405,9 @@ function applyPredicateKernel(plan, predicate, batch, selection, survivors) {
|
|
|
2289
2405
|
if (predicate.dictionaryLike !== undefined) {
|
|
2290
2406
|
return filterDictionaryLike(predicate.dictionaryLike, batch, selection, survivors);
|
|
2291
2407
|
}
|
|
2408
|
+
if (predicate.dictionaryNumeric !== undefined) {
|
|
2409
|
+
return filterDictionaryNumeric(predicate.dictionaryNumeric, batch, selection, survivors);
|
|
2410
|
+
}
|
|
2292
2411
|
if (predicate.disjunction !== undefined) {
|
|
2293
2412
|
return filterDisjunction(plan, predicate.disjunction, batch, selection, survivors);
|
|
2294
2413
|
}
|
|
@@ -3653,12 +3772,30 @@ function updateAggregatesFromValues(plan, state, values, memory) {
|
|
|
3653
3772
|
throw new Error("Spilled STRING_AGG order is invalid");
|
|
3654
3773
|
applyAggregateValue(spec, state, index, decoded[0], memory, decoded[1], encodedOrder.map(decodeSpillValue));
|
|
3655
3774
|
}
|
|
3775
|
+
else if (spec.name === "JSON_ARRAYAGG" &&
|
|
3776
|
+
(spec.orderBy?.length ?? 0) > 0 &&
|
|
3777
|
+
typeof value === "string") {
|
|
3778
|
+
const decoded = JSON.parse(value);
|
|
3779
|
+
if (!Array.isArray(decoded) || decoded.length !== 2 || !Array.isArray(decoded[1])) {
|
|
3780
|
+
throw new Error("Spilled JSON_ARRAYAGG input is invalid");
|
|
3781
|
+
}
|
|
3782
|
+
applyAggregateValue(spec, state, index, decodeSpillValue(decoded[0]), memory, undefined, decoded[1].map(decodeSpillValue));
|
|
3783
|
+
}
|
|
3656
3784
|
else {
|
|
3657
3785
|
applyAggregateValue(spec, state, index, value, memory);
|
|
3658
3786
|
}
|
|
3659
3787
|
}
|
|
3660
3788
|
}
|
|
3661
3789
|
function applyAggregateValue(spec, state, index, value, memory, delimiter, orderValues = []) {
|
|
3790
|
+
if (spec.name === "MINNOW_SINGLE_VALUE") {
|
|
3791
|
+
const count = (state.counts[index] ?? 0) + 1;
|
|
3792
|
+
state.counts[index] = count;
|
|
3793
|
+
if (count > 1) {
|
|
3794
|
+
throw new TypeError(`A scalar subquery returned ${String(count)} rows`);
|
|
3795
|
+
}
|
|
3796
|
+
replaceAggregateValue(state, index, asQueryValue(value ?? null), "Scalar subquery value", memory);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3662
3799
|
if (spec.name === "JSON_ARRAYAGG") {
|
|
3663
3800
|
const member = asQueryValue(value ?? null);
|
|
3664
3801
|
if (spec.distinct === true && !firstOfItsKind(state, index, member, memory))
|
|
@@ -3670,8 +3807,14 @@ function applyAggregateValue(spec, state, index, value, memory, delimiter, order
|
|
|
3670
3807
|
list = [];
|
|
3671
3808
|
lists[index] = list;
|
|
3672
3809
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3810
|
+
const retained = (spec.orderBy?.length ?? 0) === 0
|
|
3811
|
+
? member
|
|
3812
|
+
: JSON.stringify([
|
|
3813
|
+
encodeSpillValue(member),
|
|
3814
|
+
orderValues.map((orderValue) => encodeSpillValue(orderValue)),
|
|
3815
|
+
]);
|
|
3816
|
+
list.push(retained);
|
|
3817
|
+
memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(retained), "JSON_ARRAYAGG member"), "JSON_ARRAYAGG member");
|
|
3675
3818
|
return;
|
|
3676
3819
|
}
|
|
3677
3820
|
if (spec.name === "STRING_AGG") {
|
|
@@ -3811,7 +3954,38 @@ function evaluateFinalExpression(plan, expression, group) {
|
|
|
3811
3954
|
if (count === 0)
|
|
3812
3955
|
return null;
|
|
3813
3956
|
if (expression.name === "JSON_ARRAYAGG") {
|
|
3814
|
-
|
|
3957
|
+
const retained = required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? [];
|
|
3958
|
+
const orderBy = plan.aggregates[aggregateIndex]?.orderBy ?? [];
|
|
3959
|
+
const members = retained.map((encoded) => {
|
|
3960
|
+
if (orderBy.length === 0)
|
|
3961
|
+
return { value: encoded, order: [] };
|
|
3962
|
+
if (typeof encoded !== "string")
|
|
3963
|
+
throw new Error("JSON_ARRAYAGG member is invalid");
|
|
3964
|
+
const pair = JSON.parse(encoded);
|
|
3965
|
+
if (!Array.isArray(pair) || pair.length !== 2 || !Array.isArray(pair[1])) {
|
|
3966
|
+
throw new Error("JSON_ARRAYAGG member is invalid");
|
|
3967
|
+
}
|
|
3968
|
+
return {
|
|
3969
|
+
value: decodeSpillValue(pair[0]),
|
|
3970
|
+
order: pair[1].map(decodeSpillValue),
|
|
3971
|
+
};
|
|
3972
|
+
});
|
|
3973
|
+
if (orderBy.length > 0) {
|
|
3974
|
+
members.sort((left, right) => {
|
|
3975
|
+
for (const [index, order] of orderBy.entries()) {
|
|
3976
|
+
const a = left.order[index];
|
|
3977
|
+
const b = right.order[index];
|
|
3978
|
+
const placed = nullOrder(a, b, order.nulls, order.direction);
|
|
3979
|
+
if (placed !== undefined && placed !== 0)
|
|
3980
|
+
return placed;
|
|
3981
|
+
const compared = compareValues(a, b);
|
|
3982
|
+
if (compared !== 0)
|
|
3983
|
+
return order.direction === "desc" ? -compared : compared;
|
|
3984
|
+
}
|
|
3985
|
+
return 0;
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
return preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", members.map(({ value }) => value)));
|
|
3815
3989
|
}
|
|
3816
3990
|
if (expression.name === "STRING_AGG") {
|
|
3817
3991
|
const members = required(group.lists, "STRING_AGG list state is missing")[aggregateIndex] ?? [];
|
|
@@ -3882,7 +4056,7 @@ function projectBatchRow(plan, batch, row) {
|
|
|
3882
4056
|
}
|
|
3883
4057
|
return result;
|
|
3884
4058
|
}
|
|
3885
|
-
const multiple = plan.sourceTables.length > 1;
|
|
4059
|
+
const multiple = plan.sourceTables.filter((table) => [...table.columns.keys()].some((name) => !name.startsWith("\0"))).length > 1;
|
|
3886
4060
|
for (let source = 0; source < plan.sourceTables.length; source += 1) {
|
|
3887
4061
|
const table = required(plan.sourceTables[source], "Wildcard source table is missing");
|
|
3888
4062
|
const rowIndex = batch.rowsBySource[source]?.[row] ?? -1;
|
|
@@ -3901,7 +4075,7 @@ function projectBatchRow(plan, batch, row) {
|
|
|
3901
4075
|
return result;
|
|
3902
4076
|
}
|
|
3903
4077
|
function wildcardColumnNames(plan) {
|
|
3904
|
-
const multiple = plan.sourceTables.length > 1;
|
|
4078
|
+
const multiple = plan.sourceTables.filter((table) => [...table.columns.keys()].some((name) => !name.startsWith("\0"))).length > 1;
|
|
3905
4079
|
return plan.sourceTables.flatMap((table, source) => [...table.columns.keys()]
|
|
3906
4080
|
.filter((name) => !name.startsWith("\0"))
|
|
3907
4081
|
.map((name) => (multiple ? `${plan.sourceAliases[source] ?? ""}.${name}` : name)));
|
|
@@ -4116,6 +4290,143 @@ function dictionaryLikeMatches(dictionary, pattern, caseInsensitive, escape) {
|
|
|
4116
4290
|
}
|
|
4117
4291
|
return matches;
|
|
4118
4292
|
}
|
|
4293
|
+
const exactNumericDictionaryCache = new WeakMap();
|
|
4294
|
+
const dictionaryNumericCache = new WeakMap();
|
|
4295
|
+
function isExactNumericDictionary(dictionary) {
|
|
4296
|
+
const cached = exactNumericDictionaryCache.get(dictionary);
|
|
4297
|
+
if (cached !== undefined)
|
|
4298
|
+
return cached;
|
|
4299
|
+
const exact = dictionary.every((value) => isExactNumeric(value));
|
|
4300
|
+
exactNumericDictionaryCache.set(dictionary, exact);
|
|
4301
|
+
return exact;
|
|
4302
|
+
}
|
|
4303
|
+
function dictionaryNumericMatches(comparison, dictionary) {
|
|
4304
|
+
// An ordinary TEXT dictionary must retain the generic evaluator, including its error and
|
|
4305
|
+
// coercion behavior. Physical NUMERIC dictionaries contain only validated internal tags.
|
|
4306
|
+
if (!isExactNumericDictionary(dictionary))
|
|
4307
|
+
return undefined;
|
|
4308
|
+
let comparisons = dictionaryNumericCache.get(dictionary);
|
|
4309
|
+
if (comparisons === undefined) {
|
|
4310
|
+
comparisons = new Map();
|
|
4311
|
+
dictionaryNumericCache.set(dictionary, comparisons);
|
|
4312
|
+
}
|
|
4313
|
+
let matches = comparisons.get(comparison.signature);
|
|
4314
|
+
if (matches === undefined) {
|
|
4315
|
+
matches = new Uint8Array(dictionary.length);
|
|
4316
|
+
for (let code = 0; code < dictionary.length; code += 1) {
|
|
4317
|
+
matches[code] = comparisonValue(comparison.operator, comparison.expression.evaluate(dictionary[code] ?? ""), comparison.target)
|
|
4318
|
+
? 1
|
|
4319
|
+
: 0;
|
|
4320
|
+
}
|
|
4321
|
+
// The dictionary owns this cache weakly. Bound the number of query shapes retained by one
|
|
4322
|
+
// high-cardinality NUMERIC column, matching the dictionary LIKE cache above.
|
|
4323
|
+
if (comparisons.size >= 32)
|
|
4324
|
+
comparisons.clear();
|
|
4325
|
+
comparisons.set(comparison.signature, matches);
|
|
4326
|
+
}
|
|
4327
|
+
return matches;
|
|
4328
|
+
}
|
|
4329
|
+
function constantBoundExpressionValue(expression) {
|
|
4330
|
+
if (expression.kind === "literal")
|
|
4331
|
+
return { value: expression.value };
|
|
4332
|
+
if (expression.kind === "binary") {
|
|
4333
|
+
const left = constantBoundExpressionValue(expression.left);
|
|
4334
|
+
const right = constantBoundExpressionValue(expression.right);
|
|
4335
|
+
if (left === undefined || right === undefined)
|
|
4336
|
+
return undefined;
|
|
4337
|
+
try {
|
|
4338
|
+
return { value: binaryValue(expression.operator, left.value, right.value) };
|
|
4339
|
+
}
|
|
4340
|
+
catch {
|
|
4341
|
+
return undefined;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
// CAST is the only scalar wrapper needed by exact-NUMERIC arithmetic. Keeping the constant
|
|
4345
|
+
// folder deliberately narrow avoids changing error timing for general scalar expressions.
|
|
4346
|
+
if (expression.kind !== "call" || expression.name !== "CAST")
|
|
4347
|
+
return undefined;
|
|
4348
|
+
const values = [];
|
|
4349
|
+
for (const argument of expression.arguments) {
|
|
4350
|
+
const constant = constantBoundExpressionValue(argument);
|
|
4351
|
+
if (constant === undefined)
|
|
4352
|
+
return undefined;
|
|
4353
|
+
values.push(constant.value);
|
|
4354
|
+
}
|
|
4355
|
+
try {
|
|
4356
|
+
return { value: scalarFunctionValue(expression.name, values) };
|
|
4357
|
+
}
|
|
4358
|
+
catch {
|
|
4359
|
+
return undefined;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
function numericComparisonConstant(expression) {
|
|
4363
|
+
const constant = constantBoundExpressionValue(expression)?.value;
|
|
4364
|
+
if (typeof constant === "number" && Number.isFinite(constant))
|
|
4365
|
+
return constant;
|
|
4366
|
+
return isExactNumeric(constant) ? constant : undefined;
|
|
4367
|
+
}
|
|
4368
|
+
function dictionaryNumericExpression(expression) {
|
|
4369
|
+
if (expression.kind === "column" && expression.vector.kind === "string") {
|
|
4370
|
+
return {
|
|
4371
|
+
source: expression.source,
|
|
4372
|
+
vector: expression.vector,
|
|
4373
|
+
signature: expression.signature,
|
|
4374
|
+
evaluate: (value) => value,
|
|
4375
|
+
};
|
|
4376
|
+
}
|
|
4377
|
+
if (expression.kind !== "binary")
|
|
4378
|
+
return undefined;
|
|
4379
|
+
const sides = [
|
|
4380
|
+
{ column: expression.left, constant: expression.right, columnFirst: true },
|
|
4381
|
+
{ column: expression.right, constant: expression.left, columnFirst: false },
|
|
4382
|
+
];
|
|
4383
|
+
for (const { column, constant, columnFirst } of sides) {
|
|
4384
|
+
if (column.kind !== "column" || column.vector.kind !== "string")
|
|
4385
|
+
continue;
|
|
4386
|
+
const value = numericComparisonConstant(constant);
|
|
4387
|
+
if (value === undefined)
|
|
4388
|
+
continue;
|
|
4389
|
+
return {
|
|
4390
|
+
source: column.source,
|
|
4391
|
+
vector: column.vector,
|
|
4392
|
+
signature: expression.signature,
|
|
4393
|
+
evaluate: columnFirst
|
|
4394
|
+
? (dictionaryValue) => binaryValue(expression.operator, dictionaryValue, value)
|
|
4395
|
+
: (dictionaryValue) => binaryValue(expression.operator, value, dictionaryValue),
|
|
4396
|
+
};
|
|
4397
|
+
}
|
|
4398
|
+
return undefined;
|
|
4399
|
+
}
|
|
4400
|
+
/** Detects exact-NUMERIC dictionary arithmetic compared with a constant. */
|
|
4401
|
+
function detectDictionaryNumericComparison(predicate) {
|
|
4402
|
+
if (!primitiveOperators.has(predicate.operator))
|
|
4403
|
+
return undefined;
|
|
4404
|
+
const comparison = predicate.operator;
|
|
4405
|
+
const sides = [
|
|
4406
|
+
{ expression: predicate.left, target: predicate.right, operator: comparison },
|
|
4407
|
+
{
|
|
4408
|
+
expression: predicate.right,
|
|
4409
|
+
target: predicate.left,
|
|
4410
|
+
operator: reverseComparisonOperator(comparison),
|
|
4411
|
+
},
|
|
4412
|
+
];
|
|
4413
|
+
for (const { expression, target, operator } of sides) {
|
|
4414
|
+
const numeric = dictionaryNumericExpression(expression);
|
|
4415
|
+
if (numeric === undefined)
|
|
4416
|
+
continue;
|
|
4417
|
+
const value = numericComparisonConstant(target);
|
|
4418
|
+
if (value === undefined)
|
|
4419
|
+
continue;
|
|
4420
|
+
return {
|
|
4421
|
+
expression: numeric,
|
|
4422
|
+
operator,
|
|
4423
|
+
target: value,
|
|
4424
|
+
signature: `${numeric.signature}\u0001${operator}\u0001${target.signature}`,
|
|
4425
|
+
cache: { dictionary: undefined, matches: new Uint8Array(0) },
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4428
|
+
return undefined;
|
|
4429
|
+
}
|
|
4119
4430
|
function stringCodeAt(vector, rowIndex) {
|
|
4120
4431
|
if (rowIndex < 0 || rowIndex >= vector.length)
|
|
4121
4432
|
return undefined;
|
|
@@ -4299,6 +4610,21 @@ function evaluateBatchPredicate(plan, predicate, batch, row) {
|
|
|
4299
4610
|
const matched = like.cache.matches[code] === 1;
|
|
4300
4611
|
return like.negated ? !matched : matched;
|
|
4301
4612
|
}
|
|
4613
|
+
const dictionaryNumeric = predicate.dictionaryNumeric;
|
|
4614
|
+
if (dictionaryNumeric !== undefined) {
|
|
4615
|
+
const vector = dictionaryNumeric.expression.vector;
|
|
4616
|
+
if (dictionaryNumeric.cache.dictionary !== vector.dictionary) {
|
|
4617
|
+
const matches = dictionaryNumericMatches(dictionaryNumeric, vector.dictionary);
|
|
4618
|
+
if (matches !== undefined) {
|
|
4619
|
+
dictionaryNumeric.cache.dictionary = vector.dictionary;
|
|
4620
|
+
dictionaryNumeric.cache.matches = matches;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
if (dictionaryNumeric.cache.dictionary === vector.dictionary) {
|
|
4624
|
+
const code = stringCodeAt(vector, batch.rowsBySource[dictionaryNumeric.expression.source]?.[row] ?? -1);
|
|
4625
|
+
return code !== undefined && dictionaryNumeric.cache.matches[code] === 1;
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4302
4628
|
if (predicate.operator === "IS TRUE" ||
|
|
4303
4629
|
predicate.operator === "LIKE" ||
|
|
4304
4630
|
predicate.operator === "NOT LIKE" ||
|