squirreling 0.5.0 → 0.6.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
@@ -40,7 +40,10 @@ const users = [
40
40
  // ...more rows
41
41
  ]
42
42
 
43
- type AsyncRow = Record<string, AsyncCell>
43
+ interface AsyncRow {
44
+ columns: string[]
45
+ cells: Record<string, AsyncCell>
46
+ }
44
47
  type AsyncCell = () => Promise<SqlPrimitive>
45
48
 
46
49
  // Returns an async iterable of rows with async cells
@@ -76,6 +79,7 @@ console.log(allUsers)
76
79
  - `GROUP BY` and `HAVING` clauses
77
80
  - Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
78
81
  - String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
82
+ - Math functions: `ABS`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`, `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
79
83
  - Date functions: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
80
84
  - Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
81
85
  - Basic expressions and arithmetic operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -37,11 +37,11 @@
37
37
  "test": "vitest run"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/node": "24.10.2",
41
- "@vitest/coverage-v8": "4.0.15",
42
- "eslint": "9.39.1",
40
+ "@types/node": "24.10.4",
41
+ "@vitest/coverage-v8": "4.0.16",
42
+ "eslint": "9.39.2",
43
43
  "eslint-plugin-jsdoc": "61.5.0",
44
44
  "typescript": "5.9.3",
45
- "vitest": "4.0.15"
45
+ "vitest": "4.0.16"
46
46
  }
47
47
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
- * @import { AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
2
+ * @import { AsyncCell, AsyncCells, AsyncDataSource, AsyncRow, ScanOptions, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
-
6
5
  /**
7
6
  * Wraps an async generator of plain objects into an AsyncDataSource
8
7
  *
@@ -11,8 +10,11 @@
11
10
  */
12
11
  export function generatorSource(gen) {
13
12
  return {
14
- async *getRows() {
15
- yield* gen
13
+ async *scan({ signal }) {
14
+ for await (const row of gen) {
15
+ if (signal?.aborted) break
16
+ yield row
17
+ }
16
18
  },
17
19
  }
18
20
  }
@@ -24,12 +26,12 @@ export function generatorSource(gen) {
24
26
  * @returns {AsyncRow} a row accessor interface
25
27
  */
26
28
  function asyncRow(obj) {
27
- /** @type {AsyncRow} */
28
- const row = {}
29
+ /** @type {AsyncCells} */
30
+ const cells = {}
29
31
  for (const [key, value] of Object.entries(obj)) {
30
- row[key] = () => Promise.resolve(value)
32
+ cells[key] = () => Promise.resolve(value)
31
33
  }
32
- return row
34
+ return { columns: Object.keys(obj), cells }
33
35
  }
34
36
 
35
37
  /**
@@ -40,8 +42,9 @@ function asyncRow(obj) {
40
42
  */
41
43
  export function memorySource(data) {
42
44
  return {
43
- async *getRows() {
45
+ async *scan({ signal }) {
44
46
  for (const item of data) {
47
+ if (signal?.aborted) break
45
48
  yield asyncRow(item)
46
49
  }
47
50
  },
@@ -58,17 +61,21 @@ export function cachedDataSource(source) {
58
61
  const cache = new Map()
59
62
  return {
60
63
  /**
64
+ * @param {ScanOptions} options
61
65
  * @yields {AsyncRow}
62
66
  */
63
- async *getRows() {
67
+ async *scan(options) {
68
+ const { signal } = options
64
69
  let index = 0
65
- for await (const row of source.getRows()) {
70
+ for await (const row of source.scan(options)) {
71
+ if (signal?.aborted) break
66
72
  const rowIndex = index
67
- /** @type {AsyncRow} */
68
- const out = {}
69
- for (const [key, cell] of Object.entries(row)) {
73
+ /** @type {AsyncCells} */
74
+ const cells = {}
75
+ for (const key of row.columns) {
76
+ const cell = row.cells[key]
70
77
  // Wrap the cell to cache accesses
71
- out[key] = () => {
78
+ cells[key] = () => {
72
79
  const cacheKey = `${rowIndex}:${key}`
73
80
  let value = cache.get(cacheKey)
74
81
  if (!value) {
@@ -78,7 +85,7 @@ export function cachedDataSource(source) {
78
85
  return value
79
86
  }
80
87
  }
81
- yield out
88
+ yield { columns: row.columns, cells }
82
89
  index++
83
90
  }
84
91
  },
@@ -1,7 +1,46 @@
1
+ import { isAggregateFunc } from '../validation.js'
2
+
1
3
  /**
2
4
  * @import { ExprNode, SelectStatement, SelectColumn } from '../types.js'
3
5
  */
4
6
 
7
+ /**
8
+ * Checks if an expression contains any aggregate function calls
9
+ *
10
+ * @param {ExprNode | undefined} expr
11
+ * @returns {boolean}
12
+ */
13
+ export function containsAggregate(expr) {
14
+ if (!expr) return false
15
+ if (expr.type === 'function' && isAggregateFunc(expr.name.toUpperCase())) {
16
+ return true
17
+ }
18
+ if (expr.type === 'binary') {
19
+ return containsAggregate(expr.left) || containsAggregate(expr.right)
20
+ }
21
+ if (expr.type === 'unary') {
22
+ return containsAggregate(expr.argument)
23
+ }
24
+ if (expr.type === 'cast') {
25
+ return containsAggregate(expr.expr)
26
+ }
27
+ if (expr.type === 'case') {
28
+ if (expr.caseExpr && containsAggregate(expr.caseExpr)) return true
29
+ for (const when of expr.whenClauses) {
30
+ if (containsAggregate(when.condition) || containsAggregate(when.result)) return true
31
+ }
32
+ if (containsAggregate(expr.elseResult)) return true
33
+ }
34
+ if (expr.type === 'in valuelist') {
35
+ if (containsAggregate(expr.expr)) return true
36
+ for (const val of expr.values) {
37
+ if (containsAggregate(val)) return true
38
+ }
39
+ }
40
+ // Note: Don't recurse into subqueries - they have their own aggregate scope
41
+ return false
42
+ }
43
+
5
44
  /**
6
45
  * Extracts column names needed from a SELECT statement.
7
46
  *
@@ -50,11 +89,6 @@ export function extractColumns(select) {
50
89
  function collectColumnsFromSelectColumn(col, columns) {
51
90
  if (col.kind === 'derived') {
52
91
  collectColumnsFromExpr(col.expr, columns)
53
- } else if (col.kind === 'aggregate') {
54
- if (col.arg.kind === 'expression') {
55
- collectColumnsFromExpr(col.arg.expr, columns)
56
- }
57
- // 'star' aggregate (COUNT(*)) doesn't reference specific columns
58
92
  }
59
93
  // 'star' columns handled separately (returns undefined for all columns)
60
94
  }
@@ -2,15 +2,14 @@ import { missingClauseError } from '../parseErrors.js'
2
2
  import { tableNotFoundError, unsupportedOperationError } from '../executionErrors.js'
3
3
  import { generatorSource, memorySource } from '../backend/dataSource.js'
4
4
  import { parseSql } from '../parse/parse.js'
5
- import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
6
- import { extractColumns } from './columns.js'
5
+ import { containsAggregate, extractColumns } from './columns.js'
7
6
  import { evaluateExpr } from './expression.js'
8
7
  import { evaluateHavingExpr } from './having.js'
9
8
  import { executeJoins } from './join.js'
10
9
  import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
11
10
 
12
11
  /**
13
- * @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
12
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
14
13
  */
15
14
 
16
15
  /**
@@ -19,8 +18,8 @@ import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
19
18
  * @param {ExecuteSqlOptions} options - the execution options
20
19
  * @yields {AsyncRow} async generator yielding result rows
21
20
  */
22
- export async function* executeSql({ tables, query }) {
23
- const select = parseSql(query)
21
+ export async function* executeSql({ tables, query, signal }) {
22
+ const select = typeof query === 'string' ? parseSql(query) : query
24
23
 
25
24
  // Check for unsupported operations
26
25
  if (!select.from) {
@@ -41,17 +40,23 @@ export async function* executeSql({ tables, query }) {
41
40
  }
42
41
  }
43
42
 
44
- yield* executeSelect(select, normalizedTables)
43
+ yield* executeSelect({ select, tables: normalizedTables, signal })
45
44
  }
46
45
 
46
+ /**
47
+ * @typedef {Object} ExecuteSelectOptions
48
+ * @property {SelectStatement} select
49
+ * @property {Record<string, AsyncDataSource>} tables
50
+ * @property {AbortSignal} [signal]
51
+ */
52
+
47
53
  /**
48
54
  * Executes a SELECT query against the provided tables
49
55
  *
50
- * @param {SelectStatement} select
51
- * @param {Record<string, AsyncDataSource>} tables
56
+ * @param {ExecuteSelectOptions} options
52
57
  * @yields {AsyncRow}
53
58
  */
54
- export async function* executeSelect(select, tables) {
59
+ export async function* executeSelect({ select, tables, signal }) {
55
60
  /** @type {AsyncDataSource} */
56
61
  let dataSource
57
62
  /** @type {string} */
@@ -67,7 +72,7 @@ export async function* executeSelect(select, tables) {
67
72
  } else {
68
73
  // Nested subquery - recursively resolve
69
74
  fromTableName = select.from.alias
70
- dataSource = generatorSource(executeSelect(select.from.query, tables))
75
+ dataSource = generatorSource(executeSelect({ select: select.from.query, tables, signal }))
71
76
  }
72
77
 
73
78
  // Execute JOINs if present
@@ -75,21 +80,21 @@ export async function* executeSelect(select, tables) {
75
80
  dataSource = await executeJoins(dataSource, select.joins, fromTableName, tables)
76
81
  }
77
82
 
78
- yield* evaluateSelectAst(select, dataSource, tables)
83
+ yield* evaluateSelectAst({ select, dataSource, tables, signal })
79
84
  }
80
85
 
81
86
  /**
82
87
  * Creates a stable string key for a row to enable deduplication
83
88
  *
84
- * @param {AsyncRow} row
89
+ * @param {AsyncCells} cells
85
90
  * @returns {Promise<string>} a stable string representation of the row
86
91
  */
87
- async function stableRowKey(row) {
88
- const keys = Object.keys(row).sort()
92
+ async function stableRowKey(cells) {
93
+ const keys = Object.keys(cells).sort()
89
94
  /** @type {string[]} */
90
95
  const parts = []
91
96
  for (const k of keys) {
92
- const v = await row[k]()
97
+ const v = await cells[k]()
93
98
  parts.push(k + ':' + stringify(v))
94
99
  }
95
100
  return parts.join('|')
@@ -109,7 +114,7 @@ async function applyDistinct(rows, distinct) {
109
114
  /** @type {AsyncRow[]} */
110
115
  const result = []
111
116
  for (const row of rows) {
112
- const key = await stableRowKey(row)
117
+ const key = await stableRowKey(row.cells)
113
118
  if (seen.has(key)) continue
114
119
  seen.add(key)
115
120
  result.push(row)
@@ -201,40 +206,52 @@ async function sortRows(rows, orderBy, tables) {
201
206
  return groups.flat().map(i => rows[i])
202
207
  }
203
208
 
209
+ /**
210
+ * @typedef {Object} EvaluateSelectAstOptions
211
+ * @property {SelectStatement} select
212
+ * @property {AsyncDataSource} dataSource
213
+ * @property {Record<string, AsyncDataSource>} tables
214
+ * @property {AbortSignal} [signal]
215
+ */
216
+
204
217
  /**
205
218
  * Evaluates a select with a resolved FROM data source
206
219
  *
207
- * @param {SelectStatement} select
208
- * @param {AsyncDataSource} dataSource
209
- * @param {Record<string, AsyncDataSource>} tables
220
+ * @param {EvaluateSelectAstOptions} options
210
221
  * @yields {AsyncRow}
211
222
  */
212
- async function* evaluateSelectAst(select, dataSource, tables) {
223
+ async function* evaluateSelectAst({ select, dataSource, tables, signal }) {
213
224
  // SQL priority: from, where, group by, having, select, order by, offset, limit
214
225
 
215
- const hasAggregate = select.columns.some(col => col.kind === 'aggregate')
226
+ const hasAggregate = select.columns.some(col => col.kind === 'derived' && containsAggregate(col.expr))
216
227
  const useGrouping = hasAggregate || select.groupBy.length > 0
217
228
  const needsBuffering = useGrouping || select.orderBy.length > 0
218
229
 
219
230
  if (needsBuffering) {
220
231
  // BUFFERING PATH: Collect all rows, process, then yield
221
- yield* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping)
232
+ yield* evaluateBuffered({ select, dataSource, tables, hasAggregate, useGrouping, signal })
222
233
  } else {
223
234
  // STREAMING PATH: Yield rows one by one
224
- yield* evaluateStreaming(select, dataSource, tables)
235
+ yield* evaluateStreaming({ select, dataSource, tables, signal })
225
236
  }
226
237
  }
227
238
 
239
+ /**
240
+ * @typedef {Object} EvaluateStreamingOptions
241
+ * @property {SelectStatement} select
242
+ * @property {AsyncDataSource} dataSource
243
+ * @property {Record<string, AsyncDataSource>} tables
244
+ * @property {AbortSignal} [signal]
245
+ */
246
+
228
247
  /**
229
248
  * Streaming evaluation for simple queries (no ORDER BY or GROUP BY)
230
249
  * Supports DISTINCT by tracking seen row keys without buffering full rows
231
250
  *
232
- * @param {SelectStatement} select
233
- * @param {AsyncDataSource} dataSource
234
- * @param {Record<string, AsyncDataSource>} tables
251
+ * @param {EvaluateStreamingOptions} options
235
252
  * @yields {AsyncRow}
236
253
  */
237
- async function* evaluateStreaming(select, dataSource, tables) {
254
+ async function* evaluateStreaming({ select, dataSource, tables, signal }) {
238
255
  let rowsYielded = 0
239
256
  let rowsSkipped = 0
240
257
  let rowIndex = 0
@@ -255,7 +272,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
255
272
  offset: select.offset,
256
273
  }
257
274
 
258
- for await (const row of dataSource.getRows(hints)) {
275
+ for await (const row of dataSource.scan({ hints, signal })) {
259
276
  rowIndex++
260
277
  // WHERE filter
261
278
  if (select.where) {
@@ -270,27 +287,27 @@ async function* evaluateStreaming(select, dataSource, tables) {
270
287
  }
271
288
 
272
289
  // SELECT projection
273
- /** @type {AsyncRow} */
274
- const outRow = {}
290
+ /** @type {string[]} */
291
+ const columns = []
292
+ /** @type {AsyncCells} */
293
+ const cells = {}
275
294
  const currentRowIndex = rowIndex
276
295
  for (const col of select.columns) {
277
296
  if (col.kind === 'star') {
278
- for (const [key, cell] of Object.entries(row)) {
279
- outRow[key] = cell
297
+ for (const key of row.columns) {
298
+ columns.push(key)
299
+ cells[key] = row.cells[key]
280
300
  }
281
301
  } else if (col.kind === 'derived') {
282
302
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
283
- outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
284
- } else if (col.kind === 'aggregate') {
285
- throw new Error(
286
- 'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
287
- )
303
+ columns.push(alias)
304
+ cells[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
288
305
  }
289
306
  }
290
307
 
291
308
  // DISTINCT: skip duplicate rows
292
309
  if (seen) {
293
- const key = await stableRowKey(outRow)
310
+ const key = await stableRowKey(cells)
294
311
  if (seen.has(key)) continue
295
312
  seen.add(key)
296
313
  // OFFSET applies to distinct rows
@@ -300,7 +317,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
300
317
  }
301
318
  }
302
319
 
303
- yield outRow
320
+ yield { columns, cells }
304
321
  rowsYielded++
305
322
  if (rowsYielded >= limit) {
306
323
  break
@@ -308,17 +325,23 @@ async function* evaluateStreaming(select, dataSource, tables) {
308
325
  }
309
326
  }
310
327
 
328
+ /**
329
+ * @typedef {Object} EvaluateBufferedOptions
330
+ * @property {SelectStatement} select
331
+ * @property {AsyncDataSource} dataSource
332
+ * @property {Record<string, AsyncDataSource>} tables
333
+ * @property {boolean} hasAggregate
334
+ * @property {boolean} useGrouping
335
+ * @property {AbortSignal} [signal]
336
+ */
337
+
311
338
  /**
312
339
  * Buffered evaluation for complex queries (with ORDER BY or GROUP BY)
313
340
  *
314
- * @param {SelectStatement} select
315
- * @param {AsyncDataSource} dataSource
316
- * @param {Record<string, AsyncDataSource>} tables
317
- * @param {boolean} hasAggregate
318
- * @param {boolean} useGrouping
341
+ * @param {EvaluateBufferedOptions} options
319
342
  * @yields {AsyncRow}
320
343
  */
321
- async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
344
+ async function* evaluateBuffered({ select, dataSource, tables, hasAggregate, useGrouping, signal }) {
322
345
  // Build hints for data source optimization
323
346
  // Note: limit/offset not passed here since buffering needs all rows for sorting/grouping
324
347
  /** @type {QueryHints} */
@@ -330,7 +353,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
330
353
  // Step 1: Collect all rows from data source
331
354
  /** @type {AsyncRow[]} */
332
355
  const working = []
333
- for await (const row of dataSource.getRows(hints)) {
356
+ for await (const row of dataSource.scan({ hints, signal })) {
334
357
  working.push(row)
335
358
  }
336
359
 
@@ -392,14 +415,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
392
415
  }
393
416
 
394
417
  for (const group of groups) {
395
- /** @type {AsyncRow} */
396
- const resultRow = {}
418
+ const columns = []
419
+ /** @type {AsyncCells} */
420
+ const cells = {}
397
421
  for (const col of select.columns) {
398
422
  if (col.kind === 'star') {
399
423
  const firstRow = group[0]
400
424
  if (firstRow) {
401
- for (const [key, cell] of Object.entries(firstRow)) {
402
- resultRow[key] = cell
425
+ for (const key of firstRow.columns) {
426
+ columns.push(key)
427
+ cells[key] = firstRow.cells[key]
403
428
  }
404
429
  }
405
430
  continue
@@ -407,29 +432,23 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
407
432
 
408
433
  if (col.kind === 'derived') {
409
434
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
410
- if (group.length > 0) {
411
- resultRow[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
412
- } else {
413
- delete resultRow[alias]
414
- }
415
- continue
416
- }
417
-
418
- if (col.kind === 'aggregate') {
419
- const alias = col.alias ?? defaultAggregateAlias(col)
420
- resultRow[alias] = () => evaluateAggregate({ col, rows: group, tables })
435
+ columns.push(alias)
436
+ // Pass group to evaluateExpr so it can handle aggregate functions within expressions
437
+ // For empty groups, still provide an empty row context for aggregates to return appropriate values
438
+ cells[alias] = () => evaluateExpr({ node: col.expr, row: group[0] ?? { columns: [], cells: {} }, tables, rows: group })
421
439
  continue
422
440
  }
423
441
  }
442
+ const asyncRow = { columns, cells }
424
443
 
425
444
  // Apply HAVING filter before adding to projected results
426
445
  if (select.having) {
427
- if (!await evaluateHavingExpr(select.having, resultRow, group, tables)) {
446
+ if (!await evaluateHavingExpr(select.having, asyncRow, group, tables)) {
428
447
  continue
429
448
  }
430
449
  }
431
450
 
432
- projected.push(resultRow)
451
+ projected.push(asyncRow)
433
452
  }
434
453
  } else {
435
454
  // No grouping, simple projection
@@ -446,19 +465,22 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
446
465
  }
447
466
 
448
467
  for (const row of rowsToProject) {
449
- /** @type {AsyncRow} */
450
- const outRow = {}
468
+ const columns = []
469
+ /** @type {AsyncCells} */
470
+ const cells = {}
451
471
  for (const col of select.columns) {
452
472
  if (col.kind === 'star') {
453
- for (const [key, cell] of Object.entries(row)) {
454
- outRow[key] = cell
473
+ for (const key of row.columns) {
474
+ columns.push(key)
475
+ cells[key] = row.cells[key]
455
476
  }
456
477
  } else if (col.kind === 'derived') {
457
478
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
458
- outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
479
+ columns.push(alias)
480
+ cells[alias] = () => evaluateExpr({ node: col.expr, row, tables })
459
481
  }
460
482
  }
461
- projected.push(outRow)
483
+ projected.push({ columns, cells })
462
484
  }
463
485
  }
464
486