@minnowdb/core 0.1.0 → 0.2.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 (149) hide show
  1. package/README.md +32 -18
  2. package/dist/engine/client.d.ts +9 -1
  3. package/dist/engine/client.d.ts.map +1 -1
  4. package/dist/engine/client.js +29 -5
  5. package/dist/engine/client.js.map +1 -1
  6. package/dist/engine/database.d.ts +51 -6
  7. package/dist/engine/database.d.ts.map +1 -1
  8. package/dist/engine/database.js +2597 -865
  9. package/dist/engine/database.js.map +1 -1
  10. package/dist/engine/live.d.ts.map +1 -1
  11. package/dist/engine/live.js +35 -11
  12. package/dist/engine/live.js.map +1 -1
  13. package/dist/engine/query-cache.d.ts +13 -1
  14. package/dist/engine/query-cache.d.ts.map +1 -1
  15. package/dist/engine/query-cache.js +34 -5
  16. package/dist/engine/query-cache.js.map +1 -1
  17. package/dist/engine/query.d.ts +11 -1
  18. package/dist/engine/query.d.ts.map +1 -1
  19. package/dist/engine/query.js +49 -41
  20. package/dist/engine/query.js.map +1 -1
  21. package/dist/engine/result-wire.d.ts +68 -0
  22. package/dist/engine/result-wire.d.ts.map +1 -0
  23. package/dist/engine/result-wire.js +264 -0
  24. package/dist/engine/result-wire.js.map +1 -0
  25. package/dist/engine/sort-keys.d.ts +50 -12
  26. package/dist/engine/sort-keys.d.ts.map +1 -1
  27. package/dist/engine/sort-keys.js +371 -25
  28. package/dist/engine/sort-keys.js.map +1 -1
  29. package/dist/engine/vector.d.ts +11 -0
  30. package/dist/engine/vector.d.ts.map +1 -1
  31. package/dist/engine/vector.js +99 -203
  32. package/dist/engine/vector.js.map +1 -1
  33. package/dist/engine/worker-host.d.ts +17 -1
  34. package/dist/engine/worker-host.d.ts.map +1 -1
  35. package/dist/engine/worker-host.js +83 -13
  36. package/dist/engine/worker-host.js.map +1 -1
  37. package/dist/storage/index.d.ts +13 -0
  38. package/dist/storage/index.d.ts.map +1 -1
  39. package/dist/storage/index.js +13 -0
  40. package/dist/storage/index.js.map +1 -1
  41. package/dist/storage/indexeddb.d.ts +5 -1
  42. package/dist/storage/indexeddb.d.ts.map +1 -1
  43. package/dist/storage/indexeddb.js +484 -402
  44. package/dist/storage/indexeddb.js.map +1 -1
  45. package/dist/storage/memory.d.ts +24 -21
  46. package/dist/storage/memory.d.ts.map +1 -1
  47. package/dist/storage/memory.js +124 -1199
  48. package/dist/storage/memory.js.map +1 -1
  49. package/dist/storage/opfs/files.d.ts +64 -0
  50. package/dist/storage/opfs/files.d.ts.map +1 -0
  51. package/dist/storage/opfs/files.js +243 -0
  52. package/dist/storage/opfs/files.js.map +1 -0
  53. package/dist/storage/opfs/index.d.ts +3 -0
  54. package/dist/storage/opfs/index.d.ts.map +1 -0
  55. package/dist/storage/opfs/index.js +3 -0
  56. package/dist/storage/opfs/index.js.map +1 -0
  57. package/dist/storage/opfs/leader.d.ts +130 -0
  58. package/dist/storage/opfs/leader.d.ts.map +1 -0
  59. package/dist/storage/opfs/leader.js +1104 -0
  60. package/dist/storage/opfs/leader.js.map +1 -0
  61. package/dist/storage/opfs/rpc.d.ts +70 -0
  62. package/dist/storage/opfs/rpc.d.ts.map +1 -0
  63. package/dist/storage/opfs/rpc.js +58 -0
  64. package/dist/storage/opfs/rpc.js.map +1 -0
  65. package/dist/storage/opfs/store.d.ts +154 -0
  66. package/dist/storage/opfs/store.d.ts.map +1 -0
  67. package/dist/storage/opfs/store.js +958 -0
  68. package/dist/storage/opfs/store.js.map +1 -0
  69. package/dist/storage/toolkit/extents.d.ts +69 -0
  70. package/dist/storage/toolkit/extents.d.ts.map +1 -0
  71. package/dist/storage/toolkit/extents.js +175 -0
  72. package/dist/storage/toolkit/extents.js.map +1 -0
  73. package/dist/storage/toolkit/index.d.ts +30 -0
  74. package/dist/storage/toolkit/index.d.ts.map +1 -0
  75. package/dist/storage/toolkit/index.js +5 -0
  76. package/dist/storage/toolkit/index.js.map +1 -0
  77. package/dist/storage/toolkit/record-core.d.ts +252 -0
  78. package/dist/storage/toolkit/record-core.d.ts.map +1 -0
  79. package/dist/storage/toolkit/record-core.js +1670 -0
  80. package/dist/storage/toolkit/record-core.js.map +1 -0
  81. package/dist/storage/toolkit/sync-file.d.ts +29 -0
  82. package/dist/storage/toolkit/sync-file.d.ts.map +1 -0
  83. package/dist/storage/toolkit/sync-file.js +2 -0
  84. package/dist/storage/toolkit/sync-file.js.map +1 -0
  85. package/dist/storage/toolkit/wal.d.ts +22 -0
  86. package/dist/storage/toolkit/wal.d.ts.map +1 -0
  87. package/dist/storage/toolkit/wal.js +79 -0
  88. package/dist/storage/toolkit/wal.js.map +1 -0
  89. package/dist/storage/toolkit/wire.d.ts +25 -0
  90. package/dist/storage/toolkit/wire.d.ts.map +1 -0
  91. package/dist/storage/toolkit/wire.js +100 -0
  92. package/dist/storage/toolkit/wire.js.map +1 -0
  93. package/dist/storage/types.d.ts +343 -59
  94. package/dist/storage/types.d.ts.map +1 -1
  95. package/dist/storage/types.js +112 -11
  96. package/dist/storage/types.js.map +1 -1
  97. package/dist/testing/block-store-conformance.d.ts +51 -0
  98. package/dist/testing/block-store-conformance.d.ts.map +1 -0
  99. package/dist/testing/block-store-conformance.js +745 -0
  100. package/dist/testing/block-store-conformance.js.map +1 -0
  101. package/dist/testing/index.d.ts +3 -0
  102. package/dist/testing/index.d.ts.map +1 -1
  103. package/dist/testing/index.js +5 -0
  104. package/dist/testing/index.js.map +1 -1
  105. package/dist/testing/opfs-shim.d.ts +35 -0
  106. package/dist/testing/opfs-shim.d.ts.map +1 -0
  107. package/dist/testing/opfs-shim.js +295 -0
  108. package/dist/testing/opfs-shim.js.map +1 -0
  109. package/dist/transactions/index.d.ts +42 -1
  110. package/dist/transactions/index.d.ts.map +1 -1
  111. package/dist/transactions/index.js +219 -13
  112. package/dist/transactions/index.js.map +1 -1
  113. package/dist/worker-protocol/index.d.ts +2 -2
  114. package/dist/worker-protocol/index.d.ts.map +1 -1
  115. package/dist/worker-protocol/index.js +2 -1
  116. package/dist/worker-protocol/index.js.map +1 -1
  117. package/package.json +10 -2
  118. package/dist/engine/dsl/db.d.ts +0 -137
  119. package/dist/engine/dsl/db.d.ts.map +0 -1
  120. package/dist/engine/dsl/db.js +0 -161
  121. package/dist/engine/dsl/db.js.map +0 -1
  122. package/dist/engine/dsl/expression.d.ts +0 -172
  123. package/dist/engine/dsl/expression.d.ts.map +0 -1
  124. package/dist/engine/dsl/expression.js +0 -397
  125. package/dist/engine/dsl/expression.js.map +0 -1
  126. package/dist/engine/dsl/index.d.ts +0 -8
  127. package/dist/engine/dsl/index.d.ts.map +0 -1
  128. package/dist/engine/dsl/index.js +0 -7
  129. package/dist/engine/dsl/index.js.map +0 -1
  130. package/dist/engine/dsl/live-query.d.ts +0 -45
  131. package/dist/engine/dsl/live-query.d.ts.map +0 -1
  132. package/dist/engine/dsl/live-query.js +0 -116
  133. package/dist/engine/dsl/live-query.js.map +0 -1
  134. package/dist/engine/dsl/mutations.d.ts +0 -123
  135. package/dist/engine/dsl/mutations.d.ts.map +0 -1
  136. package/dist/engine/dsl/mutations.js +0 -241
  137. package/dist/engine/dsl/mutations.js.map +0 -1
  138. package/dist/engine/dsl/select-query-builder.d.ts +0 -148
  139. package/dist/engine/dsl/select-query-builder.d.ts.map +0 -1
  140. package/dist/engine/dsl/select-query-builder.js +0 -377
  141. package/dist/engine/dsl/select-query-builder.js.map +0 -1
  142. package/dist/engine/dsl/sql-tag.d.ts +0 -46
  143. package/dist/engine/dsl/sql-tag.d.ts.map +0 -1
  144. package/dist/engine/dsl/sql-tag.js +0 -85
  145. package/dist/engine/dsl/sql-tag.js.map +0 -1
  146. package/dist/engine/dsl/types.d.ts +0 -133
  147. package/dist/engine/dsl/types.d.ts.map +0 -1
  148. package/dist/engine/dsl/types.js +0 -9
  149. package/dist/engine/dsl/types.js.map +0 -1
