squirreling 0.2.6 → 0.3.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/README.md +34 -11
- package/package.json +3 -3
- package/src/backend/memory.js +8 -9
- package/src/execute/aggregates.js +4 -4
- package/src/execute/execute.js +280 -66
- package/src/execute/expression.js +63 -34
- package/src/execute/having.js +28 -25
- package/src/execute/utils.js +14 -0
- package/src/index.d.ts +12 -4
- package/src/index.js +1 -0
- package/src/types.d.ts +17 -12
- package/src/validation.js +1 -1
package/README.md
CHANGED
|
@@ -7,30 +7,53 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/squirreling)
|
|
8
8
|
[](https://github.com/hyparam/squirreling/actions)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
|
-

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