squirreling 0.3.1 → 0.4.1

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![minzipped](https://img.shields.io/bundlephobia/minzip/squirreling)](https://www.npmjs.com/package/squirreling)
8
8
  [![workflow status](https://github.com/hyparam/squirreling/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/squirreling/actions)
9
9
  [![mit license](https://img.shields.io/badge/License-MIT-orange.svg)](https://opensource.org/licenses/MIT)
10
- ![coverage](https://img.shields.io/badge/Coverage-93-darkred)
10
+ ![coverage](https://img.shields.io/badge/Coverage-95-darkred)
11
11
  [![dependencies](https://img.shields.io/badge/Dependencies-0-blueviolet)](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
12
12
 
13
13
  Squirreling is a streaming async SQL engine for JavaScript. It is designed to provide efficient streaming of results from pluggable backends for highly efficient retrieval of data for browser applications.
@@ -22,8 +22,8 @@ Squirreling is a streaming async SQL engine for JavaScript. It is designed to pr
22
22
  - Constant memory usage for simple queries with LIMIT
23
23
  - Robust error handling and validation designed for LLM tool use
24
24
  - In-memory data option for simple use cases
25
+ - Late materialization for efficiency
25
26
  - Select only
26
- - No joins (yet)
27
27
 
28
28
  ## Usage
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -1,20 +1,18 @@
1
1
  /**
2
- * @import { AsyncDataSource, AsyncRow } from '../types.js'
2
+ * @import { AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
 
6
6
  /**
7
7
  * Wraps an async generator of plain objects into an AsyncDataSource
8
8
  *
9
- * @param {AsyncGenerator<Record<string, any>>} gen
9
+ * @param {AsyncGenerator<AsyncRow>} gen
10
10
  * @returns {AsyncDataSource}
11
11
  */
12
12
  export function generatorSource(gen) {
13
13
  return {
14
14
  async *getRows() {
15
- for await (const row of gen) {
16
- yield asyncRow(row)
17
- }
15
+ yield* gen
18
16
  },
19
17
  }
20
18
  }
@@ -22,24 +20,22 @@ export function generatorSource(gen) {
22
20
  /**
23
21
  * Creates an async row accessor that wraps a plain JavaScript object
24
22
  *
25
- * @param {Record<string, any>} obj - the plain object
23
+ * @param {Record<string, SqlPrimitive>} obj - the plain object
26
24
  * @returns {AsyncRow} a row accessor interface
27
25
  */
28
- export function asyncRow(obj) {
29
- return {
30
- getCell(name) {
31
- return obj[name]
32
- },
33
- getKeys() {
34
- return Object.keys(obj)
35
- },
26
+ function asyncRow(obj) {
27
+ /** @type {AsyncRow} */
28
+ const row = {}
29
+ for (const [key, value] of Object.entries(obj)) {
30
+ row[key] = () => Promise.resolve(value)
36
31
  }
32
+ return row
37
33
  }
38
34
 
39
35
  /**
40
36
  * Creates an async memory-backed data source from an array of plain objects
41
37
  *
42
- * @param {Record<string, any>[]} data - array of plain objects
38
+ * @param {Record<string, SqlPrimitive>[]} data - array of plain objects
43
39
  * @returns {AsyncDataSource} an async data source interface
44
40
  */
45
41
  export function memorySource(data) {
@@ -51,3 +47,40 @@ export function memorySource(data) {
51
47
  },
52
48
  }
53
49
  }
50
+
51
+ /**
52
+ * Wraps a data source that caches all accessed rows in memory
53
+ * @param {AsyncDataSource} source
54
+ * @returns {AsyncDataSource}
55
+ */
56
+ export function cachedDataSource(source) {
57
+ /** @type {Map<string, Promise<SqlPrimitive>>} */
58
+ const cache = new Map()
59
+ return {
60
+ /**
61
+ * @yields {AsyncRow}
62
+ */
63
+ async *getRows() {
64
+ let index = 0
65
+ for await (const row of source.getRows()) {
66
+ const rowIndex = index
67
+ /** @type {AsyncRow} */
68
+ const out = {}
69
+ for (const [key, cell] of Object.entries(row)) {
70
+ // Wrap the cell to cache accesses
71
+ out[key] = () => {
72
+ const cacheKey = `${rowIndex}:${key}`
73
+ let value = cache.get(cacheKey)
74
+ if (!value) {
75
+ value = cell()
76
+ cache.set(cacheKey, value)
77
+ }
78
+ return value
79
+ }
80
+ }
81
+ yield out
82
+ index++
83
+ }
84
+ },
85
+ }
86
+ }
@@ -1,21 +1,24 @@
1
1
  import { evaluateExpr } from './expression.js'
2
+ import { defaultDerivedAlias } from './utils.js'
2
3
 
3
4
  /**
4
5
  * Evaluates an aggregate function over a set of rows
5
6
  *
6
- * @import { AggregateColumn, ExprNode, AsyncRow } from '../types.js'
7
- * @param {AggregateColumn} col - aggregate column definition
8
- * @param {AsyncRow[]} rows - rows to aggregate
7
+ * @import { AggregateColumn, AsyncDataSource, ExprNode, AsyncRow } from '../types.js'
8
+ * @param {Object} options
9
+ * @param {AggregateColumn} options.col - aggregate column definition
10
+ * @param {AsyncRow[]} options.rows - rows to aggregate
11
+ * @param {Record<string, AsyncDataSource>} options.tables
9
12
  * @returns {Promise<number | null>} aggregated result
10
13
  */
11
- export async function evaluateAggregate(col, rows) {
14
+ export async function evaluateAggregate({ col, rows, tables }) {
12
15
  const { arg, func } = col
13
16
 
14
17
  if (func === 'COUNT') {
15
18
  if (arg.kind === 'star') return rows.length
16
19
  let count = 0
17
- for (let i = 0; i < rows.length; i += 1) {
18
- const v = await evaluateExpr({ node: arg.expr, row: rows[i] })
20
+ for (const row of rows) {
21
+ const v = await evaluateExpr({ node: arg.expr, row, tables })
19
22
  if (v !== null && v !== undefined) {
20
23
  count += 1
21
24
  }
@@ -34,8 +37,8 @@ export async function evaluateAggregate(col, rows) {
34
37
  /** @type {number | null} */
35
38
  let max = null
36
39
 
37
- for (let i = 0; i < rows.length; i += 1) {
38
- const raw = await evaluateExpr({ node: arg.expr, row: rows[i] })
40
+ for (const row of rows) {
41
+ const raw = await evaluateExpr({ node: arg.expr, row, tables })
39
42
  if (raw == null) continue
40
43
  const num = Number(raw)
41
44
  if (!Number.isFinite(num)) continue
@@ -70,30 +73,5 @@ export async function evaluateAggregate(col, rows) {
70
73
  export function defaultAggregateAlias(col) {
71
74
  const base = col.func.toLowerCase()
72
75
  if (col.arg.kind === 'star') return base + '_all'
73
- return base + '_' + defaultAggregateAliasExpr(col.arg.expr)
74
- }
75
-
76
- /**
77
- * @param {ExprNode} expr
78
- * @returns {string}
79
- */
80
- export function defaultAggregateAliasExpr(expr) {
81
- if (expr.type === 'identifier') {
82
- return expr.name
83
- }
84
- if (expr.type === 'literal') {
85
- return String(expr.value)
86
- }
87
- if (expr.type === 'cast') {
88
- return defaultAggregateAliasExpr(expr.expr) + '_as_' + expr.toType
89
- }
90
- if (expr.type === 'unary') {
91
- return expr.op + '_' + defaultAggregateAliasExpr(expr.argument)
92
- }
93
- if (expr.type === 'binary') {
94
- return defaultAggregateAliasExpr(expr.left) + '_' + expr.op + '_' + defaultAggregateAliasExpr(expr.right)
95
- }
96
- if (expr.type === 'function') {
97
- return expr.name.toLowerCase() + '_' + expr.args.map(defaultAggregateAliasExpr).join('_')
98
- }
76
+ return base + '_' + defaultDerivedAlias(col.arg.expr)
99
77
  }
@@ -1,8 +1,10 @@
1
- import { evaluateExpr } from './expression.js'
1
+ import { generatorSource, memorySource } from '../backend/dataSource.js'
2
2
  import { parseSql } from '../parse/parse.js'
3
- import { asyncRow, generatorSource, memorySource } from '../backend/dataSource.js'
4
3
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
4
+ import { evaluateExpr } from './expression.js'
5
5
  import { evaluateHavingExpr } from './having.js'
6
+ import { executeJoins } from './join.js'
7
+ import { defaultDerivedAlias } from './utils.js'
6
8
 
7
9
  /**
8
10
  * @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
@@ -12,15 +14,12 @@ import { evaluateHavingExpr } from './having.js'
12
14
  * Executes a SQL SELECT query against named data sources
13
15
  *
14
16
  * @param {ExecuteSqlOptions} options - the execution options
15
- * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
17
+ * @yields {AsyncRow} async generator yielding result rows
16
18
  */
17
19
  export async function* executeSql({ tables, query }) {
18
20
  const select = parseSql(query)
19
21
 
20
22
  // Check for unsupported operations
21
- if (select.joins.length) {
22
- throw new Error('JOIN is not supported')
23
- }
24
23
  if (!select.from) {
25
24
  throw new Error('FROM clause is required')
26
25
  }
@@ -44,67 +43,47 @@ export async function* executeSql({ tables, query }) {
44
43
  *
45
44
  * @param {SelectStatement} select
46
45
  * @param {Record<string, AsyncDataSource>} tables
47
- * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
46
+ * @yields {AsyncRow}
48
47
  */
49
48
  export async function* executeSelect(select, tables) {
50
49
  /** @type {AsyncDataSource} */
51
50
  let dataSource
52
-
53
- if (typeof select.from === 'string') {
54
- const table = tables[select.from]
55
- if (table === undefined) {
56
- throw new Error(`Table "${select.from}" not found`)
51
+ /** @type {string} */
52
+ let fromTableName
53
+
54
+ if (select.from.kind === 'table') {
55
+ // Use alias for column prefixing, but look up the actual table name
56
+ fromTableName = select.from.alias ?? select.from.table
57
+ dataSource = tables[select.from.table]
58
+ if (dataSource === undefined) {
59
+ throw new Error(`Table "${select.from.table}" not found`)
57
60
  }
58
-
59
- dataSource = table
60
61
  } else {
61
62
  // Nested subquery - recursively resolve
63
+ fromTableName = select.from.alias
62
64
  dataSource = generatorSource(executeSelect(select.from.query, tables))
63
65
  }
64
66
 
65
- yield* evaluateSelectAst(select, dataSource, tables)
66
- }
67
-
68
- /**
69
- * Generates a default alias for a derived column expression
70
- *
71
- * @param {ExprNode} expr - the expression node
72
- * @returns {string} the generated alias
73
- */
74
- function defaultDerivedAlias(expr) {
75
- if (expr.type === 'identifier') {
76
- return expr.name
77
- }
78
- if (expr.type === 'function') {
79
- const base = expr.name.toLowerCase()
80
- // Try to extract column names from identifier arguments
81
- const columnNames = expr.args
82
- .filter(arg => arg.type === 'identifier')
83
- .map(arg => arg.name)
84
- if (columnNames.length > 0) {
85
- return base + '_' + columnNames.join('_')
86
- }
87
- return base
88
- }
89
- if (expr.type === 'cast') return 'cast_expr'
90
- if (expr.type === 'unary' && expr.argument.type === 'identifier') {
91
- return expr.op === '-' ? 'neg_' + expr.argument.name : 'expr'
67
+ // Execute JOINs if present
68
+ if (select.joins.length) {
69
+ dataSource = await executeJoins(dataSource, select.joins, fromTableName, tables)
92
70
  }
93
- return 'expr'
71
+
72
+ yield* evaluateSelectAst(select, dataSource, tables)
94
73
  }
95
74
 
96
75
  /**
97
76
  * Creates a stable string key for a row to enable deduplication
98
77
  *
99
- * @param {Record<string, any>} row
100
- * @returns {string} a stable string representation of the row
78
+ * @param {AsyncRow} row
79
+ * @returns {Promise<string>} a stable string representation of the row
101
80
  */
102
- function stableRowKey(row) {
81
+ async function stableRowKey(row) {
103
82
  const keys = Object.keys(row).sort()
104
83
  /** @type {string[]} */
105
84
  const parts = []
106
85
  for (const k of keys) {
107
- const v = row[k]
86
+ const v = await row[k]()
108
87
  parts.push(k + ':' + JSON.stringify(v))
109
88
  }
110
89
  return parts.join('|')
@@ -128,28 +107,28 @@ function compareValues(a, b) {
128
107
  return 0
129
108
  }
130
109
 
131
- const as = String(a)
132
- const bs = String(b)
133
- if (as < bs) return -1
134
- if (as > bs) return 1
110
+ const aa = String(a)
111
+ const bb = String(b)
112
+ if (aa < bb) return -1
113
+ if (aa > bb) return 1
135
114
  return 0
136
115
  }
137
116
 
138
117
  /**
139
118
  * Applies DISTINCT filtering to remove duplicate rows
140
119
  *
141
- * @param {Record<string, any>[]} rows - The input rows
142
- * @param {boolean} distinct - Whether to apply deduplication
143
- * @returns {Record<string, any>[]} The deduplicated rows
120
+ * @param {AsyncRow[]} rows - the input rows
121
+ * @param {boolean} distinct - whether to apply deduplication
122
+ * @returns {Promise<AsyncRow[]>} the deduplicated rows
144
123
  */
145
- function applyDistinct(rows, distinct) {
124
+ async function applyDistinct(rows, distinct) {
146
125
  if (!distinct) return rows
147
126
  /** @type {Set<string>} */
148
127
  const seen = new Set()
149
- /** @type {Record<string, any>[]} */
128
+ /** @type {AsyncRow[]} */
150
129
  const result = []
151
130
  for (const row of rows) {
152
- const key = stableRowKey(row)
131
+ const key = await stableRowKey(row)
153
132
  if (seen.has(key)) continue
154
133
  seen.add(key)
155
134
  result.push(row)
@@ -221,10 +200,10 @@ async function sortRowSources(rows, orderBy, tables) {
221
200
  /**
222
201
  * Applies ORDER BY sorting to rows
223
202
  *
224
- * @param {Record<string, any>[]} rows - the input rows
203
+ * @param {AsyncRow[]} rows - the input rows
225
204
  * @param {OrderByItem[]} orderBy - the sort specifications
226
205
  * @param {Record<string, AsyncDataSource>} tables
227
- * @returns {Promise<Record<string, any>[]>} the sorted rows
206
+ * @returns {Promise<AsyncRow[]>} the sorted rows
228
207
  */
229
208
  async function applyOrderBy(rows, orderBy, tables) {
230
209
  if (!orderBy.length) return rows
@@ -236,7 +215,7 @@ async function applyOrderBy(rows, orderBy, tables) {
236
215
  /** @type {SqlPrimitive[]} */
237
216
  const rowValues = []
238
217
  for (const term of orderBy) {
239
- const value = await evaluateExpr({ node: term.expr, row: asyncRow(row), tables })
218
+ const value = await evaluateExpr({ node: term.expr, row, tables })
240
219
  rowValues.push(value)
241
220
  }
242
221
  evaluatedValues.push(rowValues)
@@ -282,10 +261,10 @@ async function applyOrderBy(rows, orderBy, tables) {
282
261
  /**
283
262
  * Evaluates a select with a resolved FROM data source
284
263
  *
285
- * @param {SelectStatement} select - the parsed SQL AST
286
- * @param {AsyncDataSource} dataSource - the async data source
264
+ * @param {SelectStatement} select
265
+ * @param {AsyncDataSource} dataSource
287
266
  * @param {Record<string, AsyncDataSource>} tables
288
- * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
267
+ * @yields {AsyncRow}
289
268
  */
290
269
  async function* evaluateSelectAst(select, dataSource, tables) {
291
270
  // SQL priority: from, where, group by, having, select, order by, offset, limit
@@ -310,7 +289,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
310
289
  * @param {SelectStatement} select
311
290
  * @param {AsyncDataSource} dataSource
312
291
  * @param {Record<string, AsyncDataSource>} tables
313
- * @returns {AsyncGenerator<Record<string, any>>}
292
+ * @yields {AsyncRow}
314
293
  */
315
294
  async function* evaluateStreaming(select, dataSource, tables) {
316
295
  let rowsYielded = 0
@@ -337,17 +316,16 @@ async function* evaluateStreaming(select, dataSource, tables) {
337
316
  }
338
317
 
339
318
  // SELECT projection
340
- /** @type {Record<string, any>} */
319
+ /** @type {AsyncRow} */
341
320
  const outRow = {}
342
321
  for (const col of select.columns) {
343
322
  if (col.kind === 'star') {
344
- const keys = row.getKeys()
345
- for (const key of keys) {
346
- outRow[key] = row.getCell(key)
323
+ for (const [key, cell] of Object.entries(row)) {
324
+ outRow[key] = cell
347
325
  }
348
326
  } else if (col.kind === 'derived') {
349
327
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
350
- outRow[alias] = await evaluateExpr({ node: col.expr, row, tables })
328
+ outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
351
329
  } else if (col.kind === 'aggregate') {
352
330
  throw new Error(
353
331
  'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
@@ -357,7 +335,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
357
335
 
358
336
  // DISTINCT: skip duplicate rows
359
337
  if (seen) {
360
- const key = stableRowKey(outRow)
338
+ const key = await stableRowKey(outRow)
361
339
  if (seen.has(key)) continue
362
340
  seen.add(key)
363
341
  // OFFSET applies to distinct rows
@@ -383,7 +361,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
383
361
  * @param {Record<string, AsyncDataSource>} tables
384
362
  * @param {boolean} hasAggregate
385
363
  * @param {boolean} useGrouping
386
- * @returns {AsyncGenerator<Record<string, any>>}
364
+ * @yields {AsyncRow}
387
365
  */
388
366
  async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
389
367
  // Step 1: Collect all rows from data source
@@ -409,7 +387,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
409
387
  }
410
388
 
411
389
  // Step 3: Projection (grouping vs non-grouping)
412
- /** @type {Record<string, any>[]} */
390
+ /** @type {AsyncRow[]} */
413
391
  let projected = []
414
392
 
415
393
  if (useGrouping) {
@@ -446,15 +424,14 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
446
424
  }
447
425
 
448
426
  for (const group of groups) {
449
- /** @type {Record<string, any>} */
427
+ /** @type {AsyncRow} */
450
428
  const resultRow = {}
451
429
  for (const col of select.columns) {
452
430
  if (col.kind === 'star') {
453
431
  const firstRow = group[0]
454
432
  if (firstRow) {
455
- const keys = firstRow.getKeys()
456
- for (const key of keys) {
457
- resultRow[key] = firstRow.getCell(key)
433
+ for (const [key, cell] of Object.entries(firstRow)) {
434
+ resultRow[key] = cell
458
435
  }
459
436
  }
460
437
  continue
@@ -463,18 +440,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
463
440
  if (col.kind === 'derived') {
464
441
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
465
442
  if (group.length > 0) {
466
- const value = await evaluateExpr({ node: col.expr, row: group[0], tables })
467
- resultRow[alias] = value
443
+ resultRow[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
468
444
  } else {
469
- resultRow[alias] = undefined
445
+ delete resultRow[alias]
470
446
  }
471
447
  continue
472
448
  }
473
449
 
474
450
  if (col.kind === 'aggregate') {
475
451
  const alias = col.alias ?? defaultAggregateAlias(col)
476
- const value = await evaluateAggregate(col, group)
477
- resultRow[alias] = value
452
+ resultRow[alias] = () => evaluateAggregate({ col, rows: group, tables })
478
453
  continue
479
454
  }
480
455
  }
@@ -503,18 +478,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
503
478
  }
504
479
 
505
480
  for (const row of rowsToProject) {
506
- /** @type {Record<string, any>} */
481
+ /** @type {AsyncRow} */
507
482
  const outRow = {}
508
483
  for (const col of select.columns) {
509
484
  if (col.kind === 'star') {
510
- const keys = row.getKeys()
511
- for (const key of keys) {
512
- outRow[key] = row.getCell(key)
485
+ for (const [key, cell] of Object.entries(row)) {
486
+ outRow[key] = cell
513
487
  }
514
488
  } else if (col.kind === 'derived') {
515
489
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
516
- const value = await evaluateExpr({ node: col.expr, row, tables })
517
- outRow[alias] = value
490
+ outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
518
491
  }
519
492
  }
520
493
  projected.push(outRow)
@@ -522,7 +495,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
522
495
  }
523
496
 
524
497
  // Step 4: DISTINCT
525
- projected = applyDistinct(projected, select.distinct)
498
+ projected = await applyDistinct(projected, select.distinct)
526
499
 
527
500
  // Step 5: ORDER BY (final sort for grouped queries)
528
501
  projected = await applyOrderBy(projected, select.orderBy, tables)
@@ -1,5 +1,4 @@
1
1
  import { executeSelect } from './execute.js'
2
- import { collect } from './utils.js'
3
2
 
4
3
  /**
5
4
  * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
@@ -11,7 +10,7 @@ import { collect } from './utils.js'
11
10
  * @param {Object} params
12
11
  * @param {ExprNode} params.node - The expression node to evaluate
13
12
  * @param {AsyncRow} params.row - The data row to evaluate against
14
- * @param {Record<string, AsyncDataSource>} [params.tables]
13
+ * @param {Record<string, AsyncDataSource>} params.tables
15
14
  * @returns {Promise<SqlPrimitive>} The result of the evaluation
16
15
  */
17
16
  export async function evaluateExpr({ node, row, tables }) {
@@ -20,17 +19,30 @@ export async function evaluateExpr({ node, row, tables }) {
20
19
  }
21
20
 
22
21
  if (node.type === 'identifier') {
23
- return row.getCell(node.name)
22
+ // Try exact match first (handles both qualified and unqualified names)
23
+ if (row[node.name]) {
24
+ return row[node.name]()
25
+ }
26
+ // For qualified names like 'users.id', also try just the column part
27
+ if (node.name.includes('.')) {
28
+ const colName = node.name.split('.').pop()
29
+ if (colName && row[colName]) {
30
+ return row[colName]()
31
+ }
32
+ }
33
+ return undefined
24
34
  }
25
35
 
26
36
  // Scalar subquery - returns a single value
27
37
  if (node.type === 'subquery') {
28
- const results = await collect(executeSelect(node.subquery, tables))
29
- if (results.length === 0) return null
30
- // Return the first column of the first row
31
- const firstRow = results[0]
38
+ const gen = executeSelect(node.subquery, tables)
39
+ const first = await gen.next() // Start the generator
40
+ gen.return(undefined) // Stop further execution
41
+ if (!first.value) return null
42
+ /** @type {AsyncRow} */
43
+ const firstRow = first.value
32
44
  const firstKey = Object.keys(firstRow)[0]
33
- return firstRow[firstKey]
45
+ return firstRow[firstKey]()
34
46
  }
35
47
 
36
48
  // Unary operators
@@ -238,35 +250,43 @@ export async function evaluateExpr({ node, row, tables }) {
238
250
  // IN and NOT IN with subqueries
239
251
  if (node.type === 'in') {
240
252
  const exprVal = await evaluateExpr({ node: node.expr, row, tables })
241
- const results = await collect(executeSelect(node.subquery, tables))
242
- if (results.length === 0) return false
243
- const firstKey = Object.keys(results[0])[0]
244
- const values = results.map(r => r[firstKey])
253
+ const results = executeSelect(node.subquery, tables)
254
+ /** @type {SqlPrimitive[]} */
255
+ const values = []
256
+ for await (const resRow of results) {
257
+ const firstKey = Object.keys(resRow)[0]
258
+ const val = await resRow[firstKey]()
259
+ values.push(val)
260
+ }
245
261
  return values.includes(exprVal)
246
262
  }
247
263
  if (node.type === 'not in') {
248
264
  const exprVal = await evaluateExpr({ node: node.expr, row, tables })
249
- const results = await collect(executeSelect(node.subquery, tables))
250
- if (results.length === 0) return true
251
- const firstKey = Object.keys(results[0])[0]
252
- const values = results.map(r => r[firstKey])
265
+ const results = executeSelect(node.subquery, tables)
266
+ /** @type {SqlPrimitive[]} */
267
+ const values = []
268
+ for await (const resRow of results) {
269
+ const firstKey = Object.keys(resRow)[0]
270
+ const val = await resRow[firstKey]()
271
+ values.push(val)
272
+ }
253
273
  return !values.includes(exprVal)
254
274
  }
255
275
 
256
276
  // EXISTS and NOT EXISTS with subqueries
257
277
  if (node.type === 'exists') {
258
- const results = await collect(executeSelect(node.subquery, tables))
259
- return results.length > 0
278
+ const results = await executeSelect(node.subquery, tables).next()
279
+ return results.done === false
260
280
  }
261
281
  if (node.type === 'not exists') {
262
- const results = await collect(executeSelect(node.subquery, tables))
263
- return results.length === 0
282
+ const results = await executeSelect(node.subquery, tables).next()
283
+ return results.done === true
264
284
  }
265
285
 
266
286
  // CASE expressions
267
287
  if (node.type === 'case') {
268
288
  // For simple CASE: evaluate the case expression once
269
- const caseValue = node.caseExpr ? await evaluateExpr({ node: node.caseExpr, row, tables }) : undefined
289
+ const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
270
290
 
271
291
  // Iterate through WHEN clauses
272
292
  for (const whenClause of node.whenClauses) {