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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.12.20",
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.8.0",
43
- "@vitest/coverage-v8": "4.1.6",
42
+ "@types/node": "25.9.1",
43
+ "@vitest/coverage-v8": "4.1.7",
44
44
  "eslint": "9.39.4",
45
- "eslint-plugin-jsdoc": "62.9.0",
45
+ "eslint-plugin-jsdoc": "63.0.0",
46
46
  "typescript": "6.0.3",
47
- "vitest": "4.1.6"
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 (context.signal?.aborted) return
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
- for (const row of allRows) {
96
- const key = keyify(...await Promise.all(plan.groupBy.map(expr => evaluateExpr({ node: expr, row, context }))))
97
- let group = groups.get(key)
98
- if (!group) {
99
- group = []
100
- groups.set(key, group)
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 (context.signal?.aborted) return
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
 
@@ -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 derived expressions evaluate lazily and can't be
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)
@@ -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 })
@@ -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
- if (context.signal?.aborted) return []
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({
@@ -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 (context.signal?.aborted) return
40
- i++
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 (context.signal?.aborted) return
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 (context.signal?.aborted) return
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
- const partitionKeys = await Promise.all(rows.map(row =>
109
- Promise.all(spec.partitionBy.map(expr => evaluateExpr({ node: expr, row, context })))
110
- ))
111
- for (let i = 0; i < rows.length; i++) {
112
- const key = keyify(...partitionKeys[i])
113
- let bucket = partitions.get(key)
114
- if (!bucket) {
115
- bucket = []
116
- partitions.set(key, bucket)
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.map((idx, k) => ({ idx, values: orderValues[k], pos: k }))
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 (context.signal?.aborted) return
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 filterNode = node.filter
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 Promise.all(filteredRows.map(row =>
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 Promise.all(filteredRows.map(row =>
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 Promise.all(filteredRows.map(row =>
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 Promise.all(filteredRows.map(row =>
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 Promise.all(filteredRows.map(row =>
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 row of filteredRows) {
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 await Promise.all(filteredRows.map(row =>
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 row of filteredRows) {
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 row of filteredRows) {
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
  }
@@ -40,7 +40,7 @@ export function parsePrimary(state) {
40
40
  return expr
41
41
  }
42
42
 
43
- // Array literal: [elem, elem, ...] elements must be literals
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'] string subscript is equivalent to dot access
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')