@@ -4,7 +4,7 @@ import { ByteGroupIndex } from "./group-index.js";
4
4
  import { ByteJoinIndex } from "./join-index.js";
5
5
  import { QueryMemoryBudgetError, QueryMemoryContext, } from "./memory.js";
6
6
  import { compareSqlStrings, defineSqlResultProperty } from "./sql-semantics.js";
7
- import { buildSortKeyColumn } from "./sort-keys.js";
7
+ import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
8
8
  const DEFAULT_BATCH_ROWS = 2_048;
9
9
  /** Above this, locating each IN member separately costs more than scanning between them. */
10
10
  const MAX_SPLIT_LIST_MEMBERS = 64;
@@ -24,6 +24,21 @@ const PACKED_GROUP_ENTRY_BYTES = 24;
24
24
  const SPILL_PAGE_MAGIC = 0x5350494c;
25
25
  const SPILL_PAGE_HEADER_BYTES = 8;
26
26
  const vectorTextEncoder = new TextEncoder();
27
+ /** Pages per batched spill write: bounds the transient encoded copies a batch holds. */
28
+ const SPILL_WRITE_BATCH_PAGES = 8;
29
+ /** Writes already-materialized pages through the batch method when the store offers one. */
30
+ async function writeSpillPages(store, pages) {
31
+ const batched = store.putPages?.bind(store);
32
+ if (batched === undefined) {
33
+ for (const page of pages) {
34
+ await store.putPage(page.ownerId, page.runId, page.pageIndex, page.bytes);
35
+ }
36
+ return;
37
+ }
38
+ for (let start = 0; start < pages.length; start += SPILL_WRITE_BATCH_PAGES) {
39
+ await batched(pages.slice(start, start + SPILL_WRITE_BATCH_PAGES));
40
+ }
41
+ }
27
42
  export function createColumnarTable(name, columns, uniqueKey) {
28
43
  const first = columns.values().next().value;
29
44
  const rowCount = first?.values.length ?? 0;
@@ -1156,11 +1171,18 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1156
1171
  partitionBuffers.set(partition, rows);
1157
1172
  }
