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 +5 -1
- package/package.json +5 -5
- package/src/backend/dataSource.js +23 -16
- package/src/execute/columns.js +39 -5
- package/src/execute/execute.js +91 -69
- package/src/execute/expression.js +141 -40
- package/src/execute/join.js +38 -31
- package/src/execute/math.js +178 -3
- package/src/execute/utils.js +6 -2
- package/src/executionErrors.js +7 -6
- package/src/parse/expression.js +20 -0
- package/src/parse/parse.js +3 -52
- package/src/parseErrors.js +15 -14
- package/src/types.d.ts +40 -37
- package/src/validation.js +2 -0
- package/src/validationErrors.js +14 -3
- package/src/execute/aggregates.js +0 -119
package/README.md
CHANGED
|
@@ -40,7 +40,10 @@ const users = [
|
|
|
40
40
|
// ...more rows
|
|
41
41
|
]
|
|
42
42
|
|
|
43
|
-
|
|
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.
|
|
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.
|
|
41
|
-
"@vitest/coverage-v8": "4.0.
|
|
42
|
-
"eslint": "9.39.
|
|
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.
|
|
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 *
|
|
15
|
-
|
|
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 {
|
|
28
|
-
const
|
|
29
|
+
/** @type {AsyncCells} */
|
|
30
|
+
const cells = {}
|
|
29
31
|
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
-
|
|
32
|
+
cells[key] = () => Promise.resolve(value)
|
|
31
33
|
}
|
|
32
|
-
return
|
|
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 *
|
|
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 *
|
|
67
|
+
async *scan(options) {
|
|
68
|
+
const { signal } = options
|
|
64
69
|
let index = 0
|
|
65
|
-
for await (const row of source.
|
|
70
|
+
for await (const row of source.scan(options)) {
|
|
71
|
+
if (signal?.aborted) break
|
|
66
72
|
const rowIndex = index
|
|
67
|
-
/** @type {
|
|
68
|
-
const
|
|
69
|
-
for (const
|
|
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
|
-
|
|
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
|
|
88
|
+
yield { columns: row.columns, cells }
|
|
82
89
|
index++
|
|
83
90
|
}
|
|
84
91
|
},
|
package/src/execute/columns.js
CHANGED
|
@@ -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
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
89
|
+
* @param {AsyncCells} cells
|
|
85
90
|
* @returns {Promise<string>} a stable string representation of the row
|
|
86
91
|
*/
|
|
87
|
-
async function stableRowKey(
|
|
88
|
-
const keys = Object.keys(
|
|
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
|
|
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 {
|
|
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 === '
|
|
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 {
|
|
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.
|
|
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 {
|
|
274
|
-
const
|
|
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
|
|
279
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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(
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
396
|
-
|
|
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
|
|
402
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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,
|
|
446
|
+
if (!await evaluateHavingExpr(select.having, asyncRow, group, tables)) {
|
|
428
447
|
continue
|
|
429
448
|
}
|
|
430
449
|
}
|
|
431
450
|
|
|
432
|
-
projected.push(
|
|
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
|
-
|
|
450
|
-
|
|
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
|
|
454
|
-
|
|
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
|
-
|
|
479
|
+
columns.push(alias)
|
|
480
|
+
cells[alias] = () => evaluateExpr({ node: col.expr, row, tables })
|
|
459
481
|
}
|
|
460
482
|
}
|
|
461
|
-
projected.push(
|
|
483
|
+
projected.push({ columns, cells })
|
|
462
484
|
}
|
|
463
485
|
}
|
|
464
486
|
|