squirreling 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
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
+ * @returns {AsyncGenerator<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
+ }
@@ -3,19 +3,21 @@ import { evaluateExpr } from './expression.js'
3
3
  /**
4
4
  * Evaluates an aggregate function over a set of rows
5
5
  *
6
- * @import { AggregateColumn, ExprNode, AsyncRow } from '../types.js'
7
- * @param {AggregateColumn} col - aggregate column definition
8
- * @param {AsyncRow[]} rows - rows to aggregate
6
+ * @import { AggregateColumn, AsyncDataSource, ExprNode, AsyncRow } from '../types.js'
7
+ * @param {Object} options
8
+ * @param {AggregateColumn} options.col - aggregate column definition
9
+ * @param {AsyncRow[]} options.rows - rows to aggregate
10
+ * @param {Record<string, AsyncDataSource>} options.tables
9
11
  * @returns {Promise<number | null>} aggregated result
10
12
  */
11
- export async function evaluateAggregate(col, rows) {
13
+ export async function evaluateAggregate({ col, rows, tables }) {
12
14
  const { arg, func } = col
13
15
 
14
16
  if (func === 'COUNT') {
15
17
  if (arg.kind === 'star') return rows.length
16
18
  let count = 0
17
- for (let i = 0; i < rows.length; i += 1) {
18
- const v = await evaluateExpr({ node: arg.expr, row: rows[i] })
19
+ for (const row of rows) {
20
+ const v = await evaluateExpr({ node: arg.expr, row, tables })
19
21
  if (v !== null && v !== undefined) {
20
22
  count += 1
21
23
  }
@@ -34,8 +36,8 @@ export async function evaluateAggregate(col, rows) {
34
36
  /** @type {number | null} */
35
37
  let max = null
36
38
 
37
- for (let i = 0; i < rows.length; i += 1) {
38
- const raw = await evaluateExpr({ node: arg.expr, row: rows[i] })
39
+ for (const row of rows) {
40
+ const raw = await evaluateExpr({ node: arg.expr, row, tables })
39
41
  if (raw == null) continue
40
42
  const num = Number(raw)
41
43
  if (!Number.isFinite(num)) continue
@@ -80,20 +82,17 @@ export function defaultAggregateAlias(col) {
80
82
  export function defaultAggregateAliasExpr(expr) {
81
83
  if (expr.type === 'identifier') {
82
84
  return expr.name
83
- }
84
- if (expr.type === 'literal') {
85
+ } else if (expr.type === 'literal') {
85
86
  return String(expr.value)
86
- }
87
- if (expr.type === 'cast') {
87
+ } else if (expr.type === 'cast') {
88
88
  return defaultAggregateAliasExpr(expr.expr) + '_as_' + expr.toType
89
- }
90
- if (expr.type === 'unary') {
89
+ } else if (expr.type === 'unary') {
91
90
  return expr.op + '_' + defaultAggregateAliasExpr(expr.argument)
92
- }
93
- if (expr.type === 'binary') {
91
+ } else if (expr.type === 'binary') {
94
92
  return defaultAggregateAliasExpr(expr.left) + '_' + expr.op + '_' + defaultAggregateAliasExpr(expr.right)
95
- }
96
- if (expr.type === 'function') {
93
+ } else if (expr.type === 'function') {
97
94
  return expr.name.toLowerCase() + '_' + expr.args.map(defaultAggregateAliasExpr).join('_')
95
+ } else {
96
+ return 'expr'
98
97
  }
99
98
  }
@@ -1,6 +1,6 @@
1
1
  import { evaluateExpr } from './expression.js'
2
2
  import { parseSql } from '../parse/parse.js'
3
- import { asyncRow, generatorSource, memorySource } from '../backend/dataSource.js'
3
+ import { generatorSource, memorySource } from '../backend/dataSource.js'
4
4
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
5
5
  import { evaluateHavingExpr } from './having.js'
6
6
 
@@ -12,7 +12,7 @@ import { evaluateHavingExpr } from './having.js'
12
12
  * Executes a SQL SELECT query against named data sources
13
13
  *
14
14
  * @param {ExecuteSqlOptions} options - the execution options
15
- * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
15
+ * @returns {AsyncGenerator<AsyncRow>} async generator yielding result rows
16
16
  */
17
17
  export async function* executeSql({ tables, query }) {
18
18
  const select = parseSql(query)
@@ -44,7 +44,7 @@ export async function* executeSql({ tables, query }) {
44
44
  *
45
45
  * @param {SelectStatement} select
46
46
  * @param {Record<string, AsyncDataSource>} tables
47
- * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
47
+ * @returns {AsyncGenerator<AsyncRow>}
48
48
  */
49
49
  export async function* executeSelect(select, tables) {
50
50
  /** @type {AsyncDataSource} */
@@ -96,15 +96,15 @@ function defaultDerivedAlias(expr) {
96
96
  /**
97
97
  * Creates a stable string key for a row to enable deduplication
98
98
  *
99
- * @param {Record<string, any>} row
100
- * @returns {string} a stable string representation of the row
99
+ * @param {AsyncRow} row
100
+ * @returns {Promise<string>} a stable string representation of the row
101
101
  */
102
- function stableRowKey(row) {
102
+ async function stableRowKey(row) {
103
103
  const keys = Object.keys(row).sort()
104
104
  /** @type {string[]} */
105
105
  const parts = []
106
106
  for (const k of keys) {
107
- const v = row[k]
107
+ const v = await row[k]()
108
108
  parts.push(k + ':' + JSON.stringify(v))
109
109
  }
110
110
  return parts.join('|')
@@ -138,18 +138,18 @@ function compareValues(a, b) {
138
138
  /**
139
139
  * Applies DISTINCT filtering to remove duplicate rows
140
140
  *
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
141
+ * @param {AsyncRow[]} rows - the input rows
142
+ * @param {boolean} distinct - whether to apply deduplication
143
+ * @returns {Promise<AsyncRow[]>} the deduplicated rows
144
144
  */
145
- function applyDistinct(rows, distinct) {
145
+ async function applyDistinct(rows, distinct) {
146
146
  if (!distinct) return rows
147
147
  /** @type {Set<string>} */
148
148
  const seen = new Set()
149
- /** @type {Record<string, any>[]} */
149
+ /** @type {AsyncRow[]} */
150
150
  const result = []
151
151
  for (const row of rows) {
152
- const key = stableRowKey(row)
152
+ const key = await stableRowKey(row)
153
153
  if (seen.has(key)) continue
154
154
  seen.add(key)
155
155
  result.push(row)
@@ -221,10 +221,10 @@ async function sortRowSources(rows, orderBy, tables) {
221
221
  /**
222
222
  * Applies ORDER BY sorting to rows
223
223
  *
224
- * @param {Record<string, any>[]} rows - the input rows
224
+ * @param {AsyncRow[]} rows - the input rows
225
225
  * @param {OrderByItem[]} orderBy - the sort specifications
226
226
  * @param {Record<string, AsyncDataSource>} tables
227
- * @returns {Promise<Record<string, any>[]>} the sorted rows
227
+ * @returns {Promise<AsyncRow[]>} the sorted rows
228
228
  */
229
229
  async function applyOrderBy(rows, orderBy, tables) {
230
230
  if (!orderBy.length) return rows
@@ -236,7 +236,7 @@ async function applyOrderBy(rows, orderBy, tables) {
236
236
  /** @type {SqlPrimitive[]} */
237
237
  const rowValues = []
238
238
  for (const term of orderBy) {
239
- const value = await evaluateExpr({ node: term.expr, row: asyncRow(row), tables })
239
+ const value = await evaluateExpr({ node: term.expr, row, tables })
240
240
  rowValues.push(value)
241
241
  }
242
242
  evaluatedValues.push(rowValues)
@@ -282,10 +282,10 @@ async function applyOrderBy(rows, orderBy, tables) {
282
282
  /**
283
283
  * Evaluates a select with a resolved FROM data source
284
284
  *
285
- * @param {SelectStatement} select - the parsed SQL AST
286
- * @param {AsyncDataSource} dataSource - the async data source
285
+ * @param {SelectStatement} select
286
+ * @param {AsyncDataSource} dataSource
287
287
  * @param {Record<string, AsyncDataSource>} tables
288
- * @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
288
+ * @returns {AsyncGenerator<AsyncRow>}
289
289
  */
290
290
  async function* evaluateSelectAst(select, dataSource, tables) {
291
291
  // SQL priority: from, where, group by, having, select, order by, offset, limit
@@ -310,7 +310,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
310
310
  * @param {SelectStatement} select
311
311
  * @param {AsyncDataSource} dataSource
312
312
  * @param {Record<string, AsyncDataSource>} tables
313
- * @returns {AsyncGenerator<Record<string, any>>}
313
+ * @returns {AsyncGenerator<AsyncRow>}
314
314
  */
315
315
  async function* evaluateStreaming(select, dataSource, tables) {
316
316
  let rowsYielded = 0
@@ -337,17 +337,16 @@ async function* evaluateStreaming(select, dataSource, tables) {
337
337
  }
338
338
 
339
339
  // SELECT projection
340
- /** @type {Record<string, any>} */
340
+ /** @type {AsyncRow} */
341
341
  const outRow = {}
342
342
  for (const col of select.columns) {
343
343
  if (col.kind === 'star') {
344
- const keys = row.getKeys()
345
- for (const key of keys) {
346
- outRow[key] = row.getCell(key)
344
+ for (const [key, cell] of Object.entries(row)) {
345
+ outRow[key] = cell
347
346
  }
348
347
  } else if (col.kind === 'derived') {
349
348
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
350
- outRow[alias] = await evaluateExpr({ node: col.expr, row, tables })
349
+ outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
351
350
  } else if (col.kind === 'aggregate') {
352
351
  throw new Error(
353
352
  'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
@@ -357,7 +356,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
357
356
 
358
357
  // DISTINCT: skip duplicate rows
359
358
  if (seen) {
360
- const key = stableRowKey(outRow)
359
+ const key = await stableRowKey(outRow)
361
360
  if (seen.has(key)) continue
362
361
  seen.add(key)
363
362
  // OFFSET applies to distinct rows
@@ -383,7 +382,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
383
382
  * @param {Record<string, AsyncDataSource>} tables
384
383
  * @param {boolean} hasAggregate
385
384
  * @param {boolean} useGrouping
386
- * @returns {AsyncGenerator<Record<string, any>>}
385
+ * @returns {AsyncGenerator<AsyncRow>}
387
386
  */
388
387
  async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
389
388
  // Step 1: Collect all rows from data source
@@ -409,7 +408,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
409
408
  }
410
409
 
411
410
  // Step 3: Projection (grouping vs non-grouping)
412
- /** @type {Record<string, any>[]} */
411
+ /** @type {AsyncRow[]} */
413
412
  let projected = []
414
413
 
415
414
  if (useGrouping) {
@@ -446,15 +445,14 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
446
445
  }
447
446
 
448
447
  for (const group of groups) {
449
- /** @type {Record<string, any>} */
448
+ /** @type {AsyncRow} */
450
449
  const resultRow = {}
451
450
  for (const col of select.columns) {
452
451
  if (col.kind === 'star') {
453
452
  const firstRow = group[0]
454
453
  if (firstRow) {
455
- const keys = firstRow.getKeys()
456
- for (const key of keys) {
457
- resultRow[key] = firstRow.getCell(key)
454
+ for (const [key, cell] of Object.entries(firstRow)) {
455
+ resultRow[key] = cell
458
456
  }
459
457
  }
460
458
  continue
@@ -463,18 +461,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
463
461
  if (col.kind === 'derived') {
464
462
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
465
463
  if (group.length > 0) {
466
- const value = await evaluateExpr({ node: col.expr, row: group[0], tables })
467
- resultRow[alias] = value
464
+ resultRow[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
468
465
  } else {
469
- resultRow[alias] = undefined
466
+ delete resultRow[alias]
470
467
  }
471
468
  continue
472
469
  }
473
470
 
474
471
  if (col.kind === 'aggregate') {
475
472
  const alias = col.alias ?? defaultAggregateAlias(col)
476
- const value = await evaluateAggregate(col, group)
477
- resultRow[alias] = value
473
+ resultRow[alias] = () => evaluateAggregate({ col, rows: group, tables })
478
474
  continue
479
475
  }
480
476
  }
@@ -503,18 +499,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
503
499
  }
504
500
 
505
501
  for (const row of rowsToProject) {
506
- /** @type {Record<string, any>} */
502
+ /** @type {AsyncRow} */
507
503
  const outRow = {}
508
504
  for (const col of select.columns) {
509
505
  if (col.kind === 'star') {
510
- const keys = row.getKeys()
511
- for (const key of keys) {
512
- outRow[key] = row.getCell(key)
506
+ for (const [key, cell] of Object.entries(row)) {
507
+ outRow[key] = cell
513
508
  }
514
509
  } else if (col.kind === 'derived') {
515
510
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
516
- const value = await evaluateExpr({ node: col.expr, row, tables })
517
- outRow[alias] = value
511
+ outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
518
512
  }
519
513
  }
520
514
  projected.push(outRow)
@@ -522,7 +516,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
522
516
  }
523
517
 
524
518
  // Step 4: DISTINCT
525
- projected = applyDistinct(projected, select.distinct)
519
+ projected = await applyDistinct(projected, select.distinct)
526
520
 
527
521
  // Step 5: ORDER BY (final sort for grouped queries)
528
522
  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,19 @@ export async function evaluateExpr({ node, row, tables }) {
20
19
  }
21
20
 
22
21
  if (node.type === 'identifier') {
23
- return row.getCell(node.name)
22
+ return row[node.name]?.()
24
23
  }
25
24
 
26
25
  // Scalar subquery - returns a single value
27
26
  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]
27
+ const gen = executeSelect(node.subquery, tables)
28
+ const first = await gen.next() // Start the generator
29
+ gen.return() // Stop further execution
30
+ if (first.done) return null
31
+ /** @type {AsyncRow} */
32
+ const firstRow = first.value
32
33
  const firstKey = Object.keys(firstRow)[0]
33
- return firstRow[firstKey]
34
+ return firstRow[firstKey]()
34
35
  }
35
36
 
36
37
  // Unary operators
@@ -238,29 +239,37 @@ export async function evaluateExpr({ node, row, tables }) {
238
239
  // IN and NOT IN with subqueries
239
240
  if (node.type === 'in') {
240
241
  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])
242
+ const results = executeSelect(node.subquery, tables)
243
+ /** @type {SqlPrimitive[]} */
244
+ const values = []
245
+ for await (const resRow of results) {
246
+ const firstKey = Object.keys(resRow)[0]
247
+ const val = await resRow[firstKey]()
248
+ values.push(val)
249
+ }
245
250
  return values.includes(exprVal)
246
251
  }
247
252
  if (node.type === 'not in') {
248
253
  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])
254
+ const results = executeSelect(node.subquery, tables)
255
+ /** @type {SqlPrimitive[]} */
256
+ const values = []
257
+ for await (const resRow of results) {
258
+ const firstKey = Object.keys(resRow)[0]
259
+ const val = await resRow[firstKey]()
260
+ values.push(val)
261
+ }
253
262
  return !values.includes(exprVal)
254
263
  }
255
264
 
256
265
  // EXISTS and NOT EXISTS with subqueries
257
266
  if (node.type === 'exists') {
258
- const results = await collect(executeSelect(node.subquery, tables))
259
- return results.length > 0
267
+ const results = await executeSelect(node.subquery, tables).next()
268
+ return results.done === false
260
269
  }
261
270
  if (node.type === 'not exists') {
262
- const results = await collect(executeSelect(node.subquery, tables))
263
- return results.length === 0
271
+ const results = await executeSelect(node.subquery, tables).next()
272
+ return results.done === true
264
273
  }
265
274
 
266
275
  // CASE expressions
@@ -8,32 +8,17 @@ import { evaluateExpr } from './expression.js'
8
8
  /**
9
9
  * Creates a context for evaluating HAVING expressions
10
10
  *
11
- * @param {Record<string, any>} resultRow - the aggregated result row
11
+ * @param {AsyncRow} resultRow - the aggregated result row
12
12
  * @param {AsyncRow[]} group - the group of rows
13
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)
17
17
  const firstRow = group[0]
18
- /** @type {Record<string, any>} */
19
- const context = {}
20
18
  if (firstRow) {
21
- const keys = firstRow.getKeys()
22
- for (const key of keys) {
23
- context[key] = firstRow.getCell(key)
24
- }
25
- }
26
- // Merge with result row (which has aggregates computed)
27
- Object.assign(context, resultRow)
28
-
29
- // Return a Row accessor wrapping the context
30
- return {
31
- getCell(name) {
32
- return context[name]
33
- },
34
- getKeys() {
35
- return Object.keys(context)
36
- },
19
+ return { ...firstRow, ...resultRow }
20
+ } else {
21
+ return resultRow
37
22
  }
38
23
  }
39
24
 
@@ -41,7 +26,7 @@ function createHavingContext(resultRow, group) {
41
26
  * Evaluates a HAVING expression with support for aggregate functions
42
27
  *
43
28
  * @param {ExprNode} expr - the HAVING expression
44
- * @param {Record<string, any>} row - the aggregated result row
29
+ * @param {AsyncRow} row - the aggregated result row
45
30
  * @param {AsyncRow[]} group - the group of rows for re-evaluating aggregates
46
31
  * @param {Record<string, AsyncDataSource>} tables
47
32
  * @returns {Promise<boolean>} whether the HAVING condition is satisfied
@@ -1,13 +1,19 @@
1
1
  /**
2
- * Collects all results from an async generator into an array
2
+ * Collects and materialize all results from an async row generator into an array
3
3
  *
4
- * @template T
5
- * @param {AsyncGenerator<T>} asyncGen - the async generator
6
- * @returns {Promise<T[]>} array of all yielded values
4
+ * @import {AsyncRow, SqlPrimitive} from '../types.js'
5
+ * @param {AsyncGenerator<AsyncRow>} asyncRows
6
+ * @returns {Promise<Record<string, SqlPrimitive>[]>} array of all yielded values
7
7
  */
8
- export async function collect(asyncGen) {
8
+ export async function collect(asyncRows) {
9
+ /** @type {Record<string, SqlPrimitive>[]} */
9
10
  const results = []
10
- for await (const item of asyncGen) {
11
+ for await (const asyncRow of asyncRows) {
12
+ /** @type {Record<string, SqlPrimitive>} */
13
+ const item = {}
14
+ for (const [key, cell] of Object.entries(asyncRow)) {
15
+ item[key] = await cell()
16
+ }
11
17
  results.push(item)
12
18
  }
13
19
  return results
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExecuteSqlOptions, SelectStatement } from './types.js'
1
+ import type { AsyncRow, ExecuteSqlOptions, SelectStatement, SqlPrimitive } from './types.js'
2
2
 
3
3
  /**
4
4
  * Executes a SQL SELECT query against an array of data rows
@@ -8,12 +8,12 @@ import type { ExecuteSqlOptions, SelectStatement } from './types.js'
8
8
  * @param options.query - SQL query string
9
9
  * @returns async generator yielding rows matching the query
10
10
  */
11
- export function executeSql(options: ExecuteSqlOptions): AsyncGenerator<Record<string, any>>
11
+ export function executeSql(options: ExecuteSqlOptions): AsyncGenerator<AsyncRow>
12
12
 
13
13
  /**
14
14
  * Parses a SQL query string into an abstract syntax tree
15
15
  *
16
- * @param sql - SQL query string to parse
16
+ * @param query - SQL query string to parse
17
17
  * @returns parsed SQL select statement
18
18
  */
19
19
  export function parseSql(query: string): SelectStatement
@@ -24,4 +24,4 @@ export function parseSql(query: string): SelectStatement
24
24
  * @param asyncGen - the async generator
25
25
  * @returns array of all yielded values
26
26
  */
27
- export function collect<T>(asyncGen: AsyncGenerator<T>): Promise<T[]>
27
+ export function collect<T>(asyncGen: AsyncGenerator<AsyncRow>): Promise<Record<string, SqlPrimitive>[]>
package/src/types.d.ts CHANGED
@@ -6,15 +6,13 @@
6
6
  export interface AsyncDataSource {
7
7
  getRows(): AsyncIterable<AsyncRow>
8
8
  }
9
- export interface AsyncRow {
10
- getCell(name: string): any
11
- getKeys(): string[]
12
- }
9
+ export type AsyncRow = Record<string, AsyncCell>
10
+ export type AsyncCell = () => Promise<SqlPrimitive>
13
11
 
14
- export type RawData = Record<string, any>[]
12
+ export type Row = Record<string, SqlPrimitive>[]
15
13
 
16
14
  export interface ExecuteSqlOptions {
17
- tables: Record<string, RawData | AsyncDataSource>
15
+ tables: Record<string, Row | AsyncDataSource>
18
16
  query: string
19
17
  }
20
18
 
@@ -23,7 +21,7 @@ export type SqlPrimitive = string | number | bigint | boolean | null
23
21
  export interface SelectStatement {
24
22
  distinct: boolean
25
23
  columns: SelectColumn[]
26
- from?: string | FromSubquery
24
+ from: string | FromSubquery
27
25
  joins: JoinClause[]
28
26
  where?: ExprNode
29
27
  groupBy: ExprNode[]