1158
1173
  });
1174
+ const flush = [];
1159
1175
  for (const [partition, rows] of partitionBuffers) {
1160
1176
  const pageIndex = partitionPages[partition] ?? 0;
1161
- await store.putPage(ownerId, `partition-${String(partition)}`, pageIndex, encodeSpillRows(spillColumns, rows));
1177
+ flush.push({
1178
+ ownerId,
1179
+ runId: `partition-${String(partition)}`,
1180
+ pageIndex,
1181
+ bytes: encodeSpillRows(spillColumns, rows),
1182
+ });
1162
1183
  partitionPages[partition] = pageIndex + 1;
1163
1184
  }
1185
+ await writeSpillPages(store, flush);
1164
1186
  }
1165
1187
  finally {
1166
1188
  batchMemory.close();
@@ -1206,10 +1228,21 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1206
1228
  }
1207
1229
  const runId = `group-${String(runSequence++)}`;
1208
1230
  let outputPage = 0;
1231
+ let pending = [];
1209
1232
  for (let start = 0; start < rows.length; start += pageRows) {
1210
- await store.putPage(ownerId, runId, outputPage, encodeSpillRows(columns, rows.slice(start, start + pageRows)));
1233
+ pending.push({
1234
+ ownerId,
1235
+ runId,
1236
+ pageIndex: outputPage,
1237
+ bytes: encodeSpillRows(columns, rows.slice(start, start + pageRows)),
1238
+ });
1211
1239
  outputPage += 1;
1240
+ if (pending.length >= SPILL_WRITE_BATCH_PAGES) {
1241
+ await writeSpillPages(store, pending);
1242
+ pending = [];
1243
+ }
1212
1244
  }
1245
+ await writeSpillPages(store, pending);
1213
1246
  runs.push({ id: runId, pageCount: outputPage });
1214
1247
  }
1215
1248
  finally {
@@ -2329,7 +2362,17 @@ function appendJoinedRow(output, outputRow, input, row, buildSource, buildRow) {
2329
2362
  }
2330
2363
  }
2331
2364
  }
2332
- const DEFERRED_SELECTION_BYTES = 2 * QUERY_REFERENCE_BYTES;
2365
+ const RETAINED_ROW_BYTES = 3 * QUERY_REFERENCE_BYTES;
2366
+ /**
2367
+ * A LIMIT that keeps at least this share of the scanned rows is not a bound worth enforcing:
2368
+ * what the bounded sink saves in memory it spends in time, and past the share it is cheaper to
2369
+ * keep every row and sort once. Measured at 200k rows, `ORDER BY region, id` and `ORDER BY
2370
+ * amount`, against an unbounded scan-and-sort that costs the same (30 ms and 21 ms) whatever the
2371
+ * LIMIT: at 5% the bound wins (22 ms and 17 ms), at 10% it loses (39 ms and 31 ms), and at 50%
2372
+ * it loses by three times (97 ms and 72 ms). The crossover sits between those two shares, so
2373
+ * the bound stops at the upper one.
2374
+ */
2375
+ const BOUNDED_SINK_MAX_SHARE = 0.1;
2333
2376
  /**
2334
2377
  * Accumulates ungrouped result rows. With ORDER BY and LIMIT the sink keeps only the best
2335
2378
  * `limit + offset` rows instead of materializing every scanned row for one full sort, so memory
@@ -2337,15 +2380,18 @@ const DEFERRED_SELECTION_BYTES = 2 * QUERY_REFERENCE_BYTES;
2337
2380
  * ties resolve by arrival order, so the retained set is precisely the slice a full stable sort
2338
2381
  * would have produced.
2339
2382
  *
2340
- * Two bounded strategies:
2341
- * - Deferred selection (single-source plans whose order keys resolve to select expressions and
2342
- * whose scan vectors outlive the scan): rows are tracked as (keys, arrival, source row) and
2343
- * compacted by sort once the selection buffer reaches twice the capacity; only the final
2344
- * survivors are projected. A losing row never allocates a result object, so even the
2345
- * adversarial ascending-input/descending-order case stays allocation-free per row.
2346
- * - Eager heap (joins, wildcard selects, or windowed scans): each retained row is projected into
2347
- * a worst-at-root heap; a candidate that cannot beat the current worst is rejected before
2348
- * projection whenever the order keys are resolvable.
2383
+ * Candidates are tracked as (keys, arrival) entries in a selection buffer that is compacted by
2384
+ * quickselect once it reaches twice the capacity, so a retained row costs a few comparisons
2385
+ * amortized, and a row that cannot beat the current cut line costs nothing past its keys.
2386
+ * Whether the entry also carries the projected row depends on the scan: when the scan
2387
+ * vectors outlive the scan and the order keys resolve to select expressions, projection waits
2388
+ * for the survivors; a streamed scan, a join, or a wildcard select projects the candidate as it
2389
+ * arrives, since there is nothing to read it back from later.
2390
+ *
2391
+ * The bound is dropped for a page that would keep most of the scan anyway: a single-source
2392
+ * plan whose `limit + offset` reaches BOUNDED_SINK_MAX_SHARE of the scan rows keeps every row
2393
+ * and leaves the ordering to one final sort. A join's output size is not known from its scan, so
2394
+ * a joined plan stays bounded.
2349
2395
  */
2350
2396
  class ResultSink {
2351
2397
  #plan;
@@ -2354,7 +2400,6 @@ class ResultSink {
2354
2400
  #keyExpressions;
2355
2401
  #deferred;
2356
2402
  #rows = [];
2357
- #heap = [];
2358
2403
  #selection = [];
2359
2404
  #keyScratch = [];
2360
2405
  #threshold;
@@ -2367,8 +2412,13 @@ class ResultSink {
2367
2412
  constructor(plan, memory, stableScan) {
2368
2413
  this.#plan = plan;
2369
2414
  this.#memory = memory;
2370
- const bounded = !plan.grouped && plan.orderBy.length > 0 && plan.limit !== undefined;
2371
- this.#capacity = bounded ? (plan.limit ?? 0) + (plan.offset ?? 0) : undefined;
2415
+ const capacity = (plan.limit ?? 0) + (plan.offset ?? 0);
2416
+ const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
2417
+ const bounded = !plan.grouped &&
2418
+ plan.orderBy.length > 0 &&
2419
+ plan.limit !== undefined &&
2420
+ (plan.joins.length > 0 || capacity < scanRows * BOUNDED_SINK_MAX_SHARE);
2421
+ this.#capacity = bounded ? capacity : undefined;
2372
2422
  let keyExpressions;
2373
2423
  if (bounded && !plan.wildcard) {
2374
2424
  keyExpressions = [];
@@ -2428,7 +2478,7 @@ class ResultSink {
2428
2478
  for (let row = 0; row < batch.length; row += 1) {
2429
2479
  const threshold = this.#thresholdFirst;
2430
2480
  if (threshold === undefined || threshold === null) {
2431
- this.#addSlow(batch, row);
2481
+ this.#addCandidate(batch, row);
2432
2482
  continue;
2433
2483
  }
2434
2484
  const sourceRow = rows?.[row] ?? -1;
@@ -2442,16 +2492,10 @@ class ResultSink {
2442
2492
  }
2443
2493
  }
2444
2494
  }
2445
- this.#addSlow(batch, row);
2495
+ this.#addCandidate(batch, row);
2446
2496
  }
2447
2497
  return true;
2448
2498
  }
2449
- #addSlow(batch, row) {
2450
- if (this.#deferred)
2451
- this.#addDeferred(batch, row);
2452
- else
2453
- this.#addEager(batch, row);
2454
- }
2455
2499
  add(batch, row) {
2456
2500
  if (this.#capacity === undefined) {
2457
2501
  const resultRow = projectBatchRow(this.#plan, batch, row);
@@ -2461,21 +2505,17 @@ class ResultSink {
2461
2505
  }
2462
2506
  if (this.#capacity === 0)
2463
2507
  return;
2464
- if (this.#deferred) {
2465
- this.#addDeferred(batch, row);
2466
- return;
2467
- }
2468
- this.#addEager(batch, row);
2508
+ this.#addCandidate(batch, row);
2469
2509
  }
2470
2510
  /** Returns accepted rows in arrival order, ready for the shared stable sort and trim. */
2471
2511
  finish() {
2472
2512
  if (this.#capacity === undefined)
2473
2513
  return this.#rows;
2474
- if (!this.#deferred) {
2475
- return this.#heap.sort((left, right) => left.seq - right.seq).map((entry) => entry.row);
2476
- }
2477
2514
  this.#compactSelection();
2478
2515
  this.#selection.sort((left, right) => left.seq - right.seq);
2516
+ if (!this.#deferred) {
2517
+ return this.#selection.map((entry) => required(entry.row, "Retained row is missing"));
2518
+ }
2479
2519
  const scanIndex = new Int32Array(1);
2480
2520
  const rowsBySource = [scanIndex];
2481
2521
  const batch = { length: 1, rowsBySource };
@@ -2488,7 +2528,7 @@ class ResultSink {
2488
2528
  }
2489
2529
  return rows;
2490
2530
  }
2491
- #addDeferred(batch, row) {
2531
+ #addCandidate(batch, row) {
2492
2532
  const fast = this.#fastFirstKey;
2493
2533
  if (fast !== undefined && this.#thresholdFirst !== undefined && this.#thresholdFirst !== null) {
2494
2534
  const value = rawFloat64Value(fast.vector, batch.rowsBySource[fast.source]?.[row] ?? -1);
@@ -2501,7 +2541,18 @@ class ResultSink {
2501
2541
  }
2502
2542
  }
2503
2543
  }
2504
- this.#evaluateKeys(batch, row);
2544
+ let projected;
2545
+ if (this.#keyExpressions === undefined) {
2546
+ // The order keys are not select expressions, so the projected row is where they live.
2547
+ projected = projectBatchRow(this.#plan, batch, row);
2548
+ for (let index = 0; index < this.#plan.orderBy.length; index += 1) {
2549
+ const order = required(this.#plan.orderBy[index], "Order term is missing");
2550
+ this.#keyScratch[index] = projected[order.outputName] ?? null;
2551
+ }
2552
+ }
2553
+ else {
2554
+ this.#evaluateKeys(batch, row);
2555
+ }
2505
2556
  const seq = this.#seq;
2506
2557
  this.#seq += 1;
2507
2558
  // The candidate arrived after every retained row, so an order-key tie keeps the retained
@@ -2510,14 +2561,21 @@ class ResultSink {
2510
2561
  this.#compareKeys(this.#keyScratch, this.#threshold.keys) >= 0) {
2511
2562
  return;
2512
2563
  }
2513
- let bytes = DEFERRED_SELECTION_BYTES;
2564
+ if (!this.#deferred && projected === undefined) {
2565
+ projected = projectBatchRow(this.#plan, batch, row);
2566
+ }
2567
+ let bytes = RETAINED_ROW_BYTES;
2514
2568
  for (let index = 0; index < this.#plan.orderBy.length; index += 1) {
2515
2569
  bytes = safeMemorySum(bytes, queryValuePayloadBytes(this.#keyScratch[index] ?? null), "Top-N selection entry");
2516
2570
  }
2571
+ if (projected !== undefined) {
2572
+ bytes = safeMemorySum(bytes, queryRowPayloadBytes(projected), "Top-N selection entry");
2573
+ }
2517
2574
  this.#selection.push({
2518
2575
  keys: this.#keyScratch.slice(0, this.#plan.orderBy.length),
2519
2576
  seq,
2520
- sourceRow: batch.rowsBySource[this.#plan.scanSource]?.[row] ?? -1,
2577
+ sourceRow: this.#deferred ? (batch.rowsBySource[this.#plan.scanSource]?.[row] ?? -1) : -1,
2578
+ row: projected,
2521
2579
  bytes,
2522
2580
  });
2523
2581
  this.#reserveSelectionBytes(bytes);
@@ -2623,64 +2681,6 @@ class ResultSink {
2623
2681
  low = right + 1;
2624
2682
  }
2625
2683
  }
2626
- #addEager(batch, row) {
2627
- let projected;
2628
- if (this.#keyExpressions === undefined) {
2629
- projected = projectBatchRow(this.#plan, batch, row);
2630
- for (let index = 0; index < this.#plan.orderBy.length; index += 1) {
2631
- const order = required(this.#plan.orderBy[index], "Order term is missing");
2632
- this.#keyScratch[index] = projected[order.outputName] ?? null;
2633
- }
2634
- }
2635
- else {
2636
- this.#evaluateKeys(batch, row);
2637
- }
2638
- const seq = this.#seq;
2639
- this.#seq += 1;
2640
- const capacity = this.#capacity ?? 0;
2641
- if (this.#heap.length >= capacity) {
2642
- const worst = required(this.#heap[0], "Top-N heap root is missing");
2643
- // An order-key tie keeps the earlier-arriving retained row.
2644
- if (this.#compareKeys(this.#keyScratch, worst.keys) >= 0)
2645
- return;
2646
- const resultRow = projected ?? projectBatchRow(this.#plan, batch, row);
2647
- const reservation = this.#memory.reserve(this.#entryPayloadBytes(resultRow), "Top-N result row");
2648
- worst.reservation.release();
2649
- this.#heap[0] = {
2650
- row: resultRow,
2651
- keys: this.#keyScratch.slice(0, this.#plan.orderBy.length),
2652
- seq,
2653
- reservation,
2654
- };
2655
- this.#siftDown(0);
2656
- this.#updateEagerThreshold();
2657
- return;
2658
- }
2659
- const resultRow = projected ?? projectBatchRow(this.#plan, batch, row);
2660
- const reservation = this.#memory.reserve(this.#entryPayloadBytes(resultRow), "Top-N result row");
2661
- this.#heap.push({
2662
- row: resultRow,
2663
- keys: this.#keyScratch.slice(0, this.#plan.orderBy.length),
2664
- seq,
2665
- reservation,
2666
- });
2667
- this.#siftUp(this.#heap.length - 1);
2668
- this.#updateEagerThreshold();
2669
- }
2670
- /**
2671
- * The eager cut line for the unboxed batch loop: once the heap is full, its root's first
2672
- * order key as a float. Non-numeric keys leave the threshold null, which sends every row
2673
- * through the full comparison.
2674
- */
2675
- #updateEagerThreshold() {
2676
- if (this.#fastFirstKey === undefined)
2677
- return;
2678
- if (this.#heap.length < (this.#capacity ?? 0))
2679
- return;
2680
- const first = this.#heap[0]?.keys[0];
2681
- this.#thresholdFirst =
2682
- typeof first === "number" ? first : first instanceof Date ? first.getTime() : null;
2683
- }
2684
2684
  #evaluateKeys(batch, row) {
2685
2685
  const expressions = required(this.#keyExpressions, "Order keys are missing");
2686
2686
  for (let index = 0; index < expressions.length; index += 1) {
@@ -2688,13 +2688,6 @@ class ResultSink {
2688
2688
  this.#keyScratch[index] = asQueryValue(evaluateBatchExpression(this.#plan, expression, batch, row));
2689
2689
  }
2690
2690
  }
2691
- #entryPayloadBytes(row) {
2692
- let bytes = queryRowPayloadBytes(row);
2693
- for (let index = 0; index < this.#plan.orderBy.length; index += 1) {
2694
- bytes = safeMemorySum(bytes, queryValuePayloadBytes(this.#keyScratch[index] ?? null), "Top-N order keys");
2695
- }
2696
- return bytes;
2697
- }
2698
2691
  #compareKeys(left, right) {
2699
2692
  const orderBy = this.#plan.orderBy;
2700
2693
  for (let index = 0; index < orderBy.length; index += 1) {
@@ -2708,51 +2701,6 @@ class ResultSink {
2708
2701
  }
2709
2702
  return 0;
2710
2703
  }
2711
- /** Entry `left` loses to `right` when it sorts later under (keys, arrival). */
2712
- #isWorse(left, right) {
2713
- const comparison = this.#compareKeys(left.keys, right.keys);
2714
- if (comparison !== 0)
2715
- return comparison > 0;
2716
- return left.seq > right.seq;
2717
- }
2718
- #siftUp(index) {
2719
- let child = index;
2720
- while (child > 0) {
2721
- const parent = (child - 1) >> 1;
2722
- const childEntry = required(this.#heap[child], "Heap entry is missing");
2723
- const parentEntry = required(this.#heap[parent], "Heap entry is missing");
2724
- if (!this.#isWorse(childEntry, parentEntry))
2725
- break;
2726
- this.#heap[child] = parentEntry;
2727
- this.#heap[parent] = childEntry;
2728
- child = parent;
2729
- }
2730
- }
2731
- #siftDown(index) {
2732
- let parent = index;
2733
- for (;;) {
2734
- let worst = parent;
2735
- let worstEntry = required(this.#heap[worst], "Heap entry is missing");
2736
- const left = parent * 2 + 1;
2737
- const right = left + 1;
2738
- const leftEntry = this.#heap[left];
2739
- if (leftEntry !== undefined && this.#isWorse(leftEntry, worstEntry)) {
2740
- worst = left;
2741
- worstEntry = leftEntry;
2742
- }
2743
- const rightEntry = this.#heap[right];
2744
- if (rightEntry !== undefined && this.#isWorse(rightEntry, worstEntry)) {
2745
- worst = right;
2746
- worstEntry = rightEntry;
2747
- }
2748
- if (worst === parent)
2749
- return;
2750
- const parentEntry = required(this.#heap[parent], "Heap entry is missing");
2751
- this.#heap[parent] = worstEntry;
2752
- this.#heap[worst] = parentEntry;
2753
- parent = worst;
2754
- }
2755
- }
2756
2704
  }
2757
2705
  /**
2758
2706
  * Insertion-ordered group-state store. A single bare string-column GROUP BY resolves each row
@@ -4065,66 +4013,14 @@ function groupKey(value) {
4065
4013
  throw new TypeError("Group keys must be SQL scalar values");
4066
4014
  }
4067
4015
  function stableSortRows(rows, orderBy) {
4068
- if (rows.length > 0xffffffff)
4069
- throw new RangeError("Too many rows to order");
4070
- const indexes = new Uint32Array(rows.length);
4071
- const scratch = new Uint32Array(rows.length);
4072
- for (let index = 0; index < indexes.length; index += 1)
4073
- indexes[index] = index;
4074
- let source = indexes;
4075
- let target = scratch;
4076
- // One prepared column per term, extracted once; see sort-keys.ts for why the comparison
4077
- // rather than the merge is what a sort costs. Direction and explicit NULL placement are
4078
- // resolved here, outside the comparison, because they never vary row to row.
4079
- const termCount = orderBy.length;
4016
+ // One prepared column per term, extracted once; see sort-keys.ts for the kernel and for why
4017
+ // the comparison rather than the merge is what a sort costs.
4080
4018
  const terms = orderBy.map((order) => ({
4081
- /** Signed placement for an explicit NULLS FIRST/LAST, or 0 when the default applies. */
4082
- nullPlacement: order.nulls === undefined ? 0 : order.nulls === "first" ? 1 : -1,
4019
+ column: buildSortKeyColumn(rows.length, (index) => required(rows[index], "Ordering row is missing")[order.outputName]),
4083
4020
  descending: order.direction === "desc",
4021
+ nulls: order.nulls,
4084
4022
  }));
4085
- const columns = orderBy.map((order) => buildSortKeyColumn(rows.length, (index) => required(rows[index], "Ordering row is missing")[order.outputName]));
4086
- const compareIndexes = (leftIndex, rightIndex) => {
4087
- for (let term = 0; term < termCount; term += 1) {
4088
- const column = required(columns[term], "Order keys are missing");
4089
- const { nullPlacement, descending } = required(terms[term], "Order term is missing");
4090
- if (nullPlacement !== 0) {
4091
- // An explicit placement is absolute: direction must not negate it.
4092
- const leftNull = column.isNull(leftIndex);
4093
- const rightNull = column.isNull(rightIndex);
4094
- if (leftNull || rightNull) {
4095
- if (leftNull && rightNull)
4096
- continue;
4097
- return (leftNull ? -1 : 1) * nullPlacement;
4098
- }
4099
- }
4100
- const comparison = column.compare(leftIndex, rightIndex);
4101
- if (comparison !== 0)
4102
- return descending ? -comparison : comparison;
4103
- }
4104
- return 0;
4105
- };
4106
- for (let width = 1; width < rows.length; width *= 2) {
4107
- for (let start = 0; start < rows.length; start += width * 2) {
4108
- const middle = Math.min(start + width, rows.length);
4109
- const end = Math.min(start + width * 2, rows.length);
4110
- let left = start;
4111
- let right = middle;
4112
- for (let output = start; output < end; output += 1) {
4113
- if (right >= end ||
4114
- (left < middle && compareIndexes(source[left] ?? 0, source[right] ?? 0) <= 0)) {
4115
- target[output] = source[left] ?? 0;
4116
- left += 1;
4117
- }
4118
- else {
4119
- target[output] = source[right] ?? 0;
4120
- right += 1;
4121
- }
4122
- }
4123
- }
4124
- [source, target] = [target, source];
4125
- }
4126
- if (source !== indexes)
4127
- indexes.set(source);
4023
+ const indexes = sortKeyIndexes(rows.length, terms);
4128
4024
  const visited = new Uint8Array(rows.length);
4129
4025
  for (let start = 0; start < rows.length; start += 1) {
4130
4026
  if (visited[start] === 1)