squirreling 0.12.20 → 0.12.21

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.21",
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
  }
@@ -9,6 +9,9 @@ import { keyify } from './utils.js'
9
9
  * @import { HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
10
10
  */
11
11
 
12
+ // Yield to the event loop every this many iterations so that aborts can actually fire
13
+ const YIELD_INTERVAL = 4000
14
+
12
15
  /**
13
16
  * Projects aggregate columns from a group of rows
14
17
  *
@@ -83,8 +86,12 @@ export function executeHashAggregate(plan, context) {
83
86
  // Collect all rows
84
87
  /** @type {AsyncRow[]} */
85
88
  const allRows = []
89
+ let collectCount = 0
86
90
  for await (const row of child.rows()) {
87
- if (context.signal?.aborted) return
91
+ if (++collectCount % YIELD_INTERVAL === 0) {
92
+ await new Promise(resolve => setTimeout(resolve, 0))
93
+ if (context.signal?.aborted) return
94
+ }
88
95
  allRows.push(row)
89
96
  }
90
97
 
@@ -92,14 +99,27 @@ export function executeHashAggregate(plan, context) {
92
99
  /** @type {Map<any, AsyncRow[]>} */
93
100
  const groups = new Map()
94
101
 
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)
102
+ for (let chunkStart = 0; chunkStart < allRows.length; chunkStart += YIELD_INTERVAL) {
103
+ if (chunkStart > 0) {
104
+ await new Promise(resolve => setTimeout(resolve, 0))
105
+ if (context.signal?.aborted) return
106
+ }
107
+ const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, allRows.length)
108
+ const chunkKeys = await Promise.all(
109
+ allRows.slice(chunkStart, chunkEnd).map(row =>
110
+ Promise.all(plan.groupBy.map(expr => evaluateExpr({ node: expr, row, context })))
111
+ )
112
+ )
113
+ for (let j = 0; j < chunkKeys.length; j++) {
114
+ const key = keyify(...chunkKeys[j])
115
+ const row = allRows[chunkStart + j]
116
+ let group = groups.get(key)
117
+ if (!group) {
118
+ group = []
119
+ groups.set(key, group)
120
+ }
121
+ group.push(row)
101
122
  }
102
- group.push(row)
103
123
  }
104
124
 
105
125
  /** @type {{ row: AsyncRow, rows: AsyncRow[], outputRow: AsyncRow }[]} */
@@ -166,8 +186,12 @@ export function executeScalarAggregate(plan, context) {
166
186
  // Collect all rows into single group
167
187
  /** @type {AsyncRow[]} */
168
188
  const group = []
189
+ let collectCount = 0
169
190
  for await (const row of child.rows()) {
170
- if (context.signal?.aborted) return
191
+ if (++collectCount % YIELD_INTERVAL === 0) {
192
+ await new Promise(resolve => setTimeout(resolve, 0))
193
+ if (context.signal?.aborted) return
194
+ }
171
195
  group.push(row)
172
196
  }
173
197
 
@@ -16,6 +16,9 @@ import { executeWindow } from './window.js'
16
16
  * @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode, TableFunctionNode } from '../plan/types.js'
17
17
  */
18
18
 
19
+ // Yield to the event loop every 4000 iterations so that aborts can actually fire
20
+ const YIELD_INTERVAL = 4000
21
+
19
22
  /**
20
23
  * Executes a SQL SELECT query against tables
21
24
  *
@@ -406,12 +409,17 @@ async function* filterRows(rows, condition, context, limit) {
406
409
  let chunkSize = limit ?? 1
407
410
  const grow = limit !== undefined
408
411
  let rowIndex = 0
412
+ let innerCount = 0
409
413
 
410
414
  /** @type {{ row: AsyncRow, rowIndex: number }[]} */
411
415
  let buffer = []
412
416
 
413
417
  for await (const row of rows) {
414
418
  if (context.signal?.aborted) return
419
+ if (++innerCount % YIELD_INTERVAL === 0) {
420
+ await new Promise(resolve => setTimeout(resolve, 0))
421
+ if (context.signal?.aborted) return
422
+ }
415
423
  rowIndex++
416
424
  buffer.push({ row, rowIndex })
417
425
 
@@ -451,8 +459,13 @@ async function* limitRows(rows, limit = Infinity, offset = 0, signal) {
451
459
  if (limit <= 0) return
452
460
  let skipped = 0
453
461
  let yielded = 0
462
+ let innerCount = 0
454
463
  for await (const row of rows) {
455
464
  if (signal?.aborted) return
465
+ if (++innerCount % YIELD_INTERVAL === 0) {
466
+ await new Promise(resolve => setTimeout(resolve, 0))
467
+ if (signal?.aborted) return
468
+ }
456
469
  if (skipped < offset) {
457
470
  skipped++
458
471
  continue
@@ -500,16 +513,21 @@ function executeProject(plan, context) {
500
513
  maxRows: child.maxRows,
501
514
  async *rows() {
502
515
  let rowIndex = 0
516
+ let innerCount = 0
503
517
 
504
518
  for await (const row of child.rows()) {
505
519
  if (context.signal?.aborted) return
520
+ if (++innerCount % YIELD_INTERVAL === 0) {
521
+ await new Promise(resolve => setTimeout(resolve, 0))
522
+ if (context.signal?.aborted) return
523
+ }
506
524
  rowIndex++
507
525
  const currentRowIndex = rowIndex
508
526
 
509
527
  /** @type {AsyncCells} */
510
528
  const cells = {}
511
529
  // Only safe to propagate resolved when every output column comes from
512
- // the star branch derived expressions evaluate lazily and can't be
530
+ // the star branch. Derived expressions evaluate lazily and can't be
513
531
  // pre-materialized here, and a partial resolved would make
514
532
  // collect()/downstream identifier fast paths read undefined.
515
533
  const source = resolveable ? row.resolved : undefined
@@ -588,9 +606,14 @@ function executeDistinct(plan, context) {
588
606
 
589
607
  /** @type {AsyncRow[]} */
590
608
  let buffer = []
609
+ let innerCount = 0
591
610
 
592
611
  for await (const row of child.rows()) {
593
612
  if (signal?.aborted) return
613
+ if (++innerCount % YIELD_INTERVAL === 0) {
614
+ await new Promise(resolve => setTimeout(resolve, 0))
615
+ if (signal?.aborted) return
616
+ }
594
617
  buffer.push(row)
595
618
 
596
619
  if (buffer.length >= MAX_CHUNK) {
@@ -671,8 +694,13 @@ function executeSetOperation(plan, context) {
671
694
  async *rows() {
672
695
  // UNION: yield deduplicated rows from both sides
673
696
  const seen = new Set()
697
+ let count = 0
674
698
  for await (const row of left.rows()) {
675
699
  if (signal?.aborted) return
700
+ if (++count % YIELD_INTERVAL === 0) {
701
+ await new Promise(resolve => setTimeout(resolve, 0))
702
+ if (signal?.aborted) return
703
+ }
676
704
  const key = await stableRowKey(row)
677
705
  if (!seen.has(key)) {
678
706
  seen.add(key)
@@ -681,6 +709,10 @@ function executeSetOperation(plan, context) {
681
709
  }
682
710
  for await (const row of right.rows()) {
683
711
  if (signal?.aborted) return
712
+ if (++count % YIELD_INTERVAL === 0) {
713
+ await new Promise(resolve => setTimeout(resolve, 0))
714
+ if (signal?.aborted) return
715
+ }
684
716
  const key = await stableRowKey(row)
685
717
  if (!seen.has(key)) {
686
718
  seen.add(key)
@@ -700,8 +732,13 @@ function executeSetOperation(plan, context) {
700
732
  // Materialize right side keys
701
733
  /** @type {Map<any, number>} */
702
734
  const rightKeys = new Map()
735
+ let tick = 0
703
736
  for await (const row of right.rows()) {
704
737
  if (signal?.aborted) return
738
+ if (++tick % YIELD_INTERVAL === 0) {
739
+ await new Promise(resolve => setTimeout(resolve, 0))
740
+ if (signal?.aborted) return
741
+ }
705
742
  const key = await stableRowKey(row)
706
743
  rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
707
744
  }
@@ -710,6 +747,10 @@ function executeSetOperation(plan, context) {
710
747
  // INTERSECT ALL: yield each left row that matches, consuming right counts
711
748
  for await (const row of left.rows()) {
712
749
  if (signal?.aborted) return
750
+ if (++tick % YIELD_INTERVAL === 0) {
751
+ await new Promise(resolve => setTimeout(resolve, 0))
752
+ if (signal?.aborted) return
753
+ }
713
754
  const key = await stableRowKey(row)
714
755
  const count = rightKeys.get(key)
715
756
  if (count) {
@@ -722,6 +763,10 @@ function executeSetOperation(plan, context) {
722
763
  const seen = new Set()
723
764
  for await (const row of left.rows()) {
724
765
  if (signal?.aborted) return
766
+ if (++tick % YIELD_INTERVAL === 0) {
767
+ await new Promise(resolve => setTimeout(resolve, 0))
768
+ if (signal?.aborted) return
769
+ }
725
770
  const key = await stableRowKey(row)
726
771
  if (rightKeys.has(key) && !seen.has(key)) {
727
772
  seen.add(key)
@@ -742,8 +787,13 @@ function executeSetOperation(plan, context) {
742
787
  // Materialize right side keys
743
788
  /** @type {Map<any, number>} */
744
789
  const rightKeys = new Map()
790
+ let tick = 0
745
791
  for await (const row of right.rows()) {
746
792
  if (signal?.aborted) return
793
+ if (++tick % YIELD_INTERVAL === 0) {
794
+ await new Promise(resolve => setTimeout(resolve, 0))
795
+ if (signal?.aborted) return
796
+ }
747
797
  const key = await stableRowKey(row)
748
798
  rightKeys.set(key, (rightKeys.get(key) ?? 0) + 1)
749
799
  }
@@ -752,6 +802,10 @@ function executeSetOperation(plan, context) {
752
802
  // EXCEPT ALL: yield left rows, consuming right counts
753
803
  for await (const row of left.rows()) {
754
804
  if (signal?.aborted) return
805
+ if (++tick % YIELD_INTERVAL === 0) {
806
+ await new Promise(resolve => setTimeout(resolve, 0))
807
+ if (signal?.aborted) return
808
+ }
755
809
  const key = await stableRowKey(row)
756
810
  const count = rightKeys.get(key)
757
811
  if (count) {
@@ -765,6 +819,10 @@ function executeSetOperation(plan, context) {
765
819
  const seen = new Set()
766
820
  for await (const row of left.rows()) {
767
821
  if (signal?.aborted) return
822
+ if (++tick % YIELD_INTERVAL === 0) {
823
+ await new Promise(resolve => setTimeout(resolve, 0))
824
+ if (signal?.aborted) return
825
+ }
768
826
  const key = await stableRowKey(row)
769
827
  if (!rightKeys.has(key) && !seen.has(key)) {
770
828
  seen.add(key)
@@ -7,6 +7,9 @@ import { executePlan } from './execute.js'
7
7
  * @import { HashJoinNode, NestedLoopJoinNode, PositionalJoinNode } from '../plan/types.js'
8
8
  */
9
9
 
10
+ // Yield to the event loop every 4000 iterations so that aborts can actually fire
11
+ const YIELD_INTERVAL = 4000
12
+
10
13
  /**
11
14
  * Executes a nested loop join operation
12
15
  *
@@ -41,6 +44,7 @@ export function executeNestedLoopJoin(plan, context) {
41
44
  /** @type {Set<AsyncRow> | undefined} */
42
45
  const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
43
46
 
47
+ let innerCount = 0
44
48
  for await (const leftRow of left.rows()) {
45
49
  if (context.signal?.aborted) break
46
50
 
@@ -51,6 +55,10 @@ export function executeNestedLoopJoin(plan, context) {
51
55
  let hasMatch = false
52
56
 
53
57
  for (const rightRow of rightRows) {
58
+ if (++innerCount % YIELD_INTERVAL === 0) {
59
+ await new Promise(resolve => setTimeout(resolve, 0))
60
+ if (context.signal?.aborted) return
61
+ }
54
62
  const tempMerged = mergeRows(leftRow, rightRow, leftTable, rightTable)
55
63
  const matches = await evaluateExpr({
56
64
  node: plan.condition,
@@ -237,6 +245,7 @@ export function executeHashJoin(plan, context) {
237
245
  const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
238
246
 
239
247
  // Probe phase: stream left rows
248
+ let innerCount = 0
240
249
  for await (const leftRow of left.rows()) {
241
250
  if (context.signal?.aborted) break
242
251
 
@@ -253,6 +262,10 @@ export function executeHashJoin(plan, context) {
253
262
  const candidates = hashMap.get(key)
254
263
  if (candidates?.length) {
255
264
  for (const rightRow of candidates) {
265
+ if (++innerCount % YIELD_INTERVAL === 0) {
266
+ await new Promise(resolve => setTimeout(resolve, 0))
267
+ if (context.signal?.aborted) return
268
+ }
256
269
  const merged = mergeRows(leftRow, rightRow, leftTable, rightTable)
257
270
  if (residual) {
258
271
  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({
@@ -7,6 +7,9 @@ import { compareForTerm, keyify } from './utils.js'
7
7
  * @import { WindowNode, WindowSpec } from '../plan/types.js'
8
8
  */
9
9
 
10
+ // Yield to the event loop every 4000 iterations so that aborts can actually fire
11
+ const YIELD_INTERVAL = 4000
12
+
10
13
  /**
11
14
  * Executes a Window plan node: buffers the child's rows, assigns each window
12
15
  * function's output per partition, and yields rows in input order with the
@@ -36,8 +39,10 @@ export function executeWindow(plan, context) {
36
39
  async *rows() {
37
40
  let i = 0
38
41
  for await (const row of child.rows()) {
39
- if (context.signal?.aborted) return
40
- i++
42
+ if (++i % YIELD_INTERVAL === 0) {
43
+ await new Promise(resolve => setTimeout(resolve, 0))
44
+ if (context.signal?.aborted) return
45
+ }
41
46
  const cells = { ...row.cells }
42
47
  for (const w of plan.windows) {
43
48
  const value = i
@@ -59,8 +64,12 @@ export function executeWindow(plan, context) {
59
64
  async *rows() {
60
65
  /** @type {AsyncRow[]} */
61
66
  const rows = []
67
+ let collectCount = 0
62
68
  for await (const row of child.rows()) {
63
- if (context.signal?.aborted) return
69
+ if (++collectCount % YIELD_INTERVAL === 0) {
70
+ await new Promise(resolve => setTimeout(resolve, 0))
71
+ if (context.signal?.aborted) return
72
+ }
64
73
  rows.push(row)
65
74
  }
66
75
  if (rows.length === 0) return
@@ -74,8 +83,12 @@ export function executeWindow(plan, context) {
74
83
  if (context.signal?.aborted) return
75
84
  }
76
85
 
86
+ let emitCount = 0
77
87
  for (let i = 0; i < rows.length; i++) {
78
- if (context.signal?.aborted) return
88
+ if (++emitCount % YIELD_INTERVAL === 0) {
89
+ await new Promise(resolve => setTimeout(resolve, 0))
90
+ if (context.signal?.aborted) return
91
+ }
79
92
  const row = rows[i]
80
93
  const cells = { ...row.cells }
81
94
  for (let w = 0; w < plan.windows.length; w++) {
@@ -105,17 +118,26 @@ async function computeWindow(spec, rows, output, context) {
105
118
  // Bucket row indices by partition key.
106
119
  /** @type {Map<string | number | bigint | boolean, number[]>} */
107
120
  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)
121
+ for (let chunkStart = 0; chunkStart < rows.length; chunkStart += YIELD_INTERVAL) {
122
+ if (chunkStart > 0) {
123
+ await new Promise(resolve => setTimeout(resolve, 0))
124
+ if (context.signal?.aborted) return
125
+ }
126
+ const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, rows.length)
127
+ const chunkKeys = await Promise.all(
128
+ rows.slice(chunkStart, chunkEnd).map(row =>
129
+ Promise.all(spec.partitionBy.map(expr => evaluateExpr({ node: expr, row, context })))
130
+ )
131
+ )
132
+ for (let j = 0; j < chunkKeys.length; j++) {
133
+ const key = keyify(...chunkKeys[j])
134
+ let bucket = partitions.get(key)
135
+ if (!bucket) {
136
+ bucket = []
137
+ partitions.set(key, bucket)
138
+ }
139
+ bucket.push(chunkStart + j)
117
140
  }
118
- bucket.push(i)
119
141
  }
120
142
 
121
143
  for (const bucket of partitions.values()) {
@@ -125,11 +147,24 @@ async function computeWindow(spec, rows, output, context) {
125
147
  /** @type {number[]} */
126
148
  let ordered
127
149
  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
150
  /** @type {{ idx: number, values: SqlPrimitive[], pos: number }[]} */
132
- const entries = bucket.map((idx, k) => ({ idx, values: orderValues[k], pos: k }))
151
+ const entries = new Array(bucket.length)
152
+ for (let chunkStart = 0; chunkStart < bucket.length; chunkStart += YIELD_INTERVAL) {
153
+ if (chunkStart > 0) {
154
+ await new Promise(resolve => setTimeout(resolve, 0))
155
+ if (context.signal?.aborted) return
156
+ }
157
+ const chunkEnd = Math.min(chunkStart + YIELD_INTERVAL, bucket.length)
158
+ const chunkValues = await Promise.all(
159
+ bucket.slice(chunkStart, chunkEnd).map(idx =>
160
+ Promise.all(spec.orderBy.map(term => evaluateExpr({ node: term.expr, row: rows[idx], context })))
161
+ )
162
+ )
163
+ for (let j = 0; j < chunkValues.length; j++) {
164
+ const k = chunkStart + j
165
+ entries[k] = { idx: bucket[k], values: chunkValues[j], pos: k }
166
+ }
167
+ }
133
168
  entries.sort((a, b) => {
134
169
  for (let i = 0; i < spec.orderBy.length; i++) {
135
170
  const cmp = compareForTerm(a.values[i], b.values[i], spec.orderBy[i])
@@ -165,8 +200,12 @@ async function applyWindowFunction(spec, ordered, rows, output, context) {
165
200
  if (spec.funcName === 'LAG' || spec.funcName === 'LEAD') {
166
201
  const direction = spec.funcName === 'LAG' ? -1 : 1
167
202
  const [valueExpr, offsetExpr, defaultExpr] = spec.args
203
+ let tick = 0
168
204
  for (let k = 0; k < ordered.length; k++) {
169
- if (context.signal?.aborted) return
205
+ if (++tick % YIELD_INTERVAL === 0) {
206
+ await new Promise(resolve => setTimeout(resolve, 0))
207
+ if (context.signal?.aborted) return
208
+ }
170
209
  const idx = ordered[k]
171
210
  const row = rows[idx]
172
211
  const offset = offsetExpr
@@ -16,6 +16,35 @@ import { evaluateStringFunc } from './strings.js'
16
16
  * @import { ExprNode, AsyncRow, ExecuteContext, SqlPrimitive } from '../types.js'
17
17
  */
18
18
 
19
+ // Yield to the event loop every this many iterations so that aborts can actually fire
20
+ const YIELD_INTERVAL = 4000
21
+
22
+ /**
23
+ * Evaluates an expression for each row, yielding to the event loop every
24
+ * YIELD_INTERVAL rows so signal-based aborts can fire mid-evaluation.
25
+ *
26
+ * @param {ExprNode} node
27
+ * @param {AsyncRow[]} rows
28
+ * @param {ExecuteContext} context
29
+ * @returns {Promise<SqlPrimitive[]>}
30
+ */
31
+ async function evaluateAll(node, rows, context) {
32
+ /** @type {SqlPrimitive[]} */
33
+ const results = new Array(rows.length)
34
+ for (let i = 0; i < rows.length; i += YIELD_INTERVAL) {
35
+ if (i > 0) {
36
+ await new Promise(resolve => setTimeout(resolve, 0))
37
+ context.signal?.throwIfAborted()
38
+ }
39
+ const end = Math.min(i + YIELD_INTERVAL, rows.length)
40
+ const chunk = await Promise.all(rows.slice(i, end).map(row =>
41
+ evaluateExpr({ node, row, context })
42
+ ))
43
+ for (let j = 0; j < chunk.length; j++) results[i + j] = chunk[j]
44
+ }
45
+ return results
46
+ }
47
+
19
48
  /**
20
49
  * Evaluates an expression node against a row of data
21
50
  *
@@ -177,10 +206,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
177
206
  // Apply FILTER clause if present
178
207
  let filteredRows = rows
179
208
  if (node.filter) {
180
- const filterNode = node.filter
181
- const passes = await Promise.all(rows.map(row =>
182
- evaluateExpr({ node: filterNode, row, context })
183
- ))
209
+ const passes = await evaluateAll(node.filter, rows, context)
184
210
  filteredRows = rows.filter((_, i) => passes[i])
185
211
  }
186
212
 
@@ -191,9 +217,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
191
217
  return filteredRows.length
192
218
  }
193
219
 
194
- const values = await Promise.all(filteredRows.map(row =>
195
- evaluateExpr({ node: argNode, row, context })
196
- ))
220
+ const values = await evaluateAll(argNode, filteredRows, context)
197
221
  if (node.distinct) {
198
222
  const seen = new Set()
199
223
  for (const v of values) {
@@ -209,9 +233,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
209
233
  }
210
234
 
211
235
  if (funcName === 'COUNTIF') {
212
- const values = await Promise.all(filteredRows.map(row =>
213
- evaluateExpr({ node: argNode, row, context })
214
- ))
236
+ const values = await evaluateAll(argNode, filteredRows, context)
215
237
  let count = 0
216
238
  for (const v of values) {
217
239
  if (v) count++
@@ -220,9 +242,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
220
242
  }
221
243
 
222
244
  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
- ))
245
+ const rawValues = await evaluateAll(argNode, filteredRows, context)
226
246
  let sum = 0
227
247
  let count = 0
228
248
  /** @type {SqlPrimitive} */
@@ -247,9 +267,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
247
267
  }
248
268
 
249
269
  if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
250
- const rawValues = await Promise.all(filteredRows.map(row =>
251
- evaluateExpr({ node: argNode, row, context })
252
- ))
270
+ const rawValues = await evaluateAll(argNode, filteredRows, context)
253
271
  let sum = 0
254
272
  /** @type {number[]} */
255
273
  const values = []
@@ -290,9 +308,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
290
308
  ...node,
291
309
  })
292
310
  }
293
- const rawValues = await Promise.all(filteredRows.map(row =>
294
- evaluateExpr({ node: valueNode, row, context })
295
- ))
311
+ const rawValues = await evaluateAll(valueNode, filteredRows, context)
296
312
  /** @type {number[]} */
297
313
  const values = []
298
314
  for (const raw of rawValues) {
@@ -311,12 +327,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
311
327
  }
312
328
 
313
329
  if (funcName === 'JSON_ARRAYAGG' || funcName === 'ARRAY_AGG') {
330
+ const allValues = await evaluateAll(argNode, filteredRows, context)
314
331
  if (node.distinct) {
315
332
  /** @type {SqlPrimitive[]} */
316
333
  const values = []
317
334
  const seen = new Set()
318
- for (const row of filteredRows) {
319
- const v = await evaluateExpr({ node: argNode, row, context })
335
+ for (const v of allValues) {
320
336
  const key = keyify(v)
321
337
  if (!seen.has(key)) {
322
338
  seen.add(key)
@@ -325,9 +341,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
325
341
  }
326
342
  return values
327
343
  } else {
328
- return await Promise.all(filteredRows.map(row =>
329
- evaluateExpr({ node: argNode, row, context })
330
- ))
344
+ return allValues
331
345
  }
332
346
  }
333
347
 
@@ -336,10 +350,10 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
336
350
  const separator = String(await evaluateExpr({ node: separatorNode, row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
337
351
  /** @type {string[]} */
338
352
  const values = []
353
+ const allValues = await evaluateAll(argNode, filteredRows, context)
339
354
  if (node.distinct) {
340
355
  const seen = new Set()
341
- for (const row of filteredRows) {
342
- const v = await evaluateExpr({ node: argNode, row, context })
356
+ for (const v of allValues) {
343
357
  if (v == null) continue
344
358
  const str = String(v)
345
359
  const key = keyify(str)
@@ -349,8 +363,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
349
363
  }
350
364
  }
351
365
  } else {
352
- for (const row of filteredRows) {
353
- const v = await evaluateExpr({ node: argNode, row, context })
366
+ for (const v of allValues) {
354
367
  if (v != null) values.push(String(v))
355
368
  }
356
369
  }
@@ -687,7 +700,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
687
700
  if (node.type === 'in') {
688
701
  const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
689
702
  const subResult = executeStatement({ query: node.subquery, context })
703
+ let innerCount = 0
690
704
  for await (const resRow of subResult.rows()) {
705
+ if (++innerCount % YIELD_INTERVAL === 0) {
706
+ await new Promise(resolve => setTimeout(resolve, 0))
707
+ context.signal?.throwIfAborted()
708
+ }
691
709
  const value = await resRow.cells[resRow.columns[0]]()
692
710
  if (sqlEquals(exprVal, value)) return true
693
711
  }
@@ -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')