squirreling 0.12.20 → 0.12.22
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 +5 -5
- package/src/execute/aggregates.js +52 -11
- package/src/execute/execute.js +60 -1
- package/src/execute/join.js +14 -0
- package/src/execute/sort.js +1 -1
- package/src/execute/window.js +59 -19
- package/src/execute/yield.js +30 -0
- package/src/expression/evaluate.js +52 -28
- package/src/parse/primary.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.22",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"test": "vitest run"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "25.
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
42
|
+
"@types/node": "25.9.1",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.7",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
|
-
"eslint-plugin-jsdoc": "
|
|
45
|
+
"eslint-plugin-jsdoc": "63.0.0",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.7"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -3,12 +3,16 @@ import { evaluateExpr } from '../expression/evaluate.js'
|
|
|
3
3
|
import { executePlan, selectColumnNames } from './execute.js'
|
|
4
4
|
import { sortEntriesByTerms } from './sort.js'
|
|
5
5
|
import { keyify } from './utils.js'
|
|
6
|
+
import { yieldToEventLoop } from './yield.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, QueryResults, SelectColumn, SqlPrimitive } from '../types.js'
|
|
9
10
|
* @import { HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
// Yield to the event loop every this many iterations so that aborts can actually fire
|
|
14
|
+
const YIELD_INTERVAL = 4000
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Projects aggregate columns from a group of rows
|
|
14
18
|
*
|
|
@@ -83,23 +87,56 @@ export function executeHashAggregate(plan, context) {
|
|
|
83
87
|
// Collect all rows
|
|
84
88
|
/** @type {AsyncRow[]} */
|
|
85
89
|
const allRows = []
|
|
90
|
+
let collectCount = 0
|
|
86
91
|
for await (const row of child.rows()) {
|
|
87
|
-
if (
|
|
92
|
+
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
93
|
+
await yieldToEventLoop()
|
|
94
|
+
if (context.signal?.aborted) return
|
|
95
|
+
}
|
|
88
96
|
allRows.push(row)
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
// Group rows by GROUP BY keys
|
|
99
|
+
// Group rows by GROUP BY keys.
|
|
100
|
+
// Each chunk dispatches all per-row key evaluations in parallel so
|
|
101
|
+
// async cells (e.g. lazy parquet decode) overlap; the await is at the
|
|
102
|
+
// chunk boundary. Synchronous cells stay cheap because we skip the
|
|
103
|
+
// inner Promise.all wrapper when there's a single GROUP BY expression.
|
|
92
104
|
/** @type {Map<any, AsyncRow[]>} */
|
|
93
105
|
const groups = new Map()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
const { groupBy } = plan
|
|
107
|
+
const singleKey = groupBy.length === 1
|
|
108
|
+
const singleExpr = singleKey ? groupBy[0] : null
|
|
109
|
+
|
|
110
|
+
for (let chunkStart = 0; chunkStart < allRows.length; chunkStart += YIELD_INTERVAL) {
|
|
111
|
+
if (chunkStart > 0) {
|
|
112
|
+
await yieldToEventLoop()
|
|
113
|
+
if (context.signal?.aborted) return
|
|
114
|
+
}
|
|
115
|
+
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, allRows.length)
|
|
116
|
+
const chunkLen = chunkEnd - chunkStart
|
|
117
|
+
/** @type {Promise<any>[]} */
|
|
118
|
+
const pending = new Array(chunkLen)
|
|
119
|
+
if (singleKey) {
|
|
120
|
+
for (let j = 0; j < chunkLen; j++) {
|
|
121
|
+
pending[j] = evaluateExpr({ node: singleExpr, row: allRows[chunkStart + j], context })
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
for (let j = 0; j < chunkLen; j++) {
|
|
125
|
+
const row = allRows[chunkStart + j]
|
|
126
|
+
pending[j] = Promise.all(groupBy.map(expr => evaluateExpr({ node: expr, row, context })))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const chunkKeys = await Promise.all(pending)
|
|
130
|
+
for (let j = 0; j < chunkLen; j++) {
|
|
131
|
+
const key = singleKey ? keyify(chunkKeys[j]) : keyify(...chunkKeys[j])
|
|
132
|
+
const row = allRows[chunkStart + j]
|
|
133
|
+
let group = groups.get(key)
|
|
134
|
+
if (!group) {
|
|
135
|
+
group = []
|
|
136
|
+
groups.set(key, group)
|
|
137
|
+
}
|
|
138
|
+
group.push(row)
|
|
101
139
|
}
|
|
102
|
-
group.push(row)
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
/** @type {{ row: AsyncRow, rows: AsyncRow[], outputRow: AsyncRow }[]} */
|
|
@@ -166,8 +203,12 @@ export function executeScalarAggregate(plan, context) {
|
|
|
166
203
|
// Collect all rows into single group
|
|
167
204
|
/** @type {AsyncRow[]} */
|
|
168
205
|
const group = []
|
|
206
|
+
let collectCount = 0
|
|
169
207
|
for await (const row of child.rows()) {
|
|
170
|
-
if (
|
|
208
|
+
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
209
|
+
await yieldToEventLoop()
|
|
210
|
+
if (context.signal?.aborted) return
|
|
211
|
+
}
|
|
171
212
|
group.push(row)
|
|
172
213
|
}
|
|
173
214
|
|
package/src/execute/execute.js
CHANGED
|
@@ -10,12 +10,16 @@ import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from '.
|
|
|
10
10
|
import { executeSort } from './sort.js'
|
|
11
11
|
import { addBounds, minBounds, stableRowKey } from './utils.js'
|
|
12
12
|
import { executeWindow } from './window.js'
|
|
13
|
+
import { yieldToEventLoop } from './yield.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, ExecuteSqlOptions, ExprNode, IdentifierNode, QueryResults, SelectColumn, SqlPrimitive, Statement } from '../types.js'
|
|
16
17
|
* @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode, TableFunctionNode } from '../plan/types.js'
|
|
17
18
|
*/
|
|
18
19
|
|
|
20
|
+
// Yield to the event loop every 4000 iterations so that aborts can actually fire
|
|
21
|
+
const YIELD_INTERVAL = 4000
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* Executes a SQL SELECT query against tables
|
|
21
25
|
*
|
|
@@ -406,12 +410,17 @@ async function* filterRows(rows, condition, context, limit) {
|
|
|
406
410
|
let chunkSize = limit ?? 1
|
|
407
411
|
const grow = limit !== undefined
|
|
408
412
|
let rowIndex = 0
|
|
413
|
+
let innerCount = 0
|
|
409
414
|
|
|
410
415
|
/** @type {{ row: AsyncRow, rowIndex: number }[]} */
|
|
411
416
|
let buffer = []
|
|
412
417
|
|
|
413
418
|
for await (const row of rows) {
|
|
414
419
|
if (context.signal?.aborted) return
|
|
420
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
421
|
+
await yieldToEventLoop()
|
|
422
|
+
if (context.signal?.aborted) return
|
|
423
|
+
}
|
|
415
424
|
rowIndex++
|
|
416
425
|
buffer.push({ row, rowIndex })
|
|
417
426
|
|
|
@@ -451,8 +460,13 @@ async function* limitRows(rows, limit = Infinity, offset = 0, signal) {
|
|
|
451
460
|
if (limit <= 0) return
|
|
452
461
|
let skipped = 0
|
|
453
462
|
let yielded = 0
|
|
463
|
+
let innerCount = 0
|
|
454
464
|
for await (const row of rows) {
|
|
455
465
|
if (signal?.aborted) return
|
|
466
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
467
|
+
await yieldToEventLoop()
|
|
468
|
+
if (signal?.aborted) return
|
|
469
|
+
}
|
|
456
470
|
if (skipped < offset) {
|
|
457
471
|
skipped++
|
|
458
472
|
continue
|
|
@@ -500,16 +514,21 @@ function executeProject(plan, context) {
|
|
|
500
514
|
maxRows: child.maxRows,
|
|
501
515
|
async *rows() {
|
|
502
516
|
let rowIndex = 0
|
|
517
|
+
let innerCount = 0
|
|
503
518
|
|
|
504
519
|
for await (const row of child.rows()) {
|
|
505
520
|
if (context.signal?.aborted) return
|
|
521
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
522
|
+
await yieldToEventLoop()
|
|
523
|
+
if (context.signal?.aborted) return
|
|
524
|
+
}
|
|
506
525
|
rowIndex++
|
|
507
526
|
const currentRowIndex = rowIndex
|
|
508
527
|
|
|
509
528
|
/** @type {AsyncCells} */
|
|
510
529
|
const cells = {}
|
|
511
530
|
// Only safe to propagate resolved when every output column comes from
|
|
512
|
-
// the star branch
|
|
531
|
+
// the star branch. Derived expressions evaluate lazily and can't be
|
|
513
532
|
// pre-materialized here, and a partial resolved would make
|
|
514
533
|
// collect()/downstream identifier fast paths read undefined.
|
|
515
534
|
const source = resolveable ? row.resolved : undefined
|
|
@@ -588,9 +607,14 @@ function executeDistinct(plan, context) {
|
|
|
588
607
|
|
|
589
608
|
/** @type {AsyncRow[]} */
|
|
590
609
|
let buffer = []
|
|
610
|
+
let innerCount = 0
|
|
591
611
|
|
|
592
612
|
for await (const row of child.rows()) {
|
|
593
613
|
if (signal?.aborted) return
|
|
614
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
615
|
+
await yieldToEventLoop()
|
|
616
|
+
if (signal?.aborted) return
|
|
617
|
+
}
|
|
594
618
|
buffer.push(row)
|
|
595
619
|
|
|
596
620
|
if (buffer.length >= MAX_CHUNK) {
|
|
@@ -671,8 +695,13 @@ function executeSetOperation(plan, context) {
|
|
|
671
695
|
async *rows() {
|
|
672
696
|
// UNION: yield deduplicated rows from both sides
|
|
673
697
|
const seen = new Set()
|
|
698
|
+
let count = 0
|
|
674
699
|
for await (const row of left.rows()) {
|
|
675
700
|
if (signal?.aborted) return
|
|
701
|
+
if (++count % YIELD_INTERVAL === 0) {
|
|
702
|
+
await yieldToEventLoop()
|
|
703
|
+
if (signal?.aborted) return
|
|
704
|
+
}
|
|
676
705
|
const key = await stableRowKey(row)
|
|
677
706
|
if (!seen.has(key)) {
|
|
678
707
|
seen.add(key)
|
|
@@ -681,6 +710,10 @@ function executeSetOperation(plan, context) {
|
|
|
681
710
|
}
|
|
682
711
|
for await (const row of right.rows()) {
|
|
683
712
|
if (signal?.aborted) return
|
|
713
|
+
if (++count % YIELD_INTERVAL === 0) {
|
|
714
|
+
await yieldToEventLoop()
|
|
715
|
+
if (signal?.aborted) return
|
|
716
|
+
}
|
|
684
717
|
const key = await stableRowKey(row)
|
|
685
718
|
if (!seen.has(key)) {
|
|
686
719
|
seen.add(key)
|
|
@@ -700,8 +733,13 @@ function executeSetOperation(plan, context) {
|
|
|
700
733
|
// Materialize right side keys
|
|
701
734
|
/** @type {Map<any, number>} */
|
|
702
735
|
const rightKeys = new Map()
|
|
736
|
+
let tick = 0
|
|
703
737
|
for await (const row of right.rows()) {
|
|
704
738
|
if (signal?.aborted) return
|
|
739
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
740
|
+
await yieldToEventLoop()
|
|
741
|
+
if (signal?.aborted) return
|
|
742
|
+
}
|
|
705
743
|
const key = await stableRowKey(row)
|
|
706
744
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
707
745
|
}
|
|
@@ -710,6 +748,10 @@ function executeSetOperation(plan, context) {
|
|
|
710
748
|
// INTERSECT ALL: yield each left row that matches, consuming right counts
|
|
711
749
|
for await (const row of left.rows()) {
|
|
712
750
|
if (signal?.aborted) return
|
|
751
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
752
|
+
await yieldToEventLoop()
|
|
753
|
+
if (signal?.aborted) return
|
|
754
|
+
}
|
|
713
755
|
const key = await stableRowKey(row)
|
|
714
756
|
const count = rightKeys.get(key)
|
|
715
757
|
if (count) {
|
|
@@ -722,6 +764,10 @@ function executeSetOperation(plan, context) {
|
|
|
722
764
|
const seen = new Set()
|
|
723
765
|
for await (const row of left.rows()) {
|
|
724
766
|
if (signal?.aborted) return
|
|
767
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
768
|
+
await yieldToEventLoop()
|
|
769
|
+
if (signal?.aborted) return
|
|
770
|
+
}
|
|
725
771
|
const key = await stableRowKey(row)
|
|
726
772
|
if (rightKeys.has(key) && !seen.has(key)) {
|
|
727
773
|
seen.add(key)
|
|
@@ -742,8 +788,13 @@ function executeSetOperation(plan, context) {
|
|
|
742
788
|
// Materialize right side keys
|
|
743
789
|
/** @type {Map<any, number>} */
|
|
744
790
|
const rightKeys = new Map()
|
|
791
|
+
let tick = 0
|
|
745
792
|
for await (const row of right.rows()) {
|
|
746
793
|
if (signal?.aborted) return
|
|
794
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
795
|
+
await yieldToEventLoop()
|
|
796
|
+
if (signal?.aborted) return
|
|
797
|
+
}
|
|
747
798
|
const key = await stableRowKey(row)
|
|
748
799
|
rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
|
|
749
800
|
}
|
|
@@ -752,6 +803,10 @@ function executeSetOperation(plan, context) {
|
|
|
752
803
|
// EXCEPT ALL: yield left rows, consuming right counts
|
|
753
804
|
for await (const row of left.rows()) {
|
|
754
805
|
if (signal?.aborted) return
|
|
806
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
807
|
+
await yieldToEventLoop()
|
|
808
|
+
if (signal?.aborted) return
|
|
809
|
+
}
|
|
755
810
|
const key = await stableRowKey(row)
|
|
756
811
|
const count = rightKeys.get(key)
|
|
757
812
|
if (count) {
|
|
@@ -765,6 +820,10 @@ function executeSetOperation(plan, context) {
|
|
|
765
820
|
const seen = new Set()
|
|
766
821
|
for await (const row of left.rows()) {
|
|
767
822
|
if (signal?.aborted) return
|
|
823
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
824
|
+
await yieldToEventLoop()
|
|
825
|
+
if (signal?.aborted) return
|
|
826
|
+
}
|
|
768
827
|
const key = await stableRowKey(row)
|
|
769
828
|
if (!rightKeys.has(key) && !seen.has(key)) {
|
|
770
829
|
seen.add(key)
|
package/src/execute/join.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
2
2
|
import { keyify, maxBounds } from './utils.js'
|
|
3
3
|
import { executePlan } from './execute.js'
|
|
4
|
+
import { yieldToEventLoop } from './yield.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @import { AsyncCells, AsyncRow, ExecuteContext, QueryResults } from '../types.js'
|
|
7
8
|
* @import { HashJoinNode, NestedLoopJoinNode, PositionalJoinNode } from '../plan/types.js'
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
// Yield to the event loop every 4000 iterations so that aborts can actually fire
|
|
12
|
+
const YIELD_INTERVAL = 4000
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Executes a nested loop join operation
|
|
12
16
|
*
|
|
@@ -41,6 +45,7 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
41
45
|
/** @type {Set<AsyncRow> | undefined} */
|
|
42
46
|
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
43
47
|
|
|
48
|
+
let innerCount = 0
|
|
44
49
|
for await (const leftRow of left.rows()) {
|
|
45
50
|
if (context.signal?.aborted) break
|
|
46
51
|
|
|
@@ -51,6 +56,10 @@ export function executeNestedLoopJoin(plan, context) {
|
|
|
51
56
|
let hasMatch = false
|
|
52
57
|
|
|
53
58
|
for (const rightRow of rightRows) {
|
|
59
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
60
|
+
await yieldToEventLoop()
|
|
61
|
+
if (context.signal?.aborted) return
|
|
62
|
+
}
|
|
54
63
|
const tempMerged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
55
64
|
const matches = await evaluateExpr({
|
|
56
65
|
node: plan.condition,
|
|
@@ -237,6 +246,7 @@ export function executeHashJoin(plan, context) {
|
|
|
237
246
|
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
238
247
|
|
|
239
248
|
// Probe phase: stream left rows
|
|
249
|
+
let innerCount = 0
|
|
240
250
|
for await (const leftRow of left.rows()) {
|
|
241
251
|
if (context.signal?.aborted) break
|
|
242
252
|
|
|
@@ -253,6 +263,10 @@ export function executeHashJoin(plan, context) {
|
|
|
253
263
|
const candidates = hashMap.get(key)
|
|
254
264
|
if (candidates?.length) {
|
|
255
265
|
for (const rightRow of candidates) {
|
|
266
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
267
|
+
await yieldToEventLoop()
|
|
268
|
+
if (context.signal?.aborted) return
|
|
269
|
+
}
|
|
256
270
|
const merged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
257
271
|
if (residual) {
|
|
258
272
|
const ok = await evaluateExpr({ node: residual, row: merged, context })
|
package/src/execute/sort.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function sortEntriesByTerms({ entries, orderBy, context, cacheValue
|
|
|
59
59
|
let chunkSize = 1
|
|
60
60
|
let start = 0
|
|
61
61
|
while (start < missing.length) {
|
|
62
|
-
|
|
62
|
+
context.signal?.throwIfAborted()
|
|
63
63
|
const chunk = missing.slice(start, start + chunkSize)
|
|
64
64
|
const values = await Promise.all(chunk.map(idx =>
|
|
65
65
|
evaluateExpr({
|
package/src/execute/window.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
2
2
|
import { executePlan } from './execute.js'
|
|
3
3
|
import { compareForTerm, keyify } from './utils.js'
|
|
4
|
+
import { yieldToEventLoop } from './yield.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @import { AsyncRow, ExecuteContext, QueryResults, SqlPrimitive } from '../types.js'
|
|
7
8
|
* @import { WindowNode, WindowSpec } from '../plan/types.js'
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
// Yield to the event loop every 4000 iterations so that aborts can actually fire
|
|
12
|
+
const YIELD_INTERVAL = 4000
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Executes a Window plan node: buffers the child's rows, assigns each window
|
|
12
16
|
* function's output per partition, and yields rows in input order with the
|
|
@@ -36,8 +40,10 @@ export function executeWindow(plan, context) {
|
|
|
36
40
|
async *rows() {
|
|
37
41
|
let i = 0
|
|
38
42
|
for await (const row of child.rows()) {
|
|
39
|
-
if (
|
|
40
|
-
|
|
43
|
+
if (++i % YIELD_INTERVAL === 0) {
|
|
44
|
+
await yieldToEventLoop()
|
|
45
|
+
if (context.signal?.aborted) return
|
|
46
|
+
}
|
|
41
47
|
const cells = { ...row.cells }
|
|
42
48
|
for (const w of plan.windows) {
|
|
43
49
|
const value = i
|
|
@@ -59,8 +65,12 @@ export function executeWindow(plan, context) {
|
|
|
59
65
|
async *rows() {
|
|
60
66
|
/** @type {AsyncRow[]} */
|
|
61
67
|
const rows = []
|
|
68
|
+
let collectCount = 0
|
|
62
69
|
for await (const row of child.rows()) {
|
|
63
|
-
if (
|
|
70
|
+
if (++collectCount % YIELD_INTERVAL === 0) {
|
|
71
|
+
await yieldToEventLoop()
|
|
72
|
+
if (context.signal?.aborted) return
|
|
73
|
+
}
|
|
64
74
|
rows.push(row)
|
|
65
75
|
}
|
|
66
76
|
if (rows.length === 0) return
|
|
@@ -74,8 +84,12 @@ export function executeWindow(plan, context) {
|
|
|
74
84
|
if (context.signal?.aborted) return
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
let emitCount = 0
|
|
77
88
|
for (let i = 0; i < rows.length; i++) {
|
|
78
|
-
if (
|
|
89
|
+
if (++emitCount % YIELD_INTERVAL === 0) {
|
|
90
|
+
await yieldToEventLoop()
|
|
91
|
+
if (context.signal?.aborted) return
|
|
92
|
+
}
|
|
79
93
|
const row = rows[i]
|
|
80
94
|
const cells = { ...row.cells }
|
|
81
95
|
for (let w = 0; w < plan.windows.length; w++) {
|
|
@@ -105,17 +119,26 @@ async function computeWindow(spec, rows, output, context) {
|
|
|
105
119
|
// Bucket row indices by partition key.
|
|
106
120
|
/** @type {Map<string | number | bigint | boolean, number[]>} */
|
|
107
121
|
const partitions = new Map()
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
for (let chunkStart = 0; chunkStart < rows.length; chunkStart += YIELD_INTERVAL) {
|
|
123
|
+
if (chunkStart > 0) {
|
|
124
|
+
await yieldToEventLoop()
|
|
125
|
+
if (context.signal?.aborted) return
|
|
126
|
+
}
|
|
127
|
+
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, rows.length)
|
|
128
|
+
const chunkKeys = await Promise.all(
|
|
129
|
+
rows.slice(chunkStart, chunkEnd).map(row =>
|
|
130
|
+
Promise.all(spec.partitionBy.map(expr => evaluateExpr({ node: expr, row, context })))
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
for (let j = 0; j < chunkKeys.length; j++) {
|
|
134
|
+
const key = keyify(...chunkKeys[j])
|
|
135
|
+
let bucket = partitions.get(key)
|
|
136
|
+
if (!bucket) {
|
|
137
|
+
bucket = []
|
|
138
|
+
partitions.set(key, bucket)
|
|
139
|
+
}
|
|
140
|
+
bucket.push(chunkStart + j)
|
|
117
141
|
}
|
|
118
|
-
bucket.push(i)
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
for (const bucket of partitions.values()) {
|
|
@@ -125,11 +148,24 @@ async function computeWindow(spec, rows, output, context) {
|
|
|
125
148
|
/** @type {number[]} */
|
|
126
149
|
let ordered
|
|
127
150
|
if (spec.orderBy.length) {
|
|
128
|
-
const orderValues = await Promise.all(bucket.map(idx =>
|
|
129
|
-
Promise.all(spec.orderBy.map(term => evaluateExpr({ node: term.expr, row: rows[idx], context })))
|
|
130
|
-
))
|
|
131
151
|
/** @type {{ idx: number, values: SqlPrimitive[], pos: number }[]} */
|
|
132
|
-
const entries = bucket.
|
|
152
|
+
const entries = new Array(bucket.length)
|
|
153
|
+
for (let chunkStart = 0; chunkStart < bucket.length; chunkStart += YIELD_INTERVAL) {
|
|
154
|
+
if (chunkStart > 0) {
|
|
155
|
+
await yieldToEventLoop()
|
|
156
|
+
if (context.signal?.aborted) return
|
|
157
|
+
}
|
|
158
|
+
const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, bucket.length)
|
|
159
|
+
const chunkValues = await Promise.all(
|
|
160
|
+
bucket.slice(chunkStart, chunkEnd).map(idx =>
|
|
161
|
+
Promise.all(spec.orderBy.map(term => evaluateExpr({ node: term.expr, row: rows[idx], context })))
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
for (let j = 0; j < chunkValues.length; j++) {
|
|
165
|
+
const k = chunkStart + j
|
|
166
|
+
entries[k] = { idx: bucket[k], values: chunkValues[j], pos: k }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
133
169
|
entries.sort((a, b) => {
|
|
134
170
|
for (let i = 0; i < spec.orderBy.length; i++) {
|
|
135
171
|
const cmp = compareForTerm(a.values[i], b.values[i], spec.orderBy[i])
|
|
@@ -165,8 +201,12 @@ async function applyWindowFunction(spec, ordered, rows, output, context) {
|
|
|
165
201
|
if (spec.funcName === 'LAG' || spec.funcName === 'LEAD') {
|
|
166
202
|
const direction = spec.funcName === 'LAG' ? -1 : 1
|
|
167
203
|
const [valueExpr, offsetExpr, defaultExpr] = spec.args
|
|
204
|
+
let tick = 0
|
|
168
205
|
for (let k = 0; k < ordered.length; k++) {
|
|
169
|
-
if (
|
|
206
|
+
if (++tick % YIELD_INTERVAL === 0) {
|
|
207
|
+
await yieldToEventLoop()
|
|
208
|
+
if (context.signal?.aborted) return
|
|
209
|
+
}
|
|
170
210
|
const idx = ordered[k]
|
|
171
211
|
const row = rows[idx]
|
|
172
212
|
const offset = offsetExpr
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Yield to the event loop so that timer-based aborts can fire.
|
|
2
|
+
//
|
|
3
|
+
// In Node, setTimeout(fn, 0) is clamped to a 1ms minimum, so a tight loop
|
|
4
|
+
// that yields every few thousand iterations can spend hundreds of ms in
|
|
5
|
+
// scheduling overhead alone. setImmediate (Node) and MessageChannel
|
|
6
|
+
// (browsers) provide the same macrotask boundary at a fraction of the cost.
|
|
7
|
+
//
|
|
8
|
+
// We need a macrotask boundary (not just a microtask) because the abort
|
|
9
|
+
// timer itself is a macrotask; microtasks alone cannot let it fire.
|
|
10
|
+
|
|
11
|
+
/** @type {() => Promise<void>} */
|
|
12
|
+
export const yieldToEventLoop = (() => {
|
|
13
|
+
if (typeof setImmediate === 'function') {
|
|
14
|
+
return () => new Promise(resolve => setImmediate(resolve))
|
|
15
|
+
}
|
|
16
|
+
if (typeof MessageChannel !== 'undefined') {
|
|
17
|
+
const channel = new MessageChannel()
|
|
18
|
+
/** @type {Array<() => void>} */
|
|
19
|
+
const queue = []
|
|
20
|
+
channel.port1.onmessage = () => {
|
|
21
|
+
const resolve = queue.shift()
|
|
22
|
+
if (resolve) resolve()
|
|
23
|
+
}
|
|
24
|
+
return () => new Promise(resolve => {
|
|
25
|
+
queue.push(resolve)
|
|
26
|
+
channel.port2.postMessage(0)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
return () => new Promise(resolve => setTimeout(resolve, 0))
|
|
30
|
+
})()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { executeStatement } from '../execute/execute.js'
|
|
2
2
|
import { isPlainObject, keyify, sqlEquals, stringify } from '../execute/utils.js'
|
|
3
|
+
import { yieldToEventLoop } from '../execute/yield.js'
|
|
3
4
|
import { ArgValueError, ExecutionError } from '../validation/executionErrors.js'
|
|
4
5
|
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
|
|
5
6
|
import { UnknownFunctionError } from '../validation/parseErrors.js'
|
|
@@ -16,6 +17,40 @@ import { evaluateStringFunc } from './strings.js'
|
|
|
16
17
|
* @import { ExprNode, AsyncRow, ExecuteContext, SqlPrimitive } from '../types.js'
|
|
17
18
|
*/
|
|
18
19
|
|
|
20
|
+
// Yield to the event loop every this many iterations so that aborts can actually fire
|
|
21
|
+
const YIELD_INTERVAL = 4000
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Evaluates an expression for each row, yielding to the event loop every
|
|
25
|
+
* YIELD_INTERVAL rows so signal-based aborts can fire mid-evaluation.
|
|
26
|
+
*
|
|
27
|
+
* @param {ExprNode} node
|
|
28
|
+
* @param {AsyncRow[]} rows
|
|
29
|
+
* @param {ExecuteContext} context
|
|
30
|
+
* @returns {Promise<SqlPrimitive[]>}
|
|
31
|
+
*/
|
|
32
|
+
async function evaluateAll(node, rows, context) {
|
|
33
|
+
/** @type {SqlPrimitive[]} */
|
|
34
|
+
const results = new Array(rows.length)
|
|
35
|
+
/** @type {Promise<SqlPrimitive>[]} */
|
|
36
|
+
const pending = new Array(Math.min(YIELD_INTERVAL, rows.length))
|
|
37
|
+
for (let i = 0; i < rows.length; i += YIELD_INTERVAL) {
|
|
38
|
+
if (i > 0) {
|
|
39
|
+
await yieldToEventLoop()
|
|
40
|
+
context.signal?.throwIfAborted()
|
|
41
|
+
}
|
|
42
|
+
const end = Math.min(i + YIELD_INTERVAL, rows.length)
|
|
43
|
+
const chunkLen = end - i
|
|
44
|
+
pending.length = chunkLen
|
|
45
|
+
for (let j = 0; j < chunkLen; j++) {
|
|
46
|
+
pending[j] = evaluateExpr({ node, row: rows[i + j], context })
|
|
47
|
+
}
|
|
48
|
+
const chunk = await Promise.all(pending)
|
|
49
|
+
for (let j = 0; j < chunkLen; j++) results[i + j] = chunk[j]
|
|
50
|
+
}
|
|
51
|
+
return results
|
|
52
|
+
}
|
|
53
|
+
|
|
19
54
|
/**
|
|
20
55
|
* Evaluates an expression node against a row of data
|
|
21
56
|
*
|
|
@@ -177,10 +212,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
177
212
|
// Apply FILTER clause if present
|
|
178
213
|
let filteredRows = rows
|
|
179
214
|
if (node.filter) {
|
|
180
|
-
const
|
|
181
|
-
const passes = await Promise.all(rows.map(row =>
|
|
182
|
-
evaluateExpr({ node: filterNode, row, context })
|
|
183
|
-
))
|
|
215
|
+
const passes = await evaluateAll(node.filter, rows, context)
|
|
184
216
|
filteredRows = rows.filter((_, i) => passes[i])
|
|
185
217
|
}
|
|
186
218
|
|
|
@@ -191,9 +223,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
191
223
|
return filteredRows.length
|
|
192
224
|
}
|
|
193
225
|
|
|
194
|
-
const values = await
|
|
195
|
-
evaluateExpr({ node: argNode, row, context })
|
|
196
|
-
))
|
|
226
|
+
const values = await evaluateAll(argNode, filteredRows, context)
|
|
197
227
|
if (node.distinct) {
|
|
198
228
|
const seen = new Set()
|
|
199
229
|
for (const v of values) {
|
|
@@ -209,9 +239,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
209
239
|
}
|
|
210
240
|
|
|
211
241
|
if (funcName === 'COUNTIF') {
|
|
212
|
-
const values = await
|
|
213
|
-
evaluateExpr({ node: argNode, row, context })
|
|
214
|
-
))
|
|
242
|
+
const values = await evaluateAll(argNode, filteredRows, context)
|
|
215
243
|
let count = 0
|
|
216
244
|
for (const v of values) {
|
|
217
245
|
if (v) count++
|
|
@@ -220,9 +248,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
220
248
|
}
|
|
221
249
|
|
|
222
250
|
if (funcName === 'SUM' || funcName === 'AVG' || funcName === 'MIN' || funcName === 'MAX') {
|
|
223
|
-
const rawValues = await
|
|
224
|
-
evaluateExpr({ node: argNode, row, context })
|
|
225
|
-
))
|
|
251
|
+
const rawValues = await evaluateAll(argNode, filteredRows, context)
|
|
226
252
|
let sum = 0
|
|
227
253
|
let count = 0
|
|
228
254
|
/** @type {SqlPrimitive} */
|
|
@@ -247,9 +273,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
247
273
|
}
|
|
248
274
|
|
|
249
275
|
if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
|
|
250
|
-
const rawValues = await
|
|
251
|
-
evaluateExpr({ node: argNode, row, context })
|
|
252
|
-
))
|
|
276
|
+
const rawValues = await evaluateAll(argNode, filteredRows, context)
|
|
253
277
|
let sum = 0
|
|
254
278
|
/** @type {number[]} */
|
|
255
279
|
const values = []
|
|
@@ -290,9 +314,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
290
314
|
...node,
|
|
291
315
|
})
|
|
292
316
|
}
|
|
293
|
-
const rawValues = await
|
|
294
|
-
evaluateExpr({ node: valueNode, row, context })
|
|
295
|
-
))
|
|
317
|
+
const rawValues = await evaluateAll(valueNode, filteredRows, context)
|
|
296
318
|
/** @type {number[]} */
|
|
297
319
|
const values = []
|
|
298
320
|
for (const raw of rawValues) {
|
|
@@ -311,12 +333,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
311
333
|
}
|
|
312
334
|
|
|
313
335
|
if (funcName === 'JSON_ARRAYAGG' || funcName === 'ARRAY_AGG') {
|
|
336
|
+
const allValues = await evaluateAll(argNode, filteredRows, context)
|
|
314
337
|
if (node.distinct) {
|
|
315
338
|
/** @type {SqlPrimitive[]} */
|
|
316
339
|
const values = []
|
|
317
340
|
const seen = new Set()
|
|
318
|
-
for (const
|
|
319
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
341
|
+
for (const v of allValues) {
|
|
320
342
|
const key = keyify(v)
|
|
321
343
|
if (!seen.has(key)) {
|
|
322
344
|
seen.add(key)
|
|
@@ -325,9 +347,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
325
347
|
}
|
|
326
348
|
return values
|
|
327
349
|
} else {
|
|
328
|
-
return
|
|
329
|
-
evaluateExpr({ node: argNode, row, context })
|
|
330
|
-
))
|
|
350
|
+
return allValues
|
|
331
351
|
}
|
|
332
352
|
}
|
|
333
353
|
|
|
@@ -336,10 +356,10 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
336
356
|
const separator = String(await evaluateExpr({ node: separatorNode, row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
|
|
337
357
|
/** @type {string[]} */
|
|
338
358
|
const values = []
|
|
359
|
+
const allValues = await evaluateAll(argNode, filteredRows, context)
|
|
339
360
|
if (node.distinct) {
|
|
340
361
|
const seen = new Set()
|
|
341
|
-
for (const
|
|
342
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
362
|
+
for (const v of allValues) {
|
|
343
363
|
if (v == null) continue
|
|
344
364
|
const str = String(v)
|
|
345
365
|
const key = keyify(str)
|
|
@@ -349,8 +369,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
349
369
|
}
|
|
350
370
|
}
|
|
351
371
|
} else {
|
|
352
|
-
for (const
|
|
353
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
372
|
+
for (const v of allValues) {
|
|
354
373
|
if (v != null) values.push(String(v))
|
|
355
374
|
}
|
|
356
375
|
}
|
|
@@ -687,7 +706,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
687
706
|
if (node.type === 'in') {
|
|
688
707
|
const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
689
708
|
const subResult = executeStatement({ query: node.subquery, context })
|
|
709
|
+
let innerCount = 0
|
|
690
710
|
for await (const resRow of subResult.rows()) {
|
|
711
|
+
if (++innerCount % YIELD_INTERVAL === 0) {
|
|
712
|
+
await yieldToEventLoop()
|
|
713
|
+
context.signal?.throwIfAborted()
|
|
714
|
+
}
|
|
691
715
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
692
716
|
if (sqlEquals(exprVal, value)) return true
|
|
693
717
|
}
|
package/src/parse/primary.js
CHANGED
|
@@ -40,7 +40,7 @@ export function parsePrimary(state) {
|
|
|
40
40
|
return expr
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Array literal: [elem, elem, ...]
|
|
43
|
+
// Array literal: [elem, elem, ...] elements must be literals
|
|
44
44
|
if (match(state, 'bracket', '[')) {
|
|
45
45
|
/** @type {SqlPrimitive[]} */
|
|
46
46
|
const values = []
|
|
@@ -158,7 +158,7 @@ export function parsePrimary(state) {
|
|
|
158
158
|
prefix = name
|
|
159
159
|
name = expect(state, 'identifier').value
|
|
160
160
|
} else if (match(state, 'bracket', '[')) {
|
|
161
|
-
// table['column']
|
|
161
|
+
// table['column'] string subscript is equivalent to dot access
|
|
162
162
|
const fieldTok = current(state)
|
|
163
163
|
if (fieldTok.type !== 'string') {
|
|
164
164
|
throw parseError(state, 'string literal')
|