@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.
Files changed (40) hide show
  1. package/README.md +1 -1
  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 +516 -74
  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.js +540 -38
  17. package/dist/engine/query.d.ts +5 -278
  18. package/dist/engine/query.js +78 -32
  19. package/dist/engine/schema-wire.d.ts +7 -1
  20. package/dist/engine/schema-wire.js +4 -0
  21. package/dist/engine/schema.d.ts +67 -33
  22. package/dist/engine/schema.js +138 -7
  23. package/dist/engine/sql-domains.d.ts +8 -0
  24. package/dist/engine/sql-domains.js +25 -0
  25. package/dist/engine/sql-json.js +22 -3
  26. package/dist/engine/vector.d.ts +2 -2
  27. package/dist/engine/vector.js +301 -39
  28. package/dist/engine/worker-host.js +119 -44
  29. package/dist/plan/index.d.ts +5 -4
  30. package/dist/plan/index.js +3 -3
  31. package/dist/plan/model.d.ts +218 -0
  32. package/dist/plan/model.js +1 -0
  33. package/dist/storage/types.d.ts +7 -0
  34. package/dist/storage/types.js +16 -0
  35. package/dist/transactions/index.d.ts +5 -3
  36. package/dist/transactions/index.js +58 -8
  37. package/dist/worker-protocol/index.d.ts +6 -1
  38. package/dist/worker-protocol/index.js +5 -2
  39. package/package.json +1 -1
  40. package/sql-feature-matrix.json +69 -19
@@ -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,
@@ -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?.throwIfAborted();
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?.throwIfAborted();
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 output = new ResultSink(scanPlan, pageMemory, options.loadScanWindow === undefined);
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 JSON.stringify((required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? []).map(jsonValueOf));
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" ||