@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.
Files changed (42) hide show
  1. package/README.md +3 -2
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +3 -1
  5. package/dist/engine/catalog.js +1 -0
  6. package/dist/engine/client.d.ts +32 -4
  7. package/dist/engine/client.js +82 -15
  8. package/dist/engine/database.d.ts +23 -14
  9. package/dist/engine/database.js +528 -79
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +13 -0
  12. package/dist/engine/errors.js +22 -0
  13. package/dist/engine/fts.d.ts +2 -15
  14. package/dist/engine/live.d.ts +1 -7
  15. package/dist/engine/live.js +2 -12
  16. package/dist/engine/optimizer.d.ts +7 -0
  17. package/dist/engine/optimizer.js +1349 -76
  18. package/dist/engine/query.d.ts +11 -278
  19. package/dist/engine/query.js +178 -49
  20. package/dist/engine/schema-wire.d.ts +7 -1
  21. package/dist/engine/schema-wire.js +4 -0
  22. package/dist/engine/schema.d.ts +67 -33
  23. package/dist/engine/schema.js +138 -7
  24. package/dist/engine/sql-domains.d.ts +8 -0
  25. package/dist/engine/sql-domains.js +25 -0
  26. package/dist/engine/sql-json.js +22 -3
  27. package/dist/engine/vector.d.ts +2 -2
  28. package/dist/engine/vector.js +369 -43
  29. package/dist/engine/worker-host.js +119 -44
  30. package/dist/plan/index.d.ts +5 -4
  31. package/dist/plan/index.js +3 -3
  32. package/dist/plan/model.d.ts +224 -0
  33. package/dist/plan/model.js +1 -0
  34. package/dist/storage/types.d.ts +7 -0
  35. package/dist/storage/types.js +16 -0
  36. package/dist/transactions/index.d.ts +5 -3
  37. package/dist/transactions/index.js +58 -8
  38. package/dist/worker-protocol/index.d.ts +6 -1
  39. package/dist/worker-protocol/index.js +5 -2
  40. package/package.json +1 -1
  41. package/postgres-feature-profile.json +5 -0
  42. package/sql-feature-matrix.json +89 -21
@@ -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 { jsonValueOf } from "./sql-json.js";
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
- return this.execute();
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 || bound.dictionaryLike !== 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.map((source, index) => ({
656
- alias: source.alias,
657
- columns: [...(sourceTables[index]?.columns.keys() ?? [])],
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 !== "JSON_ARRAYAGG" &&
850
- expression.name !== "STRING_AGG" &&
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?.throwIfAborted();
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?.throwIfAborted();
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 output = new ResultSink(scanPlan, pageMemory, options.loadScanWindow === undefined);
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
- list.push(member);
3674
- memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(member), "JSON_ARRAYAGG member"), "JSON_ARRAYAGG member");
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
- return JSON.stringify((required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? []).map(jsonValueOf));
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" ||