squirreling 0.2.6 → 0.3.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,30 +7,56 @@
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-89-darkred)
10
+ ![coverage](https://img.shields.io/badge/Coverage-93-darkred)
11
11
  [![dependencies](https://img.shields.io/badge/Dependencies-0-blueviolet)](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
12
12
 
13
- Squirreling is a lightweight SQL engine for JavaScript applications, designed to provide efficient and easy-to-use database functionalities in the browser.
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.
14
14
 
15
15
  ## Features
16
16
 
17
17
  - Lightweight and fast
18
- - Easy to integrate with JavaScript applications
18
+ - Easy to integrate with frontend applications
19
+ - Lets you move query execution closer to your users
19
20
  - Supports standard SQL queries
20
- - In-memory database for quick data access
21
- - Robust error handling and validation
21
+ - Async streaming for large datasets
22
+ - Constant memory usage for simple queries with LIMIT
23
+ - Robust error handling and validation designed for LLM tool use
24
+ - In-memory data option for simple use cases
25
+ - Select only
26
+ - No joins (yet)
22
27
 
23
28
  ## Usage
24
29
 
30
+ Squirreling returns an async generator, allowing you to process rows one at a time without loading everything into memory.
31
+
25
32
  ```javascript
26
33
  import { executeSql } from 'squirreling'
27
34
 
28
- const source = [
29
- { id: 1, name: 'Alice' },
30
- { id: 2, name: 'Bob' },
35
+ // In-memory table
36
+ const users = [
37
+ { id: 1, name: 'Alice', active: true },
38
+ { id: 2, name: 'Bob', active: false },
39
+ { id: 3, name: 'Charlie', active: true },
40
+ // ...more rows
31
41
  ]
32
42
 
33
- const result = executeSql({ source, query: 'SELECT UPPER(name) AS name_upper FROM users' })
34
- console.log(result)
35
- // Output: [ { name_upper: 'ALICE' }, { name_upper: 'BOB' } ]
43
+ // Process rows as they arrive (streaming)
44
+ for await (const { cnt } of executeSql({
45
+ tables: { users },
46
+ query: 'SELECT count(*) as cnt FROM users WHERE active = TRUE LIMIT 10',
47
+ })) {
48
+ console.log('Count', cnt)
49
+ }
50
+ ```
51
+
52
+ There is an exported helper function `collect` to gather all rows into an array if needed:
53
+
54
+ ```javascript
55
+ import { collect, executeSql } from 'squirreling'
56
+
57
+ const allUsers = await collect(executeSql({
58
+ tables: { users },
59
+ query: 'SELECT * FROM users',
60
+ }))
61
+ console.log(allUsers)
36
62
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -38,10 +38,10 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "24.10.1",
41
- "@vitest/coverage-v8": "4.0.13",
41
+ "@vitest/coverage-v8": "4.0.14",
42
42
  "eslint": "9.39.1",
43
43
  "eslint-plugin-jsdoc": "61.4.1",
44
44
  "typescript": "5.9.3",
45
- "vitest": "4.0.13"
45
+ "vitest": "4.0.14"
46
46
  }
47
47
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @import { AsyncDataSource, AsyncRow } from '../types.js'
3
+ */
4
+
5
+
6
+ /**
7
+ * Wraps an async generator of plain objects into an AsyncDataSource
8
+ *
9
+ * @param {AsyncGenerator<Record<string, any>>} gen
10
+ * @returns {AsyncDataSource}
11
+ */
12
+ export function generatorSource(gen) {
13
+ return {
14
+ async *getRows() {
15
+ for await (const row of gen) {
16
+ yield asyncRow(row)
17
+ }
18
+ },
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Creates an async row accessor that wraps a plain JavaScript object
24
+ *
25
+ * @param {Record<string, any>} obj - the plain object
26
+ * @returns {AsyncRow} a row accessor interface
27
+ */
28
+ export function asyncRow(obj) {
29
+ return {
30
+ getCell(name) {
31
+ return obj[name]
32
+ },
33
+ getKeys() {
34
+ return Object.keys(obj)
35
+ },
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Creates an async memory-backed data source from an array of plain objects
41
+ *
42
+ * @param {Record<string, any>[]} data - array of plain objects
43
+ * @returns {AsyncDataSource} an async data source interface
44
+ */
45
+ export function memorySource(data) {
46
+ return {
47
+ async *getRows() {
48
+ for (const item of data) {
49
+ yield asyncRow(item)
50
+ }
51
+ },
52
+ }
53
+ }
@@ -3,19 +3,19 @@ import { evaluateExpr } from './expression.js'
3
3
  /**
4
4
  * Evaluates an aggregate function over a set of rows
5
5
  *
6
- * @import { AggregateColumn, ExprNode, RowSource } from '../types.js'
6
+ * @import { AggregateColumn, ExprNode, AsyncRow } from '../types.js'
7
7
  * @param {AggregateColumn} col - aggregate column definition
8
- * @param {RowSource[]} rows - rows to aggregate
9
- * @returns {number | null} aggregated result
8
+ * @param {AsyncRow[]} rows - rows to aggregate
9
+ * @returns {Promise<number | null>} aggregated result
10
10
  */
11
- export function evaluateAggregate(col, rows) {
11
+ export async function evaluateAggregate(col, rows) {
12
12
  const { arg, func } = col
13
13
 
14
14
  if (func === 'COUNT') {
15
15
  if (arg.kind === 'star') return rows.length
16
16
  let count = 0
17
17
  for (let i = 0; i < rows.length; i += 1) {
18
- const v = evaluateExpr(arg.expr, rows[i])
18
+ const v = await evaluateExpr({ node: arg.expr, row: rows[i] })
19
19
  if (v !== null && v !== undefined) {
20
20
  count += 1
21
21
  }
@@ -35,7 +35,7 @@ export function evaluateAggregate(col, rows) {
35
35
  let max = null
36
36
 
37
37
  for (let i = 0; i < rows.length; i += 1) {
38
- const raw = evaluateExpr(arg.expr, rows[i])
38
+ const raw = await evaluateExpr({ node: arg.expr, row: rows[i] })
39
39
  if (raw == null) continue
40
40
  const num = Number(raw)
41
41
  if (!Number.isFinite(num)) continue
@@ -1,23 +1,68 @@
1
- import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
2
1
  import { evaluateExpr } from './expression.js'
3
- import { evaluateHavingExpr } from './having.js'
4
2
  import { parseSql } from '../parse/parse.js'
5
- import { createMemorySource, createRowAccessor } from '../backend/memory.js'
3
+ import { asyncRow, generatorSource, memorySource } from '../backend/dataSource.js'
4
+ import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
5
+ import { evaluateHavingExpr } from './having.js'
6
6
 
7
7
  /**
8
- * @import { DataSource, ExecuteSqlOptions, ExprNode, OrderByItem, RowSource, SelectStatement, SqlPrimitive } from '../types.js'
8
+ * @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
9
9
  */
10
10
 
11
11
  /**
12
- * Executes a SQL SELECT query against a data source
12
+ * Executes a SQL SELECT query against named data sources
13
13
  *
14
14
  * @param {ExecuteSqlOptions} options - the execution options
15
- * @returns {Record<string, any>[]} the result rows matching the query
15
+ * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
16
16
  */
17
- export function executeSql({ source, query }) {
17
+ export async function* executeSql({ tables, query }) {
18
18
  const select = parseSql(query)
19
- const dataSource = Array.isArray(source) ? createMemorySource(source) : source
20
- return evaluateSelectAst(select, dataSource)
19
+
20
+ // Check for unsupported operations
21
+ if (select.joins.length) {
22
+ throw new Error('JOIN is not supported')
23
+ }
24
+ if (!select.from) {
25
+ throw new Error('FROM clause is required')
26
+ }
27
+
28
+ // Normalize tables: convert arrays to AsyncDataSource
29
+ /** @type {Record<string, AsyncDataSource>} */
30
+ const normalizedTables = {}
31
+ for (const [name, source] of Object.entries(tables)) {
32
+ if (Array.isArray(source)) {
33
+ normalizedTables[name] = memorySource(source)
34
+ } else {
35
+ normalizedTables[name] = source
36
+ }
37
+ }
38
+
39
+ yield* executeSelect(select, normalizedTables)
40
+ }
41
+
42
+ /**
43
+ * Executes a SELECT query against the provided tables
44
+ *
45
+ * @param {SelectStatement} select
46
+ * @param {Record<string, AsyncDataSource>} tables
47
+ * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
48
+ */
49
+ export async function* executeSelect(select, tables) {
50
+ /** @type {AsyncDataSource} */
51
+ 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`)
57
+ }
58
+
59
+ dataSource = table
60
+ } else {
61
+ // Nested subquery - recursively resolve
62
+ dataSource = generatorSource(executeSelect(select.from.query, tables))
63
+ }
64
+
65
+ yield* evaluateSelectAst(select, dataSource, tables)
21
66
  }
22
67
 
23
68
  /**
@@ -112,32 +157,108 @@ function applyDistinct(rows, distinct) {
112
157
  return result
113
158
  }
114
159
 
160
+ /**
161
+ * Applies ORDER BY sorting to RowSource array (before projection)
162
+ *
163
+ * @param {AsyncRow[]} rows - the input row sources
164
+ * @param {OrderByItem[]} orderBy - the sort specifications
165
+ * @param {Record<string, AsyncDataSource>} tables
166
+ * @returns {Promise<AsyncRow[]>} the sorted row sources
167
+ */
168
+ async function sortRowSources(rows, orderBy, tables) {
169
+ if (!orderBy.length) return rows
170
+
171
+ // Pre-evaluate ORDER BY expressions for all rows
172
+ /** @type {SqlPrimitive[][]} */
173
+ const evaluatedValues = []
174
+ for (const row of rows) {
175
+ /** @type {SqlPrimitive[]} */
176
+ const rowValues = []
177
+ for (const term of orderBy) {
178
+ const value = await evaluateExpr({ node: term.expr, row, tables })
179
+ rowValues.push(value)
180
+ }
181
+ evaluatedValues.push(rowValues)
182
+ }
183
+
184
+ // Create index array and sort it
185
+ const indices = rows.map((_, i) => i)
186
+ indices.sort((aIdx, bIdx) => {
187
+ for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
188
+ const term = orderBy[termIdx]
189
+ const dir = term.direction
190
+ const av = evaluatedValues[aIdx][termIdx]
191
+ const bv = evaluatedValues[bIdx][termIdx]
192
+
193
+ // Handle NULLS FIRST / NULLS LAST
194
+ const aIsNull = av == null
195
+ const bIsNull = bv == null
196
+
197
+ if (aIsNull || bIsNull) {
198
+ if (aIsNull && bIsNull) continue
199
+
200
+ const nullsFirst = term.nulls === 'LAST' ? false : true
201
+
202
+ if (aIsNull) {
203
+ return nullsFirst ? -1 : 1
204
+ } else {
205
+ return nullsFirst ? 1 : -1
206
+ }
207
+ }
208
+
209
+ const cmp = compareValues(av, bv)
210
+ if (cmp !== 0) {
211
+ return dir === 'DESC' ? -cmp : cmp
212
+ }
213
+ }
214
+ return 0
215
+ })
216
+
217
+ // Return sorted rows
218
+ return indices.map(i => rows[i])
219
+ }
220
+
115
221
  /**
116
222
  * Applies ORDER BY sorting to rows
117
223
  *
118
224
  * @param {Record<string, any>[]} rows - the input rows
119
225
  * @param {OrderByItem[]} orderBy - the sort specifications
120
- * @returns {Record<string, any>[]} the sorted rows
226
+ * @param {Record<string, AsyncDataSource>} tables
227
+ * @returns {Promise<Record<string, any>[]>} the sorted rows
121
228
  */
122
- function applyOrderBy(rows, orderBy) {
123
- if (!orderBy?.length) return rows
229
+ async function applyOrderBy(rows, orderBy, tables) {
230
+ if (!orderBy.length) return rows
124
231
 
125
- const sorted = rows.slice()
126
- sorted.sort((a, b) => {
232
+ // Pre-evaluate ORDER BY expressions for all rows
233
+ /** @type {SqlPrimitive[][]} */
234
+ const evaluatedValues = []
235
+ for (const row of rows) {
236
+ /** @type {SqlPrimitive[]} */
237
+ const rowValues = []
127
238
  for (const term of orderBy) {
239
+ const value = await evaluateExpr({ node: term.expr, row: asyncRow(row), tables })
240
+ rowValues.push(value)
241
+ }
242
+ evaluatedValues.push(rowValues)
243
+ }
244
+
245
+ // Create index array and sort it
246
+ const indices = rows.map((_, i) => i)
247
+ indices.sort((aIdx, bIdx) => {
248
+ for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
249
+ const term = orderBy[termIdx]
128
250
  const dir = term.direction
129
- const av = evaluateExpr(term.expr, createRowAccessor(a))
130
- const bv = evaluateExpr(term.expr, createRowAccessor(b))
251
+ const av = evaluatedValues[aIdx][termIdx]
252
+ const bv = evaluatedValues[bIdx][termIdx]
131
253
 
132
254
  // Handle NULLS FIRST / NULLS LAST
133
255
  const aIsNull = av == null
134
256
  const bIsNull = bv == null
135
257
 
136
258
  if (aIsNull || bIsNull) {
137
- if (aIsNull && bIsNull) continue // both null, try next sort term
259
+ if (aIsNull && bIsNull) continue
138
260
 
139
- // Determine null ordering
140
- const nullsFirst = term.nulls === 'LAST' ? false : true // default is NULLS FIRST
261
+ const nullsFirst = term.nulls === 'LAST' ? false : true
141
262
 
142
263
  if (aIsNull) {
143
264
  return nullsFirst ? -1 : 1
@@ -154,59 +275,156 @@ function applyOrderBy(rows, orderBy) {
154
275
  return 0
155
276
  })
156
277
 
157
- return sorted
278
+ // Return sorted rows
279
+ return indices.map(i => rows[i])
158
280
  }
159
281
 
160
282
  /**
161
- * Evaluates a parsed SELECT AST against data rows
283
+ * Evaluates a select with a resolved FROM data source
162
284
  *
163
285
  * @param {SelectStatement} select - the parsed SQL AST
164
- * @param {DataSource} dataSource - the data source
165
- * @returns {Record<string, any>[]} the filtered, projected, and sorted result rows
286
+ * @param {AsyncDataSource} dataSource - the async data source
287
+ * @param {Record<string, AsyncDataSource>} tables
288
+ * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
166
289
  */
167
- function evaluateSelectAst(select, dataSource) {
168
- // Check for unsupported JOIN operations
169
- if (select.joins.length) {
170
- throw new Error('JOIN is not supported')
171
- }
290
+ async function* evaluateSelectAst(select, dataSource, tables) {
291
+ // SQL priority: from, where, group by, having, select, order by, offset, limit
292
+
293
+ const hasAggregate = select.columns.some(col => col.kind === 'aggregate')
294
+ const useGrouping = hasAggregate || select.groupBy.length > 0
295
+ const needsBuffering = useGrouping || select.orderBy.length > 0
172
296
 
173
- // Check for unsupported subquery in FROM clause
174
- if (typeof select.from !== 'string') {
175
- throw new Error('Subquery in FROM clause is not supported')
297
+ if (needsBuffering) {
298
+ // BUFFERING PATH: Collect all rows, process, then yield
299
+ yield* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping)
300
+ } else {
301
+ // STREAMING PATH: Yield rows one by one
302
+ yield* evaluateStreaming(select, dataSource, tables)
176
303
  }
304
+ }
177
305
 
178
- // SQL priority: from, where, group by, having, select, order by, offset, limit
306
+ /**
307
+ * Streaming evaluation for simple queries (no ORDER BY or GROUP BY)
308
+ * Supports DISTINCT by tracking seen row keys without buffering full rows
309
+ *
310
+ * @param {SelectStatement} select
311
+ * @param {AsyncDataSource} dataSource
312
+ * @param {Record<string, AsyncDataSource>} tables
313
+ * @returns {AsyncGenerator<Record<string, any>>}
314
+ */
315
+ async function* evaluateStreaming(select, dataSource, tables) {
316
+ let rowsYielded = 0
317
+ let rowsSkipped = 0
318
+ const offset = select.offset ?? 0
319
+ const limit = select.limit ?? Infinity
320
+ if (limit <= 0) return
321
+
322
+ // For DISTINCT, track seen row keys
323
+ /** @type {Set<string> | undefined} */
324
+ const seen = select.distinct ? new Set() : undefined
325
+
326
+ for await (const row of dataSource.getRows()) {
327
+ // WHERE filter
328
+ if (select.where) {
329
+ const pass = await evaluateExpr({ node: select.where, row, tables })
330
+ if (!pass) continue
331
+ }
179
332
 
180
- // WHERE clause filtering
181
- /** @type {RowSource[]} */
182
- const working = []
183
- const length = dataSource.getNumRows()
184
- for (let i = 0; i < length; i++) {
185
- const row = dataSource.getRow(i)
186
- if (!select.where || evaluateExpr(select.where, row)) {
187
- working.push(row)
333
+ // For non-DISTINCT queries, we can skip rows before projection (optimization)
334
+ if (!seen && rowsSkipped < offset) {
335
+ rowsSkipped++
336
+ continue
337
+ }
338
+
339
+ // SELECT projection
340
+ /** @type {Record<string, any>} */
341
+ const outRow = {}
342
+ for (const col of select.columns) {
343
+ if (col.kind === 'star') {
344
+ const keys = row.getKeys()
345
+ for (const key of keys) {
346
+ outRow[key] = row.getCell(key)
347
+ }
348
+ } else if (col.kind === 'derived') {
349
+ const alias = col.alias ?? defaultDerivedAlias(col.expr)
350
+ outRow[alias] = await evaluateExpr({ node: col.expr, row, tables })
351
+ } else if (col.kind === 'aggregate') {
352
+ throw new Error(
353
+ 'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
354
+ )
355
+ }
356
+ }
357
+
358
+ // DISTINCT: skip duplicate rows
359
+ if (seen) {
360
+ const key = stableRowKey(outRow)
361
+ if (seen.has(key)) continue
362
+ seen.add(key)
363
+ // OFFSET applies to distinct rows
364
+ if (rowsSkipped < offset) {
365
+ rowsSkipped++
366
+ continue
367
+ }
368
+ }
369
+
370
+ yield outRow
371
+ rowsYielded++
372
+ if (rowsYielded >= limit) {
373
+ break
188
374
  }
189
375
  }
376
+ }
190
377
 
191
- const hasAggregate = select.columns.some(col => col.kind === 'aggregate')
192
- const useGrouping = hasAggregate || select.groupBy?.length > 0
378
+ /**
379
+ * Buffered evaluation for complex queries (with ORDER BY or GROUP BY)
380
+ *
381
+ * @param {SelectStatement} select
382
+ * @param {AsyncDataSource} dataSource
383
+ * @param {Record<string, AsyncDataSource>} tables
384
+ * @param {boolean} hasAggregate
385
+ * @param {boolean} useGrouping
386
+ * @returns {AsyncGenerator<Record<string, any>>}
387
+ */
388
+ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
389
+ // Step 1: Collect all rows from data source
390
+ /** @type {AsyncRow[]} */
391
+ const working = []
392
+ for await (const row of dataSource.getRows()) {
393
+ working.push(row)
394
+ }
395
+
396
+ // Step 2: WHERE clause filtering
397
+ /** @type {AsyncRow[]} */
398
+ const filtered = []
193
399
 
400
+ for (const row of working) {
401
+ if (select.where) {
402
+ const passes = await evaluateExpr({ node: select.where, row, tables })
403
+
404
+ if (!passes) {
405
+ continue
406
+ }
407
+ }
408
+ filtered.push(row)
409
+ }
410
+
411
+ // Step 3: Projection (grouping vs non-grouping)
194
412
  /** @type {Record<string, any>[]} */
195
- const projected = []
413
+ let projected = []
196
414
 
197
415
  if (useGrouping) {
198
416
  // Grouping due to GROUP BY or aggregate functions
199
- /** @type {RowSource[][]} */
417
+ /** @type {AsyncRow[][]} */
200
418
  const groups = []
201
419
 
202
- if (select.groupBy?.length) {
203
- /** @type {Map<string, RowSource[]>} */
420
+ if (select.groupBy.length) {
421
+ /** @type {Map<string, AsyncRow[]>} */
204
422
  const map = new Map()
205
- for (const row of working) {
423
+ for (const row of filtered) {
206
424
  /** @type {string[]} */
207
425
  const keyParts = []
208
426
  for (const expr of select.groupBy) {
209
- const v = evaluateExpr(expr, row)
427
+ const v = await evaluateExpr({ node: expr, row, tables })
210
428
  keyParts.push(JSON.stringify(v))
211
429
  }
212
430
  const key = keyParts.join('|')
@@ -219,7 +437,7 @@ function evaluateSelectAst(select, dataSource) {
219
437
  group.push(row)
220
438
  }
221
439
  } else {
222
- groups.push(working)
440
+ groups.push(filtered)
223
441
  }
224
442
 
225
443
  const hasStar = select.columns.some(col => col.kind === 'star')
@@ -244,14 +462,18 @@ function evaluateSelectAst(select, dataSource) {
244
462
 
245
463
  if (col.kind === 'derived') {
246
464
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
247
- const value = group.length > 0 ? evaluateExpr(col.expr, group[0]) : undefined
248
- resultRow[alias] = value
465
+ if (group.length > 0) {
466
+ const value = await evaluateExpr({ node: col.expr, row: group[0], tables })
467
+ resultRow[alias] = value
468
+ } else {
469
+ resultRow[alias] = undefined
470
+ }
249
471
  continue
250
472
  }
251
473
 
252
474
  if (col.kind === 'aggregate') {
253
475
  const alias = col.alias ?? defaultAggregateAlias(col)
254
- const value = evaluateAggregate(col, group)
476
+ const value = await evaluateAggregate(col, group)
255
477
  resultRow[alias] = value
256
478
  continue
257
479
  }
@@ -259,9 +481,7 @@ function evaluateSelectAst(select, dataSource) {
259
481
 
260
482
  // Apply HAVING filter before adding to projected results
261
483
  if (select.having) {
262
- // For HAVING, we need to evaluate aggregates in the context of the group
263
- // Create a special row context that includes both the group data and aggregate values
264
- if (!evaluateHavingExpr(select.having, resultRow, group)) {
484
+ if (!await evaluateHavingExpr(select.having, resultRow, group, tables)) {
265
485
  continue
266
486
  }
267
487
  }
@@ -270,7 +490,19 @@ function evaluateSelectAst(select, dataSource) {
270
490
  }
271
491
  } else {
272
492
  // No grouping, simple projection
273
- for (const row of working) {
493
+ // Sort before projection so ORDER BY can access columns not in SELECT
494
+ const sorted = await sortRowSources(filtered, select.orderBy, tables)
495
+
496
+ // OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
497
+ // to avoid reading expensive cells for rows that won't be in the final result
498
+ let rowsToProject = sorted
499
+ if (!select.distinct) {
500
+ const start = select.offset ?? 0
501
+ const end = select.limit ? start + select.limit : sorted.length
502
+ rowsToProject = sorted.slice(start, end)
503
+ }
504
+
505
+ for (const row of rowsToProject) {
274
506
  /** @type {Record<string, any>} */
275
507
  const outRow = {}
276
508
  for (const col of select.columns) {
@@ -281,29 +513,34 @@ function evaluateSelectAst(select, dataSource) {
281
513
  }
282
514
  } else if (col.kind === 'derived') {
283
515
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
284
- const value = evaluateExpr(col.expr, row)
516
+ const value = await evaluateExpr({ node: col.expr, row, tables })
285
517
  outRow[alias] = value
286
- } else if (col.kind === 'aggregate') {
287
- throw new Error(
288
- 'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
289
- )
290
518
  }
291
519
  }
292
520
  projected.push(outRow)
293
521
  }
294
522
  }
295
523
 
296
- let result = projected
524
+ // Step 4: DISTINCT
525
+ projected = applyDistinct(projected, select.distinct)
297
526
 
298
- result = applyDistinct(result, select.distinct)
299
- result = applyOrderBy(result, select.orderBy)
527
+ // Step 5: ORDER BY (final sort for grouped queries)
528
+ projected = await applyOrderBy(projected, select.orderBy, tables)
300
529
 
301
- if (typeof select.offset === 'number' && select.offset > 0) {
302
- result = result.slice(select.offset)
303
- }
304
- if (typeof select.limit === 'number') {
305
- result = result.slice(0, select.limit)
306
- }
530
+ // Step 6: OFFSET and LIMIT
531
+ // For non-DISTINCT, non-grouping queries, OFFSET/LIMIT was already applied before projection
532
+ if (select.distinct || useGrouping) {
533
+ const start = select.offset ?? 0
534
+ const end = select.limit ? start + select.limit : projected.length
307
535
 
308
- return result
536
+ // Step 7: Yield results
537
+ for (let i = start; i < end && i < projected.length; i++) {
538
+ yield projected[i]
539
+ }
540
+ } else {
541
+ // Already limited, yield all projected rows
542
+ for (const row of projected) {
543
+ yield row
544
+ }
545
+ }
309
546
  }
@@ -1,13 +1,20 @@
1
+ import { executeSelect } from './execute.js'
2
+ import { collect } from './utils.js'
1
3
 
2
4
  /**
3
- * Evaluates an expression node against a row of data
5
+ * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
6
+ */
7
+
8
+ /**
9
+ * Evaluates an expression node against a row of data (async version)
4
10
  *
5
- * @import { ExprNode, RowSource, SqlPrimitive } from '../types.js'
6
- * @param {ExprNode} node - The expression node to evaluate
7
- * @param {RowSource} row - The data row to evaluate against
8
- * @returns {SqlPrimitive} The result of the evaluation
11
+ * @param {Object} params
12
+ * @param {ExprNode} params.node - The expression node to evaluate
13
+ * @param {AsyncRow} params.row - The data row to evaluate against
14
+ * @param {Record<string, AsyncDataSource>} [params.tables]
15
+ * @returns {Promise<SqlPrimitive>} The result of the evaluation
9
16
  */
10
- export function evaluateExpr(node, row) {
17
+ export async function evaluateExpr({ node, row, tables }) {
11
18
  if (node.type === 'literal') {
12
19
  return node.value
13
20
  }
@@ -16,19 +23,29 @@ export function evaluateExpr(node, row) {
16
23
  return row.getCell(node.name)
17
24
  }
18
25
 
26
+ // Scalar subquery - returns a single value
27
+ 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]
32
+ const firstKey = Object.keys(firstRow)[0]
33
+ return firstRow[firstKey]
34
+ }
35
+
19
36
  // Unary operators
20
37
  if (node.type === 'unary') {
21
38
  if (node.op === 'NOT') {
22
- return !evaluateExpr(node.argument, row)
39
+ return !await evaluateExpr({ node: node.argument, row, tables })
23
40
  }
24
41
  if (node.op === 'IS NULL') {
25
- return evaluateExpr(node.argument, row) == null
42
+ return await evaluateExpr({ node: node.argument, row, tables }) == null
26
43
  }
27
44
  if (node.op === 'IS NOT NULL') {
28
- return evaluateExpr(node.argument, row) != null
45
+ return await evaluateExpr({ node: node.argument, row, tables }) != null
29
46
  }
30
47
  if (node.op === '-') {
31
- const val = evaluateExpr(node.argument, row)
48
+ const val = await evaluateExpr({ node: node.argument, row, tables })
32
49
  if (val == null) return null
33
50
  return -Number(val)
34
51
  }
@@ -37,19 +54,19 @@ export function evaluateExpr(node, row) {
37
54
  // Binary operators
38
55
  if (node.type === 'binary') {
39
56
  if (node.op === 'AND') {
40
- const leftVal = evaluateExpr(node.left, row)
57
+ const leftVal = await evaluateExpr({ node: node.left, row, tables })
41
58
  if (!leftVal) return false
42
- return Boolean(evaluateExpr(node.right, row))
59
+ return Boolean(await evaluateExpr({ node: node.right, row, tables }))
43
60
  }
44
61
 
45
62
  if (node.op === 'OR') {
46
- const leftVal = evaluateExpr(node.left, row)
63
+ const leftVal = await evaluateExpr({ node: node.left, row, tables })
47
64
  if (leftVal) return true
48
- return Boolean(evaluateExpr(node.right, row))
65
+ return Boolean(await evaluateExpr({ node: node.right, row, tables }))
49
66
  }
50
67
 
51
- const left = evaluateExpr(node.left, row)
52
- const right = evaluateExpr(node.right, row)
68
+ const left = await evaluateExpr({ node: node.left, row, tables })
69
+ const right = await evaluateExpr({ node: node.right, row, tables })
53
70
 
54
71
  // In SQL, NULL comparisons with =, !=, <> always return false (unknown)
55
72
  // You must use IS NULL or IS NOT NULL to check for NULL
@@ -83,9 +100,9 @@ export function evaluateExpr(node, row) {
83
100
 
84
101
  // BETWEEN and NOT BETWEEN
85
102
  if (node.type === 'between' || node.type === 'not between') {
86
- const expr = evaluateExpr(node.expr, row)
87
- const lower = evaluateExpr(node.lower, row)
88
- const upper = evaluateExpr(node.upper, row)
103
+ const expr = await evaluateExpr({ node: node.expr, row, tables })
104
+ const lower = await evaluateExpr({ node: node.lower, row, tables })
105
+ const upper = await evaluateExpr({ node: node.upper, row, tables })
89
106
 
90
107
  // If any value is NULL, return false (SQL behavior)
91
108
  if (expr == null || lower == null || upper == null) {
@@ -99,7 +116,7 @@ export function evaluateExpr(node, row) {
99
116
  // Function calls
100
117
  if (node.type === 'function') {
101
118
  const funcName = node.name.toUpperCase()
102
- const args = node.args.map(arg => evaluateExpr(arg, row))
119
+ const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
103
120
 
104
121
  if (funcName === 'UPPER') {
105
122
  if (args.length !== 1) throw new Error('UPPER requires exactly 1 argument')
@@ -161,11 +178,21 @@ export function evaluateExpr(node, row) {
161
178
  return String(val).trim()
162
179
  }
163
180
 
181
+ if (funcName === 'REPLACE') {
182
+ if (args.length !== 3) throw new Error('REPLACE requires exactly 3 arguments')
183
+ const str = args[0]
184
+ const searchStr = args[1]
185
+ const replaceStr = args[2]
186
+ // SQL REPLACE returns NULL if any argument is NULL
187
+ if (str == null || searchStr == null || replaceStr == null) return null
188
+ return String(str).replaceAll(String(searchStr), String(replaceStr))
189
+ }
190
+
164
191
  throw new Error('Unsupported function ' + funcName)
165
192
  }
166
193
 
167
194
  if (node.type === 'cast') {
168
- const val = evaluateExpr(node.expr, row)
195
+ const val = await evaluateExpr({ node: node.expr, row, tables })
169
196
  if (val == null) return null
170
197
  const toType = node.toType.toUpperCase()
171
198
  if (toType === 'INTEGER' || toType === 'INT') {
@@ -192,17 +219,17 @@ export function evaluateExpr(node, row) {
192
219
 
193
220
  // IN and NOT IN with value lists
194
221
  if (node.type === 'in valuelist') {
195
- const exprVal = evaluateExpr(node.expr, row)
222
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables })
196
223
  for (const valueNode of node.values) {
197
- const val = evaluateExpr(valueNode, row)
224
+ const val = await evaluateExpr({ node: valueNode, row, tables })
198
225
  if (exprVal === val) return true
199
226
  }
200
227
  return false
201
228
  }
202
229
  if (node.type === 'not in valuelist') {
203
- const exprVal = evaluateExpr(node.expr, row)
230
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables })
204
231
  for (const valueNode of node.values) {
205
- const val = evaluateExpr(valueNode, row)
232
+ const val = await evaluateExpr({ node: valueNode, row, tables })
206
233
  if (exprVal === val) return false
207
234
  }
208
235
  return true
@@ -210,45 +237,57 @@ export function evaluateExpr(node, row) {
210
237
 
211
238
  // IN and NOT IN with subqueries
212
239
  if (node.type === 'in') {
213
- throw new Error('WHERE IN with subqueries is not yet supported.')
240
+ 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])
245
+ return values.includes(exprVal)
214
246
  }
215
247
  if (node.type === 'not in') {
216
- throw new Error('WHERE NOT IN with subqueries is not yet supported.')
248
+ 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])
253
+ return !values.includes(exprVal)
217
254
  }
218
255
 
219
256
  // EXISTS and NOT EXISTS with subqueries
220
257
  if (node.type === 'exists') {
221
- throw new Error('WHERE EXISTS with subqueries is not yet supported.')
258
+ const results = await collect(executeSelect(node.subquery, tables))
259
+ return results.length > 0
222
260
  }
223
261
  if (node.type === 'not exists') {
224
- throw new Error('WHERE NOT EXISTS with subqueries is not yet supported.')
262
+ const results = await collect(executeSelect(node.subquery, tables))
263
+ return results.length === 0
225
264
  }
226
265
 
227
266
  // CASE expressions
228
267
  if (node.type === 'case') {
229
268
  // For simple CASE: evaluate the case expression once
230
- const caseValue = node.caseExpr ? evaluateExpr(node.caseExpr, row) : undefined
269
+ const caseValue = node.caseExpr ? await evaluateExpr({ node: node.caseExpr, row, tables }) : undefined
231
270
 
232
271
  // Iterate through WHEN clauses
233
272
  for (const whenClause of node.whenClauses) {
234
273
  let conditionResult
235
274
  if (caseValue !== undefined) {
236
275
  // Simple CASE: compare caseValue with condition
237
- const whenValue = evaluateExpr(whenClause.condition, row)
276
+ const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables })
238
277
  conditionResult = caseValue === whenValue
239
278
  } else {
240
279
  // Searched CASE: evaluate condition as boolean
241
- conditionResult = evaluateExpr(whenClause.condition, row)
280
+ conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables })
242
281
  }
243
282
 
244
283
  if (conditionResult) {
245
- return evaluateExpr(whenClause.result, row)
284
+ return evaluateExpr({ node: whenClause.result, row, tables })
246
285
  }
247
286
  }
248
287
 
249
288
  // No WHEN clause matched, return ELSE result or NULL
250
289
  if (node.elseResult) {
251
- return evaluateExpr(node.elseResult, row)
290
+ return evaluateExpr({ node: node.elseResult, row, tables })
252
291
  }
253
292
  return null
254
293
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { AggregateFunc, ExprNode, RowSource, SqlPrimitive } from '../types.js'
2
+ * @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
  import { isAggregateFunc } from '../validation.js'
@@ -9,8 +9,8 @@ import { evaluateExpr } from './expression.js'
9
9
  * Creates a context for evaluating HAVING expressions
10
10
  *
11
11
  * @param {Record<string, any>} resultRow - the aggregated result row
12
- * @param {RowSource[]} group - the group of rows
13
- * @returns {RowSource} a context row for HAVING evaluation
12
+ * @param {AsyncRow[]} group - the group of rows
13
+ * @returns {AsyncRow} a context row for HAVING evaluation
14
14
  */
15
15
  function createHavingContext(resultRow, group) {
16
16
  // Include the first row of the group (for GROUP BY columns)
@@ -42,10 +42,11 @@ function createHavingContext(resultRow, group) {
42
42
  *
43
43
  * @param {ExprNode} expr - the HAVING expression
44
44
  * @param {Record<string, any>} row - the aggregated result row
45
- * @param {RowSource[]} group - the group of rows for re-evaluating aggregates
46
- * @returns {boolean} whether the HAVING condition is satisfied
45
+ * @param {AsyncRow[]} group - the group of rows for re-evaluating aggregates
46
+ * @param {Record<string, AsyncDataSource>} tables
47
+ * @returns {Promise<boolean>} whether the HAVING condition is satisfied
47
48
  */
48
- export function evaluateHavingExpr(expr, row, group) {
49
+ export async function evaluateHavingExpr(expr, row, group, tables) {
49
50
  const context = createHavingContext(row, group)
50
51
 
51
52
  // For HAVING, we need special handling of aggregate functions
@@ -54,13 +55,13 @@ export function evaluateHavingExpr(expr, row, group) {
54
55
  const funcName = expr.name.toUpperCase()
55
56
  if (isAggregateFunc(funcName)) {
56
57
  // Evaluate aggregate function on the group
57
- return Boolean(evaluateAggregateFunction(funcName, expr.args, group))
58
+ return Boolean(await evaluateAggregateFunction(funcName, expr.args, group, tables))
58
59
  }
59
60
  }
60
61
 
61
62
  if (expr.type === 'binary') {
62
- const left = evaluateHavingValue(expr.left, context, group)
63
- const right = evaluateHavingValue(expr.right, context, group)
63
+ const left = await evaluateHavingValue(expr.left, context, group, tables)
64
+ const right = await evaluateHavingValue(expr.right, context, group, tables)
64
65
 
65
66
  if (expr.op === 'AND') {
66
67
  return Boolean(left && right)
@@ -96,20 +97,20 @@ export function evaluateHavingExpr(expr, row, group) {
96
97
 
97
98
  if (expr.type === 'unary') {
98
99
  if (expr.op === 'NOT') {
99
- return !evaluateHavingExpr(expr.argument, context, group)
100
+ return !await evaluateHavingExpr(expr.argument, context, group, tables)
100
101
  }
101
102
  if (expr.op === 'IS NULL') {
102
- return evaluateHavingValue(expr.argument, context, group) == null
103
+ return await evaluateHavingValue(expr.argument, context, group, tables) == null
103
104
  }
104
105
  if (expr.op === 'IS NOT NULL') {
105
- return evaluateHavingValue(expr.argument, context, group) != null
106
+ return await evaluateHavingValue(expr.argument, context, group, tables) != null
106
107
  }
107
108
  }
108
109
 
109
110
  if (expr.type === 'between' || expr.type === 'not between') {
110
- const exprVal = evaluateHavingValue(expr.expr, context, group)
111
- const lower = evaluateHavingValue(expr.lower, context, group)
112
- const upper = evaluateHavingValue(expr.upper, context, group)
111
+ const exprVal = await evaluateHavingValue(expr.expr, context, group, tables)
112
+ const lower = await evaluateHavingValue(expr.lower, context, group, tables)
113
+ const upper = await evaluateHavingValue(expr.upper, context, group, tables)
113
114
 
114
115
  // If any value is NULL, return false (SQL behavior)
115
116
  if (exprVal == null || lower == null || upper == null) {
@@ -121,31 +122,32 @@ export function evaluateHavingExpr(expr, row, group) {
121
122
  }
122
123
 
123
124
  // For other expression types, use the context row
124
- return Boolean(evaluateExpr(expr, context))
125
+ return Boolean(await evaluateExpr({ node: expr, row: context, tables }))
125
126
  }
126
127
 
127
128
  /**
128
129
  * Evaluates a value in a HAVING expression
129
130
  *
130
131
  * @param {ExprNode} expr
131
- * @param {RowSource} context - the context row
132
- * @param {RowSource[]} group - the group of rows
133
- * @returns {SqlPrimitive} the evaluated value
132
+ * @param {AsyncRow} context - the context row
133
+ * @param {AsyncRow[]} group - the group of rows
134
+ * @param {Record<string, AsyncDataSource>} tables
135
+ * @returns {Promise<SqlPrimitive>} the evaluated value
134
136
  */
135
- function evaluateHavingValue(expr, context, group) {
137
+ function evaluateHavingValue(expr, context, group, tables) {
136
138
  if (expr.type === 'function') {
137
139
  const funcName = expr.name.toUpperCase()
138
140
  if (isAggregateFunc(funcName)) {
139
- return evaluateAggregateFunction(funcName, expr.args, group)
141
+ return evaluateAggregateFunction(funcName, expr.args, group, tables)
140
142
  }
141
143
  }
142
144
 
143
145
  // For binary expressions, we need to use evaluateHavingExpr to properly handle aggregates
144
146
  if (expr.type === 'binary' || expr.type === 'unary' || expr.type === 'between' || expr.type === 'not between') {
145
- return evaluateHavingExpr(expr, context, group)
147
+ return evaluateHavingExpr(expr, context, group, tables)
146
148
  }
147
149
 
148
- return evaluateExpr(expr, context)
150
+ return evaluateExpr({ node: expr, row: context, tables })
149
151
  }
150
152
 
151
153
  /**
@@ -153,10 +155,11 @@ function evaluateHavingValue(expr, context, group) {
153
155
  *
154
156
  * @param {AggregateFunc} funcName - aggregate function name
155
157
  * @param {ExprNode[]} args - function arguments
156
- * @param {RowSource[]} group - the group of rows
157
- * @returns {SqlPrimitive} the aggregate result
158
+ * @param {AsyncRow[]} group - the group of rows
159
+ * @param {Record<string, AsyncDataSource>} tables
160
+ * @returns {Promise<SqlPrimitive>} the aggregate result
158
161
  */
159
- function evaluateAggregateFunction(funcName, args, group) {
162
+ async function evaluateAggregateFunction(funcName, args, group, tables) {
160
163
  if (funcName === 'COUNT') {
161
164
  if (args.length === 1 && args[0].type === 'identifier' && args[0].name === '*') {
162
165
  return group.length
@@ -164,7 +167,7 @@ function evaluateAggregateFunction(funcName, args, group) {
164
167
  // COUNT(column) - count non-null values
165
168
  let count = 0
166
169
  for (const row of group) {
167
- const val = evaluateExpr(args[0], row)
170
+ const val = await evaluateExpr({ node: args[0], row, tables })
168
171
  if (val != null) count++
169
172
  }
170
173
  return count
@@ -173,7 +176,7 @@ function evaluateAggregateFunction(funcName, args, group) {
173
176
  if (funcName === 'SUM') {
174
177
  let sum = 0
175
178
  for (const row of group) {
176
- const val = evaluateExpr(args[0], row)
179
+ const val = await evaluateExpr({ node: args[0], row, tables })
177
180
  if (val != null) sum += Number(val)
178
181
  }
179
182
  return sum
@@ -183,7 +186,7 @@ function evaluateAggregateFunction(funcName, args, group) {
183
186
  let sum = 0
184
187
  let count = 0
185
188
  for (const row of group) {
186
- const val = evaluateExpr(args[0], row)
189
+ const val = await evaluateExpr({ node: args[0], row, tables })
187
190
  if (val != null) {
188
191
  sum += Number(val)
189
192
  count++
@@ -195,7 +198,7 @@ function evaluateAggregateFunction(funcName, args, group) {
195
198
  if (funcName === 'MIN') {
196
199
  let min = null
197
200
  for (const row of group) {
198
- const val = evaluateExpr(args[0], row)
201
+ const val = await evaluateExpr({ node: args[0], row, tables })
199
202
  if (val != null && (min == null || val < min)) {
200
203
  min = val
201
204
  }
@@ -206,7 +209,7 @@ function evaluateAggregateFunction(funcName, args, group) {
206
209
  if (funcName === 'MAX') {
207
210
  let max = null
208
211
  for (const row of group) {
209
- const val = evaluateExpr(args[0], row)
212
+ const val = await evaluateExpr({ node: args[0], row, tables })
210
213
  if (val != null && (max == null || val > max)) {
211
214
  max = val
212
215
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Collects all results from an async generator into an array
3
+ *
4
+ * @template T
5
+ * @param {AsyncGenerator<T>} asyncGen - the async generator
6
+ * @returns {Promise<T[]>} array of all yielded values
7
+ */
8
+ export async function collect(asyncGen) {
9
+ const results = []
10
+ for await (const item of asyncGen) {
11
+ results.push(item)
12
+ }
13
+ return results
14
+ }
package/src/index.d.ts CHANGED
@@ -4,11 +4,11 @@ import type { ExecuteSqlOptions, SelectStatement } from './types.js'
4
4
  * Executes a SQL SELECT query against an array of data rows
5
5
  *
6
6
  * @param options
7
- * @param options.source - source data as a list of objects or a DataSource
8
- * @param options.sql - SQL query string
9
- * @returns rows matching the query
7
+ * @param options.tables - source data as a list of objects or an AsyncDataSource
8
+ * @param options.query - SQL query string
9
+ * @returns async generator yielding rows matching the query
10
10
  */
11
- export function executeSql(options: ExecuteSqlOptions): Record<string, any>[]
11
+ export function executeSql(options: ExecuteSqlOptions): AsyncGenerator<Record<string, any>>
12
12
 
13
13
  /**
14
14
  * Parses a SQL query string into an abstract syntax tree
@@ -17,3 +17,11 @@ export function executeSql(options: ExecuteSqlOptions): Record<string, any>[]
17
17
  * @returns parsed SQL select statement
18
18
  */
19
19
  export function parseSql(query: string): SelectStatement
20
+
21
+ /**
22
+ * Collects all results from an async generator into an array
23
+ *
24
+ * @param asyncGen - the async generator
25
+ * @returns array of all yielded values
26
+ */
27
+ export function collect<T>(asyncGen: AsyncGenerator<T>): Promise<T[]>
package/src/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { executeSql } from './execute/execute.js'
2
2
  export { parseSql } from './parse/parse.js'
3
+ export { collect } from './execute/utils.js'
@@ -20,6 +20,17 @@ function parsePrimary(c) {
20
20
  const tok = c.current()
21
21
 
22
22
  if (tok.type === 'paren' && tok.value === '(') {
23
+ // Peek ahead to see if this is a scalar subquery
24
+ const nextTok = c.peek(1)
25
+ if (nextTok.type === 'keyword' && nextTok.value === 'SELECT') {
26
+ // It's a scalar subquery
27
+ const subquery = c.parseSubquery()
28
+ return {
29
+ type: 'subquery',
30
+ subquery,
31
+ }
32
+ }
33
+ // Regular grouped expression
23
34
  c.consume()
24
35
  const expr = parseExpression(c)
25
36
  c.expect('paren', ')')
@@ -132,9 +143,6 @@ function parsePrimary(c) {
132
143
  }
133
144
  if (tok.value === 'EXISTS') {
134
145
  c.consume() // EXISTS
135
- if (!c.parseSubquery) {
136
- throw new Error('Subquery parsing not available in this context')
137
- }
138
146
  const subquery = c.parseSubquery()
139
147
  return {
140
148
  type: 'exists',
package/src/types.d.ts CHANGED
@@ -1,26 +1,25 @@
1
- export interface RowSource {
1
+
2
+ /**
3
+ * Async data source for streaming SQL execution.
4
+ * Provides an async iterator over rows.
5
+ */
6
+ export interface AsyncDataSource {
7
+ getRows(): AsyncIterable<AsyncRow>
8
+ }
9
+ export interface AsyncRow {
2
10
  getCell(name: string): any
3
11
  getKeys(): string[]
4
12
  }
5
13
 
6
- export interface DataSource {
7
- getNumRows(): number
8
- getRow(index: number): RowSource
9
- }
14
+ export type RawData = Record<string, any>[]
10
15
 
11
16
  export interface ExecuteSqlOptions {
12
- source: Record<string, any>[] | DataSource
17
+ tables: Record<string, RawData | AsyncDataSource>
13
18
  query: string
14
19
  }
15
20
 
16
21
  export type SqlPrimitive = string | number | bigint | boolean | null
17
22
 
18
- export interface FromSubquery {
19
- kind: 'subquery'
20
- query: SelectStatement
21
- alias: string
22
- }
23
-
24
23
  export interface SelectStatement {
25
24
  distinct: boolean
26
25
  columns: SelectColumn[]
@@ -34,6 +33,12 @@ export interface SelectStatement {
34
33
  offset?: number
35
34
  }
36
35
 
36
+ export interface FromSubquery {
37
+ kind: 'subquery'
38
+ query: SelectStatement
39
+ alias: string
40
+ }
41
+
37
42
  export type BinaryOp =
38
43
  | 'AND'
39
44
  | 'OR'
@@ -117,6 +122,11 @@ export interface CaseNode {
117
122
  elseResult?: ExprNode
118
123
  }
119
124
 
125
+ export interface SubqueryNode {
126
+ type: 'subquery'
127
+ subquery: SelectStatement
128
+ }
129
+
120
130
  export type ExprNode =
121
131
  | LiteralNode
122
132
  | IdentifierNode
@@ -129,6 +139,7 @@ export type ExprNode =
129
139
  | InValuesNode
130
140
  | ExistsNode
131
141
  | CaseNode
142
+ | SubqueryNode
132
143
 
133
144
  export interface StarColumn {
134
145
  kind: 'star'
@@ -138,7 +149,7 @@ export interface StarColumn {
138
149
 
139
150
  export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
140
151
 
141
- export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM'
152
+ export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM' | 'REPLACE'
142
153
 
143
154
  export interface AggregateArgStar {
144
155
  kind: 'star'
@@ -192,7 +203,7 @@ export interface ExprCursor {
192
203
  match(type: TokenType, value?: string): boolean
193
204
  expect(type: TokenType, value: string): Token
194
205
  expectIdentifier(): Token
195
- parseSubquery?: () => SelectStatement
206
+ parseSubquery: () => SelectStatement
196
207
  }
197
208
 
198
209
  // Tokenizer types
package/src/validation.js CHANGED
@@ -13,5 +13,5 @@ export function isAggregateFunc(name) {
13
13
  * @returns {name is StringFunc}
14
14
  */
15
15
  export function isStringFunc(name) {
16
- return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM'].includes(name)
16
+ return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE'].includes(name)
17
17
  }
@@ -1,37 +0,0 @@
1
- /**
2
- * @import { DataSource, RowSource } from '../types.js'
3
- */
4
-
5
- /**
6
- * Creates a row accessor that wraps a plain JavaScript object
7
- *
8
- * @param {Record<string, any>} obj - the plain object
9
- * @returns {RowSource} a row accessor interface
10
- */
11
- export function createRowAccessor(obj) {
12
- return {
13
- getCell(name) {
14
- return obj[name]
15
- },
16
- getKeys() {
17
- return Object.keys(obj)
18
- },
19
- }
20
- }
21
-
22
- /**
23
- * Creates a memory-backed data source from an array of plain objects
24
- *
25
- * @param {Record<string, any>[]} data - array of plain objects
26
- * @returns {DataSource} a data source interface
27
- */
28
- export function createMemorySource(data) {
29
- return {
30
- getNumRows() {
31
- return data.length
32
- },
33
- getRow(index) {
34
- return createRowAccessor(data[index])
35
- },
36
- }
37
- }