@minnowdb/core 0.5.0 → 0.6.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 +1 -1
- 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 +516 -74
- 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.js +540 -38
- package/dist/engine/query.d.ts +5 -278
- package/dist/engine/query.js +78 -32
- 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 +301 -39
- 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 +218 -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/sql-feature-matrix.json +69 -19
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,
|
|
@@ -1217,6 +1235,7 @@ function executeBoundPlan(plan, memory) {
|
|
|
1217
1235
|
return finishResult(plan, rows, memory);
|
|
1218
1236
|
}
|
|
1219
1237
|
async function executeBoundPlanAsync(plan, memory, options) {
|
|
1238
|
+
throwIfAborted(options.signal);
|
|
1220
1239
|
const metadataCount = executeMetadataCount(plan, memory);
|
|
1221
1240
|
if (metadataCount !== undefined)
|
|
1222
1241
|
return metadataCount;
|
|
@@ -1224,11 +1243,13 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1224
1243
|
const output = new ResultSink(plan, memory, options.loadScanWindow === undefined);
|
|
1225
1244
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
1226
1245
|
for (let start = 0; start < scanRows;) {
|
|
1246
|
+
throwIfAborted(options.signal);
|
|
1227
1247
|
let length = Math.min(DEFAULT_BATCH_ROWS, scanRows - start);
|
|
1228
1248
|
// The loader answers synchronously when the batch is already resident — the common case,
|
|
1229
1249
|
// every batch but the first per block — so the scan loop only pays await on real slides.
|
|
1230
1250
|
const loaded = options.loadScanWindow?.(start, length);
|
|
1231
1251
|
const residentEnd = typeof loaded === "number" || loaded === undefined ? loaded : await loaded;
|
|
1252
|
+
throwIfAborted(options.signal);
|
|
1232
1253
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1233
1254
|
length = Math.min(length, residentEnd - start);
|
|
1234
1255
|
}
|
|
@@ -1250,6 +1271,7 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1250
1271
|
const ranges = narrowed.ranges ?? [{ begin: narrowed.begin, end: narrowed.end }];
|
|
1251
1272
|
for (const range of ranges) {
|
|
1252
1273
|
for (let row = range.begin; row < range.end; row += DEFAULT_BATCH_ROWS) {
|
|
1274
|
+
throwIfAborted(options.signal);
|
|
1253
1275
|
const rows = Math.min(DEFAULT_BATCH_ROWS, range.end - row);
|
|
1254
1276
|
if (runScanBatch(plan, row, rows, groups, output, memory)) {
|
|
1255
1277
|
stopped = true;
|
|
@@ -1263,6 +1285,7 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1263
1285
|
break;
|
|
1264
1286
|
start = windowEnd;
|
|
1265
1287
|
}
|
|
1288
|
+
throwIfAborted(options.signal);
|
|
1266
1289
|
const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
|
|
1267
1290
|
return finishResult(plan, rows, memory);
|
|
1268
1291
|
}
|
|
@@ -1275,24 +1298,44 @@ async function executeBoundPlanBatches(plan, memory, options, consume) {
|
|
|
1275
1298
|
const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
|
|
1276
1299
|
const { limit, offset, ...unbounded } = plan;
|
|
1277
1300
|
const scanPlan = unbounded;
|
|
1301
|
+
const consumeFirstColumn = options.consumeFirstColumn;
|
|
1302
|
+
const firstExpression = consumeFirstColumn === undefined ? undefined : scanPlan.select[0]?.expression;
|
|
1278
1303
|
let skipped = 0;
|
|
1279
1304
|
let emitted = 0;
|
|
1280
1305
|
const scanRows = scanPlan.sourceTables[scanPlan.scanSource]?.rowCount ?? 0;
|
|
1281
1306
|
const step = Math.min(DEFAULT_BATCH_ROWS, options.batchRows);
|
|
1282
1307
|
for (let start = 0; start < scanRows && (limit === undefined || emitted < limit);) {
|
|
1283
|
-
options.signal
|
|
1308
|
+
throwIfAborted(options.signal);
|
|
1284
1309
|
let length = Math.min(step, scanRows - start);
|
|
1285
1310
|
const loaded = options.loadScanWindow?.(start, length);
|
|
1286
1311
|
const residentEnd = typeof loaded === "number" || loaded === undefined ? loaded : await loaded;
|
|
1287
|
-
options.signal
|
|
1312
|
+
throwIfAborted(options.signal);
|
|
1288
1313
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1289
1314
|
length = Math.min(length, residentEnd - start);
|
|
1290
1315
|
}
|
|
1291
1316
|
const pageMemory = memory.createChild();
|
|
1292
1317
|
try {
|
|
1293
1318
|
const groups = new GroupAccumulator(scanPlan, pageMemory);
|
|
1294
|
-
const
|
|
1319
|
+
const values = [];
|
|
1320
|
+
const output = firstExpression === undefined
|
|
1321
|
+
? new ResultSink(scanPlan, pageMemory, options.loadScanWindow === undefined)
|
|
1322
|
+
: {
|
|
1323
|
+
get size() {
|
|
1324
|
+
return values.length;
|
|
1325
|
+
},
|
|
1326
|
+
tryAddBatch: () => false,
|
|
1327
|
+
add: (batch, row) => {
|
|
1328
|
+
values.push(asQueryValue(evaluateBatchExpression(scanPlan, firstExpression, batch, row)));
|
|
1329
|
+
},
|
|
1330
|
+
finish: () => [],
|
|
1331
|
+
};
|
|
1295
1332
|
runScanBatch(scanPlan, start, length, groups, output, pageMemory);
|
|
1333
|
+
if (firstExpression !== undefined && consumeFirstColumn !== undefined) {
|
|
1334
|
+
if (values.length > 0)
|
|
1335
|
+
await consumeFirstColumn(values);
|
|
1336
|
+
start += length;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1296
1339
|
let rows = output.finish();
|
|
1297
1340
|
const remainingOffset = Math.max(0, (offset ?? 0) - skipped);
|
|
1298
1341
|
if (remainingOffset > 0) {
|
|
@@ -1323,6 +1366,7 @@ function createSpillOwnerId() {
|
|
|
1323
1366
|
return `query-${globalThis.crypto.randomUUID()}`;
|
|
1324
1367
|
}
|
|
1325
1368
|
async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
1369
|
+
throwIfAborted(options.signal);
|
|
1326
1370
|
const store = required(options.spillStore, "Query spill store is missing");
|
|
1327
1371
|
const pageRows = boundedSpillPageRows(options.spillPageRows);
|
|
1328
1372
|
const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
|
|
@@ -1333,9 +1377,11 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1333
1377
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
1334
1378
|
const scanBatchRows = Math.min(DEFAULT_BATCH_ROWS, pageRows);
|
|
1335
1379
|
for (let start = 0; start < scanRows;) {
|
|
1380
|
+
throwIfAborted(options.signal);
|
|
1336
1381
|
let length = Math.min(scanBatchRows, scanRows - start);
|
|
1337
1382
|
const loadedSort = options.loadScanWindow?.(start, length);
|
|
1338
1383
|
const residentEnd = typeof loadedSort === "number" || loadedSort === undefined ? loadedSort : await loadedSort;
|
|
1384
|
+
throwIfAborted(options.signal);
|
|
1339
1385
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1340
1386
|
length = Math.min(length, residentEnd - start);
|
|
1341
1387
|
}
|
|
@@ -1351,6 +1397,7 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1351
1397
|
for (let index = 0; index < length; index += 1)
|
|
1352
1398
|
scan[index] = start + index;
|
|
1353
1399
|
await spillJoinedBatches(plan, { length, rowsBySource: sourceRows, memory: batchMemory }, 0, memory, async (batch) => {
|
|
1400
|
+
throwIfAborted(options.signal);
|
|
1354
1401
|
const outputMemory = memory.createChild();
|
|
1355
1402
|
try {
|
|
1356
1403
|
const rows = [];
|
|
@@ -1365,13 +1412,13 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1365
1412
|
ordering.release();
|
|
1366
1413
|
}
|
|
1367
1414
|
const runId = `run-${String(runSequence++)}`;
|
|
1368
|
-
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows);
|
|
1415
|
+
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows, options.signal);
|
|
1369
1416
|
runs.push({ id: runId, pageCount });
|
|
1370
1417
|
}
|
|
1371
1418
|
finally {
|
|
1372
1419
|
outputMemory.close();
|
|
1373
1420
|
}
|
|
1374
|
-
});
|
|
1421
|
+
}, options.signal);
|
|
1375
1422
|
}
|
|
1376
1423
|
finally {
|
|
1377
1424
|
batchMemory.close();
|
|
@@ -1382,8 +1429,10 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1382
1429
|
return { columns, columnDomains: unknownColumnDomains(columns), rows: [] };
|
|
1383
1430
|
let active = runs;
|
|
1384
1431
|
while (active.length > 1) {
|
|
1432
|
+
throwIfAborted(options.signal);
|
|
1385
1433
|
const merged = [];
|
|
1386
1434
|
for (let index = 0; index < active.length; index += 2) {
|
|
1435
|
+
throwIfAborted(options.signal);
|
|
1387
1436
|
const left = required(active[index], "Left spill run is missing");
|
|
1388
1437
|
const right = active[index + 1];
|
|
1389
1438
|
if (right === undefined) {
|
|
@@ -1391,9 +1440,10 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1391
1440
|
continue;
|
|
1392
1441
|
}
|
|
1393
1442
|
const outputId = `merge-${String(runSequence++)}`;
|
|
1394
|
-
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, plan.orderBy, pageRows, memory));
|
|
1443
|
+
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, plan.orderBy, pageRows, memory, options.signal));
|
|
1395
1444
|
await store.removeRun(ownerId, left.id);
|
|
1396
1445
|
await store.removeRun(ownerId, right.id);
|
|
1446
|
+
throwIfAborted(options.signal);
|
|
1397
1447
|
}
|
|
1398
1448
|
active = merged;
|
|
1399
1449
|
}
|
|
@@ -1402,7 +1452,9 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1402
1452
|
const offset = plan.offset ?? 0;
|
|
1403
1453
|
const limit = plan.limit === undefined ? Number.MAX_SAFE_INTEGER : plan.limit + offset;
|
|
1404
1454
|
for (let pageIndex = 0; pageIndex < finalRun.pageCount && rows.length < limit; pageIndex += 1) {
|
|
1455
|
+
throwIfAborted(options.signal);
|
|
1405
1456
|
const bytes = await store.getPage(ownerId, finalRun.id, pageIndex);
|
|
1457
|
+
throwIfAborted(options.signal);
|
|
1406
1458
|
if (bytes === undefined)
|
|
1407
1459
|
throw new Error("Query spill page is missing");
|
|
1408
1460
|
for (const row of decodeSpillRows(columns, bytes)) {
|
|
@@ -1420,6 +1472,7 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
|
|
|
1420
1472
|
}
|
|
1421
1473
|
}
|
|
1422
1474
|
async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
1475
|
+
throwIfAborted(options.signal);
|
|
1423
1476
|
const store = required(options.spillStore, "Query spill store is missing");
|
|
1424
1477
|
const pageRows = boundedSpillPageRows(options.spillPageRows);
|
|
1425
1478
|
const partitionCount = 64;
|
|
@@ -1436,9 +1489,11 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1436
1489
|
// page size while staying coarse enough to amortize partition-page write transactions.
|
|
1437
1490
|
const scanChunkRows = Math.min(DEFAULT_BATCH_ROWS, HASH_SPILL_SCAN_CHUNK_ROWS);
|
|
1438
1491
|
for (let start = 0; start < scanRows;) {
|
|
1492
|
+
throwIfAborted(options.signal);
|
|
1439
1493
|
let length = Math.min(scanChunkRows, scanRows - start);
|
|
1440
1494
|
const loadedHash = options.loadScanWindow?.(start, length);
|
|
1441
1495
|
const residentEnd = typeof loadedHash === "number" || loadedHash === undefined ? loadedHash : await loadedHash;
|
|
1496
|
+
throwIfAborted(options.signal);
|
|
1442
1497
|
if (typeof residentEnd === "number" && residentEnd > start) {
|
|
1443
1498
|
length = Math.min(length, residentEnd - start);
|
|
1444
1499
|
}
|
|
@@ -1458,6 +1513,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1458
1513
|
// Each surviving row spills its evaluated group keys and aggregate arguments, so the
|
|
1459
1514
|
// partition phase never re-reads source vectors and the scan source may be windowed.
|
|
1460
1515
|
async (batch) => {
|
|
1516
|
+
throwIfAborted(options.signal);
|
|
1461
1517
|
for (let row = 0; row < batch.length; row += 1) {
|
|
1462
1518
|
if (!plan.predicates.every((predicate) => evaluateBatchPredicate(plan, predicate, batch, row))) {
|
|
1463
1519
|
continue;
|
|
@@ -1491,7 +1547,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1491
1547
|
rows.push(spillRow);
|
|
1492
1548
|
partitionBuffers.set(partition, rows);
|
|
1493
1549
|
}
|
|
1494
|
-
});
|
|
1550
|
+
}, options.signal);
|
|
1495
1551
|
const flush = [];
|
|
1496
1552
|
let flushBytes = 0;
|
|
1497
1553
|
for (const [partition, rows] of partitionBuffers) {
|
|
@@ -1499,7 +1555,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1499
1555
|
if (flush.length > 0 &&
|
|
1500
1556
|
(flush.length === SPILL_WRITE_BATCH_PAGES ||
|
|
1501
1557
|
flushBytes + bytes.byteLength > MAX_TEMP_RUN_BATCH_BYTES)) {
|
|
1502
|
-
await writeSpillPages(store, flush);
|
|
1558
|
+
await writeSpillPages(store, flush, options.signal);
|
|
1503
1559
|
flush.length = 0;
|
|
1504
1560
|
flushBytes = 0;
|
|
1505
1561
|
}
|
|
@@ -1514,7 +1570,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1514
1570
|
partitionPages[partition] = pageIndex + 1;
|
|
1515
1571
|
}
|
|
1516
1572
|
}
|
|
1517
|
-
await writeSpillPages(store, flush);
|
|
1573
|
+
await writeSpillPages(store, flush, options.signal);
|
|
1518
1574
|
}
|
|
1519
1575
|
finally {
|
|
1520
1576
|
batchMemory.close();
|
|
@@ -1523,6 +1579,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1523
1579
|
}
|
|
1524
1580
|
const runs = [];
|
|
1525
1581
|
for (let partition = 0; partition < partitionCount; partition += 1) {
|
|
1582
|
+
throwIfAborted(options.signal);
|
|
1526
1583
|
const sourcePageCount = partitionPages[partition] ?? 0;
|
|
1527
1584
|
if (sourcePageCount === 0)
|
|
1528
1585
|
continue;
|
|
@@ -1530,7 +1587,9 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1530
1587
|
try {
|
|
1531
1588
|
const groups = new ByteGroupIndex(partitionMemory);
|
|
1532
1589
|
for (let pageIndex = 0; pageIndex < sourcePageCount; pageIndex += 1) {
|
|
1590
|
+
throwIfAborted(options.signal);
|
|
1533
1591
|
const bytes = await store.getPage(ownerId, `partition-${String(partition)}`, pageIndex);
|
|
1592
|
+
throwIfAborted(options.signal);
|
|
1534
1593
|
if (bytes === undefined)
|
|
1535
1594
|
throw new Error("Query hash spill page is missing");
|
|
1536
1595
|
const pageMemory = partitionMemory.createChild();
|
|
@@ -1559,7 +1618,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1559
1618
|
}
|
|
1560
1619
|
}
|
|
1561
1620
|
const runId = `group-${String(runSequence++)}`;
|
|
1562
|
-
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows);
|
|
1621
|
+
const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows, options.signal);
|
|
1563
1622
|
runs.push({ id: runId, pageCount });
|
|
1564
1623
|
}
|
|
1565
1624
|
finally {
|
|
@@ -1569,9 +1628,9 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1569
1628
|
}
|
|
1570
1629
|
if (runs.length === 0)
|
|
1571
1630
|
return { columns, columnDomains: unknownColumnDomains(columns), rows: [] };
|
|
1572
|
-
const finalRun = await mergeAllSpillRuns(store, ownerId, runs, columns, plan.orderBy, pageRows, () => `merge-${String(runSequence++)}`, memory);
|
|
1631
|
+
const finalRun = await mergeAllSpillRuns(store, ownerId, runs, columns, plan.orderBy, pageRows, () => `merge-${String(runSequence++)}`, memory, options.signal);
|
|
1573
1632
|
const spillOffset = plan.offset ?? 0;
|
|
1574
|
-
const result = await readFinalSpillRun(store, ownerId, finalRun, columns, plan.limit === undefined ? undefined : plan.limit + spillOffset);
|
|
1633
|
+
const result = await readFinalSpillRun(store, ownerId, finalRun, columns, plan.limit === undefined ? undefined : plan.limit + spillOffset, options.signal);
|
|
1575
1634
|
if (spillOffset > 0)
|
|
1576
1635
|
result.rows.splice(0, Math.min(spillOffset, result.rows.length));
|
|
1577
1636
|
return result;
|
|
@@ -1580,11 +1639,13 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1580
1639
|
await store.removeOwner(ownerId);
|
|
1581
1640
|
}
|
|
1582
1641
|
}
|
|
1583
|
-
async function mergeAllSpillRuns(store, ownerId, runs, columns, orderBy, pageRows, nextRunId, memory) {
|
|
1642
|
+
async function mergeAllSpillRuns(store, ownerId, runs, columns, orderBy, pageRows, nextRunId, memory, signal) {
|
|
1584
1643
|
let active = [...runs];
|
|
1585
1644
|
while (active.length > 1) {
|
|
1645
|
+
throwIfAborted(signal);
|
|
1586
1646
|
const merged = [];
|
|
1587
1647
|
for (let index = 0; index < active.length; index += 2) {
|
|
1648
|
+
throwIfAborted(signal);
|
|
1588
1649
|
const left = required(active[index], "Left spill run is missing");
|
|
1589
1650
|
const right = active[index + 1];
|
|
1590
1651
|
if (right === undefined) {
|
|
@@ -1592,19 +1653,22 @@ async function mergeAllSpillRuns(store, ownerId, runs, columns, orderBy, pageRow
|
|
|
1592
1653
|
continue;
|
|
1593
1654
|
}
|
|
1594
1655
|
const outputId = nextRunId();
|
|
1595
|
-
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory));
|
|
1656
|
+
merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory, signal));
|
|
1596
1657
|
await store.removeRun(ownerId, left.id);
|
|
1597
1658
|
await store.removeRun(ownerId, right.id);
|
|
1659
|
+
throwIfAborted(signal);
|
|
1598
1660
|
}
|
|
1599
1661
|
active = merged;
|
|
1600
1662
|
}
|
|
1601
1663
|
return required(active[0], "Final spill run is missing");
|
|
1602
1664
|
}
|
|
1603
|
-
async function readFinalSpillRun(store, ownerId, run, columns, requestedLimit) {
|
|
1665
|
+
async function readFinalSpillRun(store, ownerId, run, columns, requestedLimit, signal) {
|
|
1604
1666
|
const rows = [];
|
|
1605
1667
|
const limit = requestedLimit ?? Number.MAX_SAFE_INTEGER;
|
|
1606
1668
|
for (let pageIndex = 0; pageIndex < run.pageCount && rows.length < limit; pageIndex += 1) {
|
|
1669
|
+
throwIfAborted(signal);
|
|
1607
1670
|
const bytes = await store.getPage(ownerId, run.id, pageIndex);
|
|
1671
|
+
throwIfAborted(signal);
|
|
1608
1672
|
if (bytes === undefined) {
|
|
1609
1673
|
throw new Error(`Query spill page is missing: ${run.id}/${String(pageIndex)}`);
|
|
1610
1674
|
}
|
|
@@ -1651,15 +1715,17 @@ function hashQueryValues(values) {
|
|
|
1651
1715
|
}
|
|
1652
1716
|
return hash;
|
|
1653
1717
|
}
|
|
1654
|
-
async function spillJoinedBatches(plan, batch, joinIndex, memory, consume) {
|
|
1718
|
+
async function spillJoinedBatches(plan, batch, joinIndex, memory, consume, signal) {
|
|
1719
|
+
throwIfAborted(signal);
|
|
1655
1720
|
const join = plan.joins[joinIndex];
|
|
1656
1721
|
if (join === undefined) {
|
|
1657
1722
|
await consume(batch);
|
|
1658
1723
|
return;
|
|
1659
1724
|
}
|
|
1660
1725
|
for (const joined of joinBatches(plan, batch, join, memory)) {
|
|
1726
|
+
throwIfAborted(signal);
|
|
1661
1727
|
try {
|
|
1662
|
-
await spillJoinedBatches(plan, joined, joinIndex + 1, memory, consume);
|
|
1728
|
+
await spillJoinedBatches(plan, joined, joinIndex + 1, memory, consume, signal);
|
|
1663
1729
|
}
|
|
1664
1730
|
finally {
|
|
1665
1731
|
joined.memory?.close();
|
|
@@ -1686,25 +1752,27 @@ function passesPredicates(plan, batch, row) {
|
|
|
1686
1752
|
}
|
|
1687
1753
|
return true;
|
|
1688
1754
|
}
|
|
1689
|
-
async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory) {
|
|
1755
|
+
async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory, signal) {
|
|
1690
1756
|
const mergeMemory = memory.createChild();
|
|
1691
|
-
const leftReader = createSpillRunReader(store, ownerId, left, columns, mergeMemory);
|
|
1692
|
-
const rightReader = createSpillRunReader(store, ownerId, right, columns, mergeMemory);
|
|
1757
|
+
const leftReader = createSpillRunReader(store, ownerId, left, columns, mergeMemory, signal);
|
|
1758
|
+
const rightReader = createSpillRunReader(store, ownerId, right, columns, mergeMemory, signal);
|
|
1693
1759
|
let outputPage = [];
|
|
1694
1760
|
let outputMemory = mergeMemory.createChild();
|
|
1695
1761
|
let pageIndex = 0;
|
|
1696
1762
|
const flush = async () => {
|
|
1697
1763
|
if (outputPage.length === 0)
|
|
1698
1764
|
return;
|
|
1699
|
-
pageIndex += await writeSpillRowPages(store, ownerId, outputId, pageIndex, columns, outputPage, pageRows);
|
|
1765
|
+
pageIndex += await writeSpillRowPages(store, ownerId, outputId, pageIndex, columns, outputPage, pageRows, signal);
|
|
1700
1766
|
outputPage = [];
|
|
1701
1767
|
outputMemory.close();
|
|
1702
1768
|
outputMemory = mergeMemory.createChild();
|
|
1703
1769
|
};
|
|
1704
1770
|
try {
|
|
1771
|
+
throwIfAborted(signal);
|
|
1705
1772
|
let leftRow = await leftReader.next();
|
|
1706
1773
|
let rightRow = await rightReader.next();
|
|
1707
1774
|
while (leftRow !== undefined || rightRow !== undefined) {
|
|
1775
|
+
throwIfAborted(signal);
|
|
1708
1776
|
if (rightRow === undefined ||
|
|
1709
1777
|
(leftRow !== undefined && compareOrderedRows(leftRow, rightRow, orderBy) <= 0)) {
|
|
1710
1778
|
const row = required(leftRow, "Left spill row is missing");
|
|
@@ -1730,17 +1798,19 @@ async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, or
|
|
|
1730
1798
|
mergeMemory.close();
|
|
1731
1799
|
}
|
|
1732
1800
|
}
|
|
1733
|
-
function createSpillRunReader(store, ownerId, run, columns, memory) {
|
|
1801
|
+
function createSpillRunReader(store, ownerId, run, columns, memory, signal) {
|
|
1734
1802
|
let pageIndex = 0;
|
|
1735
1803
|
let rows = [];
|
|
1736
1804
|
let rowIndex = 0;
|
|
1737
1805
|
let pageReservation;
|
|
1738
1806
|
return {
|
|
1739
1807
|
async next() {
|
|
1808
|
+
throwIfAborted(signal);
|
|
1740
1809
|
while (rowIndex >= rows.length) {
|
|
1741
1810
|
if (pageIndex >= run.pageCount)
|
|
1742
1811
|
return undefined;
|
|
1743
1812
|
const bytes = await store.getPage(ownerId, run.id, pageIndex);
|
|
1813
|
+
throwIfAborted(signal);
|
|
1744
1814
|
if (bytes === undefined)
|
|
1745
1815
|
throw new Error("Query spill page is missing");
|
|
1746
1816
|
pageReservation?.release();
|
|
@@ -2269,6 +2339,43 @@ function filterDictionaryLike(fast, batch, selection, survivors) {
|
|
|
2269
2339
|
}
|
|
2270
2340
|
return kept;
|
|
2271
2341
|
}
|
|
2342
|
+
/** Compacts the selection using a match table computed once per exact-NUMERIC dictionary. */
|
|
2343
|
+
function filterDictionaryNumeric(fast, batch, selection, survivors) {
|
|
2344
|
+
const vector = fast.expression.vector;
|
|
2345
|
+
if (fast.cache.dictionary !== vector.dictionary) {
|
|
2346
|
+
const matches = dictionaryNumericMatches(fast, vector.dictionary);
|
|
2347
|
+
if (matches === undefined)
|
|
2348
|
+
return undefined;
|
|
2349
|
+
fast.cache.dictionary = vector.dictionary;
|
|
2350
|
+
fast.cache.matches = matches;
|
|
2351
|
+
}
|
|
2352
|
+
const matches = fast.cache.matches;
|
|
2353
|
+
const rows = batch.rowsBySource[fast.expression.source];
|
|
2354
|
+
const codes = vector.codes;
|
|
2355
|
+
const validity = vector.validity;
|
|
2356
|
+
const windowStart = vector.window?.start ?? 0;
|
|
2357
|
+
const slots = codes.length;
|
|
2358
|
+
const vectorLength = vector.length;
|
|
2359
|
+
let kept = 0;
|
|
2360
|
+
for (let index = 0; index < survivors; index += 1) {
|
|
2361
|
+
const row = selection[index] ?? 0;
|
|
2362
|
+
const sourceRow = rows?.[row] ?? -1;
|
|
2363
|
+
if (sourceRow < 0 || sourceRow >= vectorLength)
|
|
2364
|
+
continue;
|
|
2365
|
+
const slot = sourceRow - windowStart;
|
|
2366
|
+
if (slot < 0 || slot >= slots) {
|
|
2367
|
+
throw new RangeError("Streamed vector row is outside the resident window");
|
|
2368
|
+
}
|
|
2369
|
+
if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) === 0)
|
|
2370
|
+
continue;
|
|
2371
|
+
const code = codes[slot] ?? NULL_STRING_CODE;
|
|
2372
|
+
if (code === NULL_STRING_CODE || matches[code] !== 1)
|
|
2373
|
+
continue;
|
|
2374
|
+
selection[kept] = row;
|
|
2375
|
+
kept += 1;
|
|
2376
|
+
}
|
|
2377
|
+
return kept;
|
|
2378
|
+
}
|
|
2272
2379
|
// The per-batch selection scratch: batches are bounded by DEFAULT_BATCH_ROWS, spills may use
|
|
2273
2380
|
// larger pages, so the scratch grows to the largest batch seen and is trivially small.
|
|
2274
2381
|
let selectionScratch = new Uint32Array(DEFAULT_BATCH_ROWS);
|
|
@@ -2289,6 +2396,9 @@ function applyPredicateKernel(plan, predicate, batch, selection, survivors) {
|
|
|
2289
2396
|
if (predicate.dictionaryLike !== undefined) {
|
|
2290
2397
|
return filterDictionaryLike(predicate.dictionaryLike, batch, selection, survivors);
|
|
2291
2398
|
}
|
|
2399
|
+
if (predicate.dictionaryNumeric !== undefined) {
|
|
2400
|
+
return filterDictionaryNumeric(predicate.dictionaryNumeric, batch, selection, survivors);
|
|
2401
|
+
}
|
|
2292
2402
|
if (predicate.disjunction !== undefined) {
|
|
2293
2403
|
return filterDisjunction(plan, predicate.disjunction, batch, selection, survivors);
|
|
2294
2404
|
}
|
|
@@ -3811,7 +3921,7 @@ function evaluateFinalExpression(plan, expression, group) {
|
|
|
3811
3921
|
if (count === 0)
|
|
3812
3922
|
return null;
|
|
3813
3923
|
if (expression.name === "JSON_ARRAYAGG") {
|
|
3814
|
-
return
|
|
3924
|
+
return preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? []));
|
|
3815
3925
|
}
|
|
3816
3926
|
if (expression.name === "STRING_AGG") {
|
|
3817
3927
|
const members = required(group.lists, "STRING_AGG list state is missing")[aggregateIndex] ?? [];
|
|
@@ -3882,7 +3992,7 @@ function projectBatchRow(plan, batch, row) {
|
|
|
3882
3992
|
}
|
|
3883
3993
|
return result;
|
|
3884
3994
|
}
|
|
3885
|
-
const multiple = plan.sourceTables.length > 1;
|
|
3995
|
+
const multiple = plan.sourceTables.filter((table) => [...table.columns.keys()].some((name) => !name.startsWith("\0"))).length > 1;
|
|
3886
3996
|
for (let source = 0; source < plan.sourceTables.length; source += 1) {
|
|
3887
3997
|
const table = required(plan.sourceTables[source], "Wildcard source table is missing");
|
|
3888
3998
|
const rowIndex = batch.rowsBySource[source]?.[row] ?? -1;
|
|
@@ -3901,7 +4011,7 @@ function projectBatchRow(plan, batch, row) {
|
|
|
3901
4011
|
return result;
|
|
3902
4012
|
}
|
|
3903
4013
|
function wildcardColumnNames(plan) {
|
|
3904
|
-
const multiple = plan.sourceTables.length > 1;
|
|
4014
|
+
const multiple = plan.sourceTables.filter((table) => [...table.columns.keys()].some((name) => !name.startsWith("\0"))).length > 1;
|
|
3905
4015
|
return plan.sourceTables.flatMap((table, source) => [...table.columns.keys()]
|
|
3906
4016
|
.filter((name) => !name.startsWith("\0"))
|
|
3907
4017
|
.map((name) => (multiple ? `${plan.sourceAliases[source] ?? ""}.${name}` : name)));
|
|
@@ -4116,6 +4226,143 @@ function dictionaryLikeMatches(dictionary, pattern, caseInsensitive, escape) {
|
|
|
4116
4226
|
}
|
|
4117
4227
|
return matches;
|
|
4118
4228
|
}
|
|
4229
|
+
const exactNumericDictionaryCache = new WeakMap();
|
|
4230
|
+
const dictionaryNumericCache = new WeakMap();
|
|
4231
|
+
function isExactNumericDictionary(dictionary) {
|
|
4232
|
+
const cached = exactNumericDictionaryCache.get(dictionary);
|
|
4233
|
+
if (cached !== undefined)
|
|
4234
|
+
return cached;
|
|
4235
|
+
const exact = dictionary.every((value) => isExactNumeric(value));
|
|
4236
|
+
exactNumericDictionaryCache.set(dictionary, exact);
|
|
4237
|
+
return exact;
|
|
4238
|
+
}
|
|
4239
|
+
function dictionaryNumericMatches(comparison, dictionary) {
|
|
4240
|
+
// An ordinary TEXT dictionary must retain the generic evaluator, including its error and
|
|
4241
|
+
// coercion behavior. Physical NUMERIC dictionaries contain only validated internal tags.
|
|
4242
|
+
if (!isExactNumericDictionary(dictionary))
|
|
4243
|
+
return undefined;
|
|
4244
|
+
let comparisons = dictionaryNumericCache.get(dictionary);
|
|
4245
|
+
if (comparisons === undefined) {
|
|
4246
|
+
comparisons = new Map();
|
|
4247
|
+
dictionaryNumericCache.set(dictionary, comparisons);
|
|
4248
|
+
}
|
|
4249
|
+
let matches = comparisons.get(comparison.signature);
|
|
4250
|
+
if (matches === undefined) {
|
|
4251
|
+
matches = new Uint8Array(dictionary.length);
|
|
4252
|
+
for (let code = 0; code < dictionary.length; code += 1) {
|
|
4253
|
+
matches[code] = comparisonValue(comparison.operator, comparison.expression.evaluate(dictionary[code] ?? ""), comparison.target)
|
|
4254
|
+
? 1
|
|
4255
|
+
: 0;
|
|
4256
|
+
}
|
|
4257
|
+
// The dictionary owns this cache weakly. Bound the number of query shapes retained by one
|
|
4258
|
+
// high-cardinality NUMERIC column, matching the dictionary LIKE cache above.
|
|
4259
|
+
if (comparisons.size >= 32)
|
|
4260
|
+
comparisons.clear();
|
|
4261
|
+
comparisons.set(comparison.signature, matches);
|
|
4262
|
+
}
|
|
4263
|
+
return matches;
|
|
4264
|
+
}
|
|
4265
|
+
function constantBoundExpressionValue(expression) {
|
|
4266
|
+
if (expression.kind === "literal")
|
|
4267
|
+
return { value: expression.value };
|
|
4268
|
+
if (expression.kind === "binary") {
|
|
4269
|
+
const left = constantBoundExpressionValue(expression.left);
|
|
4270
|
+
const right = constantBoundExpressionValue(expression.right);
|
|
4271
|
+
if (left === undefined || right === undefined)
|
|
4272
|
+
return undefined;
|
|
4273
|
+
try {
|
|
4274
|
+
return { value: binaryValue(expression.operator, left.value, right.value) };
|
|
4275
|
+
}
|
|
4276
|
+
catch {
|
|
4277
|
+
return undefined;
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
// CAST is the only scalar wrapper needed by exact-NUMERIC arithmetic. Keeping the constant
|
|
4281
|
+
// folder deliberately narrow avoids changing error timing for general scalar expressions.
|
|
4282
|
+
if (expression.kind !== "call" || expression.name !== "CAST")
|
|
4283
|
+
return undefined;
|
|
4284
|
+
const values = [];
|
|
4285
|
+
for (const argument of expression.arguments) {
|
|
4286
|
+
const constant = constantBoundExpressionValue(argument);
|
|
4287
|
+
if (constant === undefined)
|
|
4288
|
+
return undefined;
|
|
4289
|
+
values.push(constant.value);
|
|
4290
|
+
}
|
|
4291
|
+
try {
|
|
4292
|
+
return { value: scalarFunctionValue(expression.name, values) };
|
|
4293
|
+
}
|
|
4294
|
+
catch {
|
|
4295
|
+
return undefined;
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
function numericComparisonConstant(expression) {
|
|
4299
|
+
const constant = constantBoundExpressionValue(expression)?.value;
|
|
4300
|
+
if (typeof constant === "number" && Number.isFinite(constant))
|
|
4301
|
+
return constant;
|
|
4302
|
+
return isExactNumeric(constant) ? constant : undefined;
|
|
4303
|
+
}
|
|
4304
|
+
function dictionaryNumericExpression(expression) {
|
|
4305
|
+
if (expression.kind === "column" && expression.vector.kind === "string") {
|
|
4306
|
+
return {
|
|
4307
|
+
source: expression.source,
|
|
4308
|
+
vector: expression.vector,
|
|
4309
|
+
signature: expression.signature,
|
|
4310
|
+
evaluate: (value) => value,
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
if (expression.kind !== "binary")
|
|
4314
|
+
return undefined;
|
|
4315
|
+
const sides = [
|
|
4316
|
+
{ column: expression.left, constant: expression.right, columnFirst: true },
|
|
4317
|
+
{ column: expression.right, constant: expression.left, columnFirst: false },
|
|
4318
|
+
];
|
|
4319
|
+
for (const { column, constant, columnFirst } of sides) {
|
|
4320
|
+
if (column.kind !== "column" || column.vector.kind !== "string")
|
|
4321
|
+
continue;
|
|
4322
|
+
const value = numericComparisonConstant(constant);
|
|
4323
|
+
if (value === undefined)
|
|
4324
|
+
continue;
|
|
4325
|
+
return {
|
|
4326
|
+
source: column.source,
|
|
4327
|
+
vector: column.vector,
|
|
4328
|
+
signature: expression.signature,
|
|
4329
|
+
evaluate: columnFirst
|
|
4330
|
+
? (dictionaryValue) => binaryValue(expression.operator, dictionaryValue, value)
|
|
4331
|
+
: (dictionaryValue) => binaryValue(expression.operator, value, dictionaryValue),
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4334
|
+
return undefined;
|
|
4335
|
+
}
|
|
4336
|
+
/** Detects exact-NUMERIC dictionary arithmetic compared with a constant. */
|
|
4337
|
+
function detectDictionaryNumericComparison(predicate) {
|
|
4338
|
+
if (!primitiveOperators.has(predicate.operator))
|
|
4339
|
+
return undefined;
|
|
4340
|
+
const comparison = predicate.operator;
|
|
4341
|
+
const sides = [
|
|
4342
|
+
{ expression: predicate.left, target: predicate.right, operator: comparison },
|
|
4343
|
+
{
|
|
4344
|
+
expression: predicate.right,
|
|
4345
|
+
target: predicate.left,
|
|
4346
|
+
operator: reverseComparisonOperator(comparison),
|
|
4347
|
+
},
|
|
4348
|
+
];
|
|
4349
|
+
for (const { expression, target, operator } of sides) {
|
|
4350
|
+
const numeric = dictionaryNumericExpression(expression);
|
|
4351
|
+
if (numeric === undefined)
|
|
4352
|
+
continue;
|
|
4353
|
+
const value = numericComparisonConstant(target);
|
|
4354
|
+
if (value === undefined)
|
|
4355
|
+
continue;
|
|
4356
|
+
return {
|
|
4357
|
+
expression: numeric,
|
|
4358
|
+
operator,
|
|
4359
|
+
target: value,
|
|
4360
|
+
signature: `${numeric.signature}\u0001${operator}\u0001${target.signature}`,
|
|
4361
|
+
cache: { dictionary: undefined, matches: new Uint8Array(0) },
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
return undefined;
|
|
4365
|
+
}
|
|
4119
4366
|
function stringCodeAt(vector, rowIndex) {
|
|
4120
4367
|
if (rowIndex < 0 || rowIndex >= vector.length)
|
|
4121
4368
|
return undefined;
|
|
@@ -4299,6 +4546,21 @@ function evaluateBatchPredicate(plan, predicate, batch, row) {
|
|
|
4299
4546
|
const matched = like.cache.matches[code] === 1;
|
|
4300
4547
|
return like.negated ? !matched : matched;
|
|
4301
4548
|
}
|
|
4549
|
+
const dictionaryNumeric = predicate.dictionaryNumeric;
|
|
4550
|
+
if (dictionaryNumeric !== undefined) {
|
|
4551
|
+
const vector = dictionaryNumeric.expression.vector;
|
|
4552
|
+
if (dictionaryNumeric.cache.dictionary !== vector.dictionary) {
|
|
4553
|
+
const matches = dictionaryNumericMatches(dictionaryNumeric, vector.dictionary);
|
|
4554
|
+
if (matches !== undefined) {
|
|
4555
|
+
dictionaryNumeric.cache.dictionary = vector.dictionary;
|
|
4556
|
+
dictionaryNumeric.cache.matches = matches;
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
if (dictionaryNumeric.cache.dictionary === vector.dictionary) {
|
|
4560
|
+
const code = stringCodeAt(vector, batch.rowsBySource[dictionaryNumeric.expression.source]?.[row] ?? -1);
|
|
4561
|
+
return code !== undefined && dictionaryNumeric.cache.matches[code] === 1;
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4302
4564
|
if (predicate.operator === "IS TRUE" ||
|
|
4303
4565
|
predicate.operator === "LIKE" ||
|
|
4304
4566
|
predicate.operator === "NOT LIKE" ||
|