squirreling 0.12.26 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/execute/accumulator.js +89 -0
- package/src/execute/aggregates.js +27 -62
- package/src/execute/execute.js +35 -28
- package/src/execute/join.js +13 -13
- package/src/execute/sort.js +8 -3
- package/src/execute/streamingAggregate.js +598 -0
- package/src/execute/window.js +8 -8
- package/src/expression/evaluate.js +1 -1
- package/src/index.d.ts +1 -1
- package/src/plan/columns.js +1 -1
- package/src/plan/plan.js +39 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@types/node": "26.1.0",
|
|
43
43
|
"@vitest/coverage-v8": "4.1.9",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
|
-
"eslint-plugin-jsdoc": "63.0.
|
|
45
|
+
"eslint-plugin-jsdoc": "63.0.11",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
47
|
"vitest": "4.1.9"
|
|
48
48
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { keyify } from './utils.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import { SqlPrimitive } from '../types.js'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Incremental state for one streamable aggregate. Shared by the streaming
|
|
9
|
+
* aggregate executor and the scanColumn fast path so COUNT/COUNTIF/SUM/AVG/
|
|
10
|
+
* MIN/MAX fold semantics live in one place.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {{
|
|
13
|
+
* count: number,
|
|
14
|
+
* sum: number,
|
|
15
|
+
* min: SqlPrimitive,
|
|
16
|
+
* max: SqlPrimitive,
|
|
17
|
+
* seen: Set<unknown> | null,
|
|
18
|
+
* }} Accumulator
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} funcName
|
|
23
|
+
* @param {boolean} [distinct]
|
|
24
|
+
* @returns {Accumulator}
|
|
25
|
+
*/
|
|
26
|
+
export function newAccumulator(funcName, distinct) {
|
|
27
|
+
return {
|
|
28
|
+
count: 0,
|
|
29
|
+
sum: 0,
|
|
30
|
+
min: null,
|
|
31
|
+
max: null,
|
|
32
|
+
seen: funcName === 'COUNT' && distinct ? new Set() : null,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Folds one value into an accumulator, matching the buffered semantics in
|
|
38
|
+
* evaluate.js: COUNT counts non-null, COUNTIF counts truthy, MIN/MAX compare
|
|
39
|
+
* raw values, SUM/AVG only accumulate finite numbers.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} funcName
|
|
42
|
+
* @param {Accumulator} acc
|
|
43
|
+
* @param {SqlPrimitive} value
|
|
44
|
+
*/
|
|
45
|
+
export function updateAccumulator(funcName, acc, value) {
|
|
46
|
+
switch (funcName) {
|
|
47
|
+
case 'COUNT':
|
|
48
|
+
if (value == null) break
|
|
49
|
+
if (acc.seen) acc.seen.add(keyify(value))
|
|
50
|
+
else acc.count++
|
|
51
|
+
break
|
|
52
|
+
case 'COUNTIF':
|
|
53
|
+
if (value) acc.count++
|
|
54
|
+
break
|
|
55
|
+
case 'MIN':
|
|
56
|
+
if (value != null && (acc.min === null || value < acc.min)) acc.min = value
|
|
57
|
+
break
|
|
58
|
+
case 'MAX':
|
|
59
|
+
if (value != null && (acc.max === null || value > acc.max)) acc.max = value
|
|
60
|
+
break
|
|
61
|
+
default: { // SUM, AVG
|
|
62
|
+
if (value == null) break
|
|
63
|
+
const num = Number(value)
|
|
64
|
+
if (Number.isFinite(num)) {
|
|
65
|
+
acc.sum += num
|
|
66
|
+
acc.count++
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reduces an accumulator to its final aggregate value.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} funcName
|
|
76
|
+
* @param {Accumulator} acc
|
|
77
|
+
* @returns {SqlPrimitive}
|
|
78
|
+
*/
|
|
79
|
+
export function finalizeAccumulator(funcName, acc) {
|
|
80
|
+
switch (funcName) {
|
|
81
|
+
case 'COUNT': return acc.seen ? acc.seen.size : acc.count
|
|
82
|
+
case 'COUNTIF': return acc.count
|
|
83
|
+
case 'SUM': return acc.count === 0 ? null : acc.sum
|
|
84
|
+
case 'AVG': return acc.count === 0 ? null : acc.sum / acc.count
|
|
85
|
+
case 'MIN': return acc.min
|
|
86
|
+
case 'MAX': return acc.max
|
|
87
|
+
default: return null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { derivedAlias } from '../expression/alias.js'
|
|
2
2
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
3
|
+
import { finalizeAccumulator, newAccumulator, updateAccumulator } from './accumulator.js'
|
|
3
4
|
import { executePlan, selectColumnNames } from './execute.js'
|
|
4
5
|
import { sortEntriesByTerms } from './sort.js'
|
|
6
|
+
import { planStreamingAggregates, streamingHashAggregateRows, streamingScalarAggregateRows } from './streamingAggregate.js'
|
|
5
7
|
import { keyify } from './utils.js'
|
|
6
8
|
import { yieldToEventLoop } from './yield.js'
|
|
7
9
|
|
|
@@ -80,6 +82,14 @@ function aggregateContextRow(group, aggregateRow) {
|
|
|
80
82
|
*/
|
|
81
83
|
export function executeHashAggregate(plan, context) {
|
|
82
84
|
const child = executePlan({ plan: plan.child, context })
|
|
85
|
+
const streaming = planStreamingAggregates(plan, child.columns)
|
|
86
|
+
if (streaming) {
|
|
87
|
+
return {
|
|
88
|
+
columns: selectColumnNames(plan.columns, child.columns),
|
|
89
|
+
maxRows: child.maxRows,
|
|
90
|
+
rows: streamingHashAggregateRows({ plan, streaming, child, context }),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
83
93
|
return {
|
|
84
94
|
columns: selectColumnNames(plan.columns, child.columns),
|
|
85
95
|
maxRows: child.maxRows,
|
|
@@ -91,7 +101,7 @@ export function executeHashAggregate(plan, context) {
|
|
|
91
101
|
for await (const row of child.rows()) {
|
|
92
102
|
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
93
103
|
await yieldToEventLoop()
|
|
94
|
-
|
|
104
|
+
context.signal?.throwIfAborted()
|
|
95
105
|
}
|
|
96
106
|
allRows.push(row)
|
|
97
107
|
}
|
|
@@ -111,7 +121,7 @@ export function executeHashAggregate(plan, context) {
|
|
|
111
121
|
for (let chunkStart = 0; chunkStart < allRows.length; chunkStart += YIELD_INTERVAL) {
|
|
112
122
|
if (chunkStart > 0) {
|
|
113
123
|
await yieldToEventLoop()
|
|
114
|
-
|
|
124
|
+
context.signal?.throwIfAborted()
|
|
115
125
|
}
|
|
116
126
|
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, allRows.length)
|
|
117
127
|
const chunkLen = chunkEnd - chunkStart
|
|
@@ -196,6 +206,15 @@ export function executeScalarAggregate(plan, context) {
|
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
const child = executePlan({ plan: plan.child, context })
|
|
209
|
+
const streaming = planStreamingAggregates(plan, child.columns)
|
|
210
|
+
if (streaming) {
|
|
211
|
+
return {
|
|
212
|
+
columns: selectColumnNames(plan.columns, child.columns),
|
|
213
|
+
numRows: plan.having ? undefined : 1,
|
|
214
|
+
maxRows: 1,
|
|
215
|
+
rows: streamingScalarAggregateRows({ plan, streaming, child, context }),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
199
218
|
return {
|
|
200
219
|
columns: selectColumnNames(plan.columns, child.columns),
|
|
201
220
|
numRows: plan.having ? undefined : 1,
|
|
@@ -208,7 +227,7 @@ export function executeScalarAggregate(plan, context) {
|
|
|
208
227
|
for await (const row of child.rows()) {
|
|
209
228
|
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
210
229
|
await yieldToEventLoop()
|
|
211
|
-
|
|
230
|
+
context.signal?.throwIfAborted()
|
|
212
231
|
}
|
|
213
232
|
group.push(row)
|
|
214
233
|
}
|
|
@@ -344,17 +363,6 @@ function extractColumnAggSpec({ expr, alias }) {
|
|
|
344
363
|
}
|
|
345
364
|
}
|
|
346
365
|
|
|
347
|
-
/**
|
|
348
|
-
* @typedef {{
|
|
349
|
-
* spec: ColumnAggSpec,
|
|
350
|
-
* count: number,
|
|
351
|
-
* sum: number,
|
|
352
|
-
* min: SqlPrimitive,
|
|
353
|
-
* max: SqlPrimitive,
|
|
354
|
-
* seen: Set<unknown> | null,
|
|
355
|
-
* }} AggAccumulator
|
|
356
|
-
*/
|
|
357
|
-
|
|
358
366
|
/**
|
|
359
367
|
* Scans a column once and computes every aggregate over it in a single pass.
|
|
360
368
|
* All specs share the one scanColumn walk, so MIN(x)/MAX(x)/AVG(x) decode x once.
|
|
@@ -371,41 +379,15 @@ async function scanColumnGroup({ table, specs, limit, offset, signal }) {
|
|
|
371
379
|
const { column } = specs[0]
|
|
372
380
|
const values = table.scanColumn({ column, limit, offset, signal })
|
|
373
381
|
|
|
374
|
-
|
|
375
|
-
const accs = specs.map(spec => /** @type {AggAccumulator} */ ({
|
|
376
|
-
spec,
|
|
377
|
-
count: 0,
|
|
378
|
-
sum: 0,
|
|
379
|
-
min: null,
|
|
380
|
-
max: null,
|
|
381
|
-
seen: spec.funcName === 'COUNT' && spec.distinct ? new Set() : null,
|
|
382
|
-
}))
|
|
382
|
+
const accs = specs.map(spec => ({ spec, acc: newAccumulator(spec.funcName, spec.distinct) }))
|
|
383
383
|
|
|
384
384
|
for await (const chunk of values) {
|
|
385
385
|
signal?.throwIfAborted()
|
|
386
386
|
for (let i = 0; i < chunk.length; i++) {
|
|
387
387
|
const v = chunk[i]
|
|
388
388
|
if (v == null) continue
|
|
389
|
-
for (const acc of accs) {
|
|
390
|
-
|
|
391
|
-
case 'COUNT':
|
|
392
|
-
if (acc.seen) acc.seen.add(keyify(v))
|
|
393
|
-
else acc.count++
|
|
394
|
-
break
|
|
395
|
-
case 'MIN':
|
|
396
|
-
if (acc.min === null || v < acc.min) acc.min = v
|
|
397
|
-
break
|
|
398
|
-
case 'MAX':
|
|
399
|
-
if (acc.max === null || v > acc.max) acc.max = v
|
|
400
|
-
break
|
|
401
|
-
default: { // SUM, AVG
|
|
402
|
-
const num = Number(v)
|
|
403
|
-
if (Number.isFinite(num)) {
|
|
404
|
-
acc.sum += num
|
|
405
|
-
acc.count++
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
389
|
+
for (const { spec, acc } of accs) {
|
|
390
|
+
updateAccumulator(spec.funcName, acc, v)
|
|
409
391
|
}
|
|
410
392
|
}
|
|
411
393
|
}
|
|
@@ -413,25 +395,8 @@ async function scanColumnGroup({ table, specs, limit, offset, signal }) {
|
|
|
413
395
|
|
|
414
396
|
/** @type {Map<string, SqlPrimitive>} */
|
|
415
397
|
const result = new Map()
|
|
416
|
-
for (const acc of accs) {
|
|
417
|
-
result.set(
|
|
398
|
+
for (const { spec, acc } of accs) {
|
|
399
|
+
result.set(spec.alias, finalizeAccumulator(spec.funcName, acc))
|
|
418
400
|
}
|
|
419
401
|
return result
|
|
420
402
|
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Reduces one accumulator to its final aggregate value.
|
|
424
|
-
*
|
|
425
|
-
* @param {AggAccumulator} acc
|
|
426
|
-
* @returns {SqlPrimitive}
|
|
427
|
-
*/
|
|
428
|
-
function finalizeAgg(acc) {
|
|
429
|
-
switch (acc.spec.funcName) {
|
|
430
|
-
case 'COUNT': return acc.seen ? acc.seen.size : acc.count
|
|
431
|
-
case 'SUM': return acc.count === 0 ? null : acc.sum
|
|
432
|
-
case 'AVG': return acc.count === 0 ? null : acc.sum / acc.count
|
|
433
|
-
case 'MIN': return acc.min
|
|
434
|
-
case 'MAX': return acc.max
|
|
435
|
-
default: return null
|
|
436
|
-
}
|
|
437
|
-
}
|
package/src/execute/execute.js
CHANGED
|
@@ -174,7 +174,7 @@ function executeUnnest(plan, context) {
|
|
|
174
174
|
const value = await evaluateExpr({ node: plan.args[0], row, rowIndex: 1, context })
|
|
175
175
|
if (!Array.isArray(value)) return
|
|
176
176
|
for (const element of value) {
|
|
177
|
-
|
|
177
|
+
context.signal?.throwIfAborted()
|
|
178
178
|
yield {
|
|
179
179
|
columns,
|
|
180
180
|
cells: { [columnName]: () => Promise.resolve(element) },
|
|
@@ -209,7 +209,7 @@ function executeJsonEach(plan, context) {
|
|
|
209
209
|
}
|
|
210
210
|
if (Array.isArray(parsed)) {
|
|
211
211
|
for (let i = 0; i < parsed.length; i++) {
|
|
212
|
-
|
|
212
|
+
context.signal?.throwIfAborted()
|
|
213
213
|
const k = i
|
|
214
214
|
const v = parsed[i]
|
|
215
215
|
yield {
|
|
@@ -224,7 +224,7 @@ function executeJsonEach(plan, context) {
|
|
|
224
224
|
}
|
|
225
225
|
if (typeof parsed === 'object' && parsed !== null) {
|
|
226
226
|
for (const [k, v] of Object.entries(parsed)) {
|
|
227
|
-
|
|
227
|
+
context.signal?.throwIfAborted()
|
|
228
228
|
yield {
|
|
229
229
|
columns,
|
|
230
230
|
cells: {
|
|
@@ -294,7 +294,7 @@ function executeScan(plan, context) {
|
|
|
294
294
|
async *rows() {
|
|
295
295
|
const columns = [column]
|
|
296
296
|
for await (const chunk of chunks) {
|
|
297
|
-
|
|
297
|
+
signal?.throwIfAborted()
|
|
298
298
|
for (let i = 0; i < chunk.length; i++) {
|
|
299
299
|
const value = chunk[i]
|
|
300
300
|
yield {
|
|
@@ -303,6 +303,9 @@ function executeScan(plan, context) {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
+
// A data source may end its stream cooperatively on abort; surface
|
|
307
|
+
// the abort so a truncated scan is not mistaken for a complete one
|
|
308
|
+
signal?.throwIfAborted()
|
|
306
309
|
},
|
|
307
310
|
}
|
|
308
311
|
}
|
|
@@ -335,6 +338,10 @@ function executeScan(plan, context) {
|
|
|
335
338
|
}
|
|
336
339
|
|
|
337
340
|
yield* result
|
|
341
|
+
|
|
342
|
+
// A data source may end its stream cooperatively on abort; surface
|
|
343
|
+
// the abort so a truncated scan is not mistaken for a complete one
|
|
344
|
+
signal?.throwIfAborted()
|
|
338
345
|
},
|
|
339
346
|
}
|
|
340
347
|
}
|
|
@@ -420,10 +427,10 @@ async function* filterRows(rows, condition, context, limit) {
|
|
|
420
427
|
let buffer = []
|
|
421
428
|
|
|
422
429
|
for await (const row of rows) {
|
|
423
|
-
|
|
430
|
+
context.signal?.throwIfAborted()
|
|
424
431
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
425
432
|
await yieldToEventLoop()
|
|
426
|
-
|
|
433
|
+
context.signal?.throwIfAborted()
|
|
427
434
|
}
|
|
428
435
|
rowIndex++
|
|
429
436
|
buffer.push({ row, rowIndex })
|
|
@@ -466,10 +473,10 @@ async function* limitRows(rows, limit = Infinity, offset = 0, signal) {
|
|
|
466
473
|
let yielded = 0
|
|
467
474
|
let innerCount = 0
|
|
468
475
|
for await (const row of rows) {
|
|
469
|
-
|
|
476
|
+
signal?.throwIfAborted()
|
|
470
477
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
471
478
|
await yieldToEventLoop()
|
|
472
|
-
|
|
479
|
+
signal?.throwIfAborted()
|
|
473
480
|
}
|
|
474
481
|
if (skipped < offset) {
|
|
475
482
|
skipped++
|
|
@@ -521,10 +528,10 @@ function executeProject(plan, context) {
|
|
|
521
528
|
let innerCount = 0
|
|
522
529
|
|
|
523
530
|
for await (const row of child.rows()) {
|
|
524
|
-
|
|
531
|
+
context.signal?.throwIfAborted()
|
|
525
532
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
526
533
|
await yieldToEventLoop()
|
|
527
|
-
|
|
534
|
+
context.signal?.throwIfAborted()
|
|
528
535
|
}
|
|
529
536
|
rowIndex++
|
|
530
537
|
const currentRowIndex = rowIndex
|
|
@@ -614,10 +621,10 @@ function executeDistinct(plan, context) {
|
|
|
614
621
|
let innerCount = 0
|
|
615
622
|
|
|
616
623
|
for await (const row of child.rows()) {
|
|
617
|
-
|
|
624
|
+
signal?.throwIfAborted()
|
|
618
625
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
619
626
|
await yieldToEventLoop()
|
|
620
|
-
|
|
627
|
+
signal?.throwIfAborted()
|
|
621
628
|
}
|
|
622
629
|
buffer.push(row)
|
|
623
630
|
|
|
@@ -701,10 +708,10 @@ function executeSetOperation(plan, context) {
|
|
|
701
708
|
const seen = new Set()
|
|
702
709
|
let count = 0
|
|
703
710
|
for await (const row of left.rows()) {
|
|
704
|
-
|
|
711
|
+
signal?.throwIfAborted()
|
|
705
712
|
if (++count % YIELD_INTERVAL === 0) {
|
|
706
713
|
await yieldToEventLoop()
|
|
707
|
-
|
|
714
|
+
signal?.throwIfAborted()
|
|
708
715
|
}
|
|
709
716
|
const key = await stableRowKey(row)
|
|
710
717
|
if (!seen.has(key)) {
|
|
@@ -713,10 +720,10 @@ function executeSetOperation(plan, context) {
|
|
|
713
720
|
}
|
|
714
721
|
}
|
|
715
722
|
for await (const row of right.rows()) {
|
|
716
|
-
|
|
723
|
+
signal?.throwIfAborted()
|
|
717
724
|
if (++count % YIELD_INTERVAL === 0) {
|
|
718
725
|
await yieldToEventLoop()
|
|
719
|
-
|
|
726
|
+
signal?.throwIfAborted()
|
|
720
727
|
}
|
|
721
728
|
const key = await stableRowKey(row)
|
|
722
729
|
if (!seen.has(key)) {
|
|
@@ -739,10 +746,10 @@ function executeSetOperation(plan, context) {
|
|
|
739
746
|
const rightKeys = new Map()
|
|
740
747
|
let tick = 0
|
|
741
748
|
for await (const row of right.rows()) {
|
|
742
|
-
|
|
749
|
+
signal?.throwIfAborted()
|
|
743
750
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
744
751
|
await yieldToEventLoop()
|
|
745
|
-
|
|
752
|
+
signal?.throwIfAborted()
|
|
746
753
|
}
|
|
747
754
|
const key = await stableRowKey(row)
|
|
748
755
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
@@ -751,10 +758,10 @@ function executeSetOperation(plan, context) {
|
|
|
751
758
|
if (plan.all) {
|
|
752
759
|
// INTERSECT ALL: yield each left row that matches, consuming right counts
|
|
753
760
|
for await (const row of left.rows()) {
|
|
754
|
-
|
|
761
|
+
signal?.throwIfAborted()
|
|
755
762
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
756
763
|
await yieldToEventLoop()
|
|
757
|
-
|
|
764
|
+
signal?.throwIfAborted()
|
|
758
765
|
}
|
|
759
766
|
const key = await stableRowKey(row)
|
|
760
767
|
const count = rightKeys.get(key)
|
|
@@ -767,10 +774,10 @@ function executeSetOperation(plan, context) {
|
|
|
767
774
|
// INTERSECT: yield deduplicated rows present in both
|
|
768
775
|
const seen = new Set()
|
|
769
776
|
for await (const row of left.rows()) {
|
|
770
|
-
|
|
777
|
+
signal?.throwIfAborted()
|
|
771
778
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
772
779
|
await yieldToEventLoop()
|
|
773
|
-
|
|
780
|
+
signal?.throwIfAborted()
|
|
774
781
|
}
|
|
775
782
|
const key = await stableRowKey(row)
|
|
776
783
|
if (rightKeys.has(key) && !seen.has(key)) {
|
|
@@ -794,10 +801,10 @@ function executeSetOperation(plan, context) {
|
|
|
794
801
|
const rightKeys = new Map()
|
|
795
802
|
let tick = 0
|
|
796
803
|
for await (const row of right.rows()) {
|
|
797
|
-
|
|
804
|
+
signal?.throwIfAborted()
|
|
798
805
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
799
806
|
await yieldToEventLoop()
|
|
800
|
-
|
|
807
|
+
signal?.throwIfAborted()
|
|
801
808
|
}
|
|
802
809
|
const key = await stableRowKey(row)
|
|
803
810
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
@@ -806,10 +813,10 @@ function executeSetOperation(plan, context) {
|
|
|
806
813
|
if (plan.all) {
|
|
807
814
|
// EXCEPT ALL: yield left rows, consuming right counts
|
|
808
815
|
for await (const row of left.rows()) {
|
|
809
|
-
|
|
816
|
+
signal?.throwIfAborted()
|
|
810
817
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
811
818
|
await yieldToEventLoop()
|
|
812
|
-
|
|
819
|
+
signal?.throwIfAborted()
|
|
813
820
|
}
|
|
814
821
|
const key = await stableRowKey(row)
|
|
815
822
|
const count = rightKeys.get(key)
|
|
@@ -823,10 +830,10 @@ function executeSetOperation(plan, context) {
|
|
|
823
830
|
// EXCEPT: yield deduplicated left rows not in right
|
|
824
831
|
const seen = new Set()
|
|
825
832
|
for await (const row of left.rows()) {
|
|
826
|
-
|
|
833
|
+
signal?.throwIfAborted()
|
|
827
834
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
828
835
|
await yieldToEventLoop()
|
|
829
|
-
|
|
836
|
+
signal?.throwIfAborted()
|
|
830
837
|
}
|
|
831
838
|
const key = await stableRowKey(row)
|
|
832
839
|
if (!rightKeys.has(key) && !seen.has(key)) {
|
package/src/execute/join.js
CHANGED
|
@@ -56,7 +56,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
56
56
|
/** @type {AsyncRow[]} */
|
|
57
57
|
const innerRows = []
|
|
58
58
|
for await (const row of inner.rows()) {
|
|
59
|
-
|
|
59
|
+
context.signal?.throwIfAborted()
|
|
60
60
|
innerRows.push(row)
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -69,7 +69,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
69
69
|
|
|
70
70
|
let innerCount = 0
|
|
71
71
|
for await (const outerRow of outer.rows()) {
|
|
72
|
-
|
|
72
|
+
context.signal?.throwIfAborted()
|
|
73
73
|
|
|
74
74
|
if (!outerCols) {
|
|
75
75
|
outerCols = outerRow.columns
|
|
@@ -80,7 +80,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
80
80
|
for (const innerRow of innerRows) {
|
|
81
81
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
82
82
|
await yieldToEventLoop()
|
|
83
|
-
|
|
83
|
+
context.signal?.throwIfAborted()
|
|
84
84
|
}
|
|
85
85
|
const tempMerged = merge(outerRow, innerRow)
|
|
86
86
|
const matches = await evaluateExpr({
|
|
@@ -101,7 +101,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
context.signal?.throwIfAborted()
|
|
105
105
|
|
|
106
106
|
// Unmatched inner rows for outer joins on the buffered side
|
|
107
107
|
if (matchedInnerRows) {
|
|
@@ -134,7 +134,7 @@ function executeLateralJoin(plan, context) {
|
|
|
134
134
|
const rightTable = plan.rightAlias
|
|
135
135
|
|
|
136
136
|
for await (const leftRow of left.rows()) {
|
|
137
|
-
|
|
137
|
+
context.signal?.throwIfAborted()
|
|
138
138
|
|
|
139
139
|
// When nested inside a correlated subquery, preserve the enclosing
|
|
140
140
|
// outer row so UNNEST args can reference its columns (e.g. o.arr).
|
|
@@ -146,7 +146,7 @@ function executeLateralJoin(plan, context) {
|
|
|
146
146
|
|
|
147
147
|
let hasMatch = false
|
|
148
148
|
for await (const rightRow of right.rows()) {
|
|
149
|
-
|
|
149
|
+
context.signal?.throwIfAborted()
|
|
150
150
|
const merged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
151
151
|
const matches = plan.condition === undefined
|
|
152
152
|
? true
|
|
@@ -199,10 +199,10 @@ export function executePositionalJoin(plan, context) {
|
|
|
199
199
|
while (true) {
|
|
200
200
|
const [leftResult, rightResult] = await Promise.all([leftRows.next(), rightRows.next()])
|
|
201
201
|
if (leftResult.done && rightResult.done) return
|
|
202
|
-
|
|
202
|
+
signal?.throwIfAborted()
|
|
203
203
|
if (++tick % YIELD_INTERVAL === 0) {
|
|
204
204
|
await yieldToEventLoop()
|
|
205
|
-
|
|
205
|
+
signal?.throwIfAborted()
|
|
206
206
|
}
|
|
207
207
|
if (!leftResult.done && !leftCols.length) leftCols = leftResult.value.columns
|
|
208
208
|
if (!rightResult.done && !rightCols.length) rightCols = rightResult.value.columns
|
|
@@ -266,10 +266,10 @@ export function executeHashJoin(plan, context) {
|
|
|
266
266
|
let buildCols = []
|
|
267
267
|
let innerCount = 0
|
|
268
268
|
for await (const buildRow of build.rows()) {
|
|
269
|
-
|
|
269
|
+
context.signal?.throwIfAborted()
|
|
270
270
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
271
271
|
await yieldToEventLoop()
|
|
272
|
-
|
|
272
|
+
context.signal?.throwIfAborted()
|
|
273
273
|
}
|
|
274
274
|
if (!buildCols.length) {
|
|
275
275
|
buildCols = buildRow.columns
|
|
@@ -297,7 +297,7 @@ export function executeHashJoin(plan, context) {
|
|
|
297
297
|
|
|
298
298
|
// Probe phase: stream the other side
|
|
299
299
|
for await (const probeRow of probe.rows()) {
|
|
300
|
-
|
|
300
|
+
context.signal?.throwIfAborted()
|
|
301
301
|
|
|
302
302
|
if (!probeCols) {
|
|
303
303
|
probeCols = probeRow.columns
|
|
@@ -314,7 +314,7 @@ export function executeHashJoin(plan, context) {
|
|
|
314
314
|
for (const buildRow of candidates) {
|
|
315
315
|
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
316
316
|
await yieldToEventLoop()
|
|
317
|
-
|
|
317
|
+
context.signal?.throwIfAborted()
|
|
318
318
|
}
|
|
319
319
|
const merged = merge(probeRow, buildRow)
|
|
320
320
|
if (residual) {
|
|
@@ -333,7 +333,7 @@ export function executeHashJoin(plan, context) {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
|
|
336
|
+
context.signal?.throwIfAborted()
|
|
337
337
|
|
|
338
338
|
// Unmatched build rows for outer joins on the build side
|
|
339
339
|
if (buildRows && matchedBuildRows) {
|
package/src/execute/sort.js
CHANGED
|
@@ -4,16 +4,21 @@ import { executePlan } from './execute.js'
|
|
|
4
4
|
import { compareForTerm } from './utils.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @import { AsyncRow, ExecuteContext, OrderByItem, QueryResults, SqlPrimitive } from '../types.js'
|
|
7
|
+
* @import { AsyncRow, ExecuteContext, ExprNode, OrderByItem, QueryResults, SqlPrimitive } from '../types.js'
|
|
8
8
|
* @import { SortNode } from '../plan/types.js'
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const MAX_CHUNK = 256
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
+
* When exprs is set, exprs[i] is evaluated for ORDER BY term i instead of the
|
|
15
|
+
* term's own expression, so callers can pre-substitute per-entry values (for
|
|
16
|
+
* example, finalized aggregates) while keeping tie-aware term evaluation.
|
|
17
|
+
*
|
|
14
18
|
* @typedef {{
|
|
15
19
|
* row: AsyncRow,
|
|
16
20
|
* rows?: AsyncRow[],
|
|
21
|
+
* exprs?: ExprNode[],
|
|
17
22
|
* }} SortEntry
|
|
18
23
|
*/
|
|
19
24
|
|
|
@@ -63,7 +68,7 @@ export async function sortEntriesByTerms({ entries, orderBy, context, cacheValue
|
|
|
63
68
|
const chunk = missing.slice(start, start + chunkSize)
|
|
64
69
|
const values = await Promise.all(chunk.map(idx =>
|
|
65
70
|
evaluateExpr({
|
|
66
|
-
node: term.expr,
|
|
71
|
+
node: entries[idx].exprs?.[orderByIdx] ?? term.expr,
|
|
67
72
|
row: entries[idx].row,
|
|
68
73
|
rows: entries[idx].rows,
|
|
69
74
|
context,
|
|
@@ -141,7 +146,7 @@ export function executeSort(plan, context) {
|
|
|
141
146
|
/** @type {SortEntry[]} */
|
|
142
147
|
let entries = []
|
|
143
148
|
for await (const row of child.rows()) {
|
|
144
|
-
|
|
149
|
+
context.signal?.throwIfAborted()
|
|
145
150
|
entries.push({ row })
|
|
146
151
|
if (entries.length >= bufferLimit) {
|
|
147
152
|
// Sort keys are cached on the rows, so survivors of one truncation
|