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 +1 -1
- package/src/backend/dataSource.js +48 -15
- package/src/execute/aggregates.js +17 -18
- package/src/execute/execute.js +38 -44
- package/src/execute/expression.js +29 -20
- package/src/execute/having.js +5 -20
- package/src/execute/utils.js +12 -6
- package/src/index.d.ts +4 -4
- package/src/types.d.ts +5 -7
package/package.json
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
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,
|
|
23
|
+
* @param {Record<string, SqlPrimitive>} obj - the plain object
|
|
26
24
|
* @returns {AsyncRow} a row accessor interface
|
|
27
25
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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,
|
|
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 {
|
|
8
|
-
* @param {
|
|
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 (
|
|
18
|
-
const v = await evaluateExpr({ node: arg.expr, row
|
|
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 (
|
|
38
|
-
const raw = await evaluateExpr({ node: arg.expr, row
|
|
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
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { evaluateExpr } from './expression.js'
|
|
2
2
|
import { parseSql } from '../parse/parse.js'
|
|
3
|
-
import {
|
|
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<
|
|
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<
|
|
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 {
|
|
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 {
|
|
142
|
-
* @param {boolean} distinct -
|
|
143
|
-
* @returns {
|
|
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 {
|
|
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 {
|
|
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<
|
|
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
|
|
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
|
|
286
|
-
* @param {AsyncDataSource} dataSource
|
|
285
|
+
* @param {SelectStatement} select
|
|
286
|
+
* @param {AsyncDataSource} dataSource
|
|
287
287
|
* @param {Record<string, AsyncDataSource>} tables
|
|
288
|
-
* @returns {AsyncGenerator<
|
|
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<
|
|
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 {
|
|
340
|
+
/** @type {AsyncRow} */
|
|
341
341
|
const outRow = {}
|
|
342
342
|
for (const col of select.columns) {
|
|
343
343
|
if (col.kind === 'star') {
|
|
344
|
-
const
|
|
345
|
-
|
|
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] =
|
|
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<
|
|
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 {
|
|
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 {
|
|
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
|
|
456
|
-
|
|
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
|
-
|
|
467
|
-
resultRow[alias] = value
|
|
464
|
+
resultRow[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
|
|
468
465
|
} else {
|
|
469
|
-
resultRow[alias]
|
|
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
|
-
|
|
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 {
|
|
502
|
+
/** @type {AsyncRow} */
|
|
507
503
|
const outRow = {}
|
|
508
504
|
for (const col of select.columns) {
|
|
509
505
|
if (col.kind === 'star') {
|
|
510
|
-
const
|
|
511
|
-
|
|
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
|
-
|
|
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>}
|
|
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
|
|
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
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
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 =
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
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 =
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
const
|
|
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
|
|
259
|
-
return results.
|
|
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
|
|
263
|
-
return results.
|
|
271
|
+
const results = await executeSelect(node.subquery, tables).next()
|
|
272
|
+
return results.done === true
|
|
264
273
|
}
|
|
265
274
|
|
|
266
275
|
// CASE expressions
|
package/src/execute/having.js
CHANGED
|
@@ -8,32 +8,17 @@ import { evaluateExpr } from './expression.js'
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates a context for evaluating HAVING expressions
|
|
10
10
|
*
|
|
11
|
-
* @param {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
package/src/execute/utils.js
CHANGED
|
@@ -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
|
-
* @
|
|
5
|
-
* @param {AsyncGenerator<
|
|
6
|
-
* @returns {Promise<
|
|
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(
|
|
8
|
+
export async function collect(asyncRows) {
|
|
9
|
+
/** @type {Record<string, SqlPrimitive>[]} */
|
|
9
10
|
const results = []
|
|
10
|
-
for await (const
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
10
|
-
|
|
11
|
-
getKeys(): string[]
|
|
12
|
-
}
|
|
9
|
+
export type AsyncRow = Record<string, AsyncCell>
|
|
10
|
+
export type AsyncCell = () => Promise<SqlPrimitive>
|
|
13
11
|
|
|
14
|
-
export type
|
|
12
|
+
export type Row = Record<string, SqlPrimitive>[]
|
|
15
13
|
|
|
16
14
|
export interface ExecuteSqlOptions {
|
|
17
|
-
tables: Record<string,
|
|
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
|
|
24
|
+
from: string | FromSubquery
|
|
27
25
|
joins: JoinClause[]
|
|
28
26
|
where?: ExprNode
|
|
29
27
|
groupBy: ExprNode[]
|