squirreling 0.2.6 → 0.3.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 +37 -11
- package/package.json +3 -3
- package/src/backend/dataSource.js +53 -0
- package/src/execute/aggregates.js +6 -6
- package/src/execute/execute.js +308 -71
- package/src/execute/expression.js +73 -34
- package/src/execute/having.js +34 -31
- package/src/execute/utils.js +14 -0
- package/src/index.d.ts +12 -4
- package/src/index.js +1 -0
- package/src/parse/expression.js +11 -3
- package/src/types.d.ts +25 -14
- package/src/validation.js +1 -1
- package/src/backend/memory.js +0 -37
package/README.md
CHANGED
|
@@ -7,30 +7,56 @@
|
|
|
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 backends 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
|
|
25
|
+
- Select only
|
|
26
|
+
- No joins (yet)
|
|
22
27
|
|
|
23
28
|
## Usage
|
|
24
29
|
|
|
30
|
+
Squirreling returns an async generator, allowing you to process rows one at a time without loading everything into memory.
|
|
31
|
+
|
|
25
32
|
```javascript
|
|
26
33
|
import { executeSql } from 'squirreling'
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ id:
|
|
35
|
+
// In-memory table
|
|
36
|
+
const users = [
|
|
37
|
+
{ id: 1, name: 'Alice', active: true },
|
|
38
|
+
{ id: 2, name: 'Bob', active: false },
|
|
39
|
+
{ id: 3, name: 'Charlie', active: true },
|
|
40
|
+
// ...more rows
|
|
31
41
|
]
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
// Process rows as they arrive (streaming)
|
|
44
|
+
for await (const { cnt } of executeSql({
|
|
45
|
+
tables: { users },
|
|
46
|
+
query: 'SELECT count(*) as cnt FROM users WHERE active = TRUE LIMIT 10',
|
|
47
|
+
})) {
|
|
48
|
+
console.log('Count', cnt)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
There is an exported helper function `collect` to gather all rows into an array if needed:
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
import { collect, executeSql } from 'squirreling'
|
|
56
|
+
|
|
57
|
+
const allUsers = await collect(executeSql({
|
|
58
|
+
tables: { users },
|
|
59
|
+
query: 'SELECT * FROM users',
|
|
60
|
+
}))
|
|
61
|
+
console.log(allUsers)
|
|
36
62
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { AsyncDataSource, AsyncRow } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wraps an async generator of plain objects into an AsyncDataSource
|
|
8
|
+
*
|
|
9
|
+
* @param {AsyncGenerator<Record<string, any>>} gen
|
|
10
|
+
* @returns {AsyncDataSource}
|
|
11
|
+
*/
|
|
12
|
+
export function generatorSource(gen) {
|
|
13
|
+
return {
|
|
14
|
+
async *getRows() {
|
|
15
|
+
for await (const row of gen) {
|
|
16
|
+
yield asyncRow(row)
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates an async row accessor that wraps a plain JavaScript object
|
|
24
|
+
*
|
|
25
|
+
* @param {Record<string, any>} obj - the plain object
|
|
26
|
+
* @returns {AsyncRow} a row accessor interface
|
|
27
|
+
*/
|
|
28
|
+
export function asyncRow(obj) {
|
|
29
|
+
return {
|
|
30
|
+
getCell(name) {
|
|
31
|
+
return obj[name]
|
|
32
|
+
},
|
|
33
|
+
getKeys() {
|
|
34
|
+
return Object.keys(obj)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates an async memory-backed data source from an array of plain objects
|
|
41
|
+
*
|
|
42
|
+
* @param {Record<string, any>[]} data - array of plain objects
|
|
43
|
+
* @returns {AsyncDataSource} an async data source interface
|
|
44
|
+
*/
|
|
45
|
+
export function memorySource(data) {
|
|
46
|
+
return {
|
|
47
|
+
async *getRows() {
|
|
48
|
+
for (const item of data) {
|
|
49
|
+
yield asyncRow(item)
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -3,19 +3,19 @@ import { evaluateExpr } from './expression.js'
|
|
|
3
3
|
/**
|
|
4
4
|
* Evaluates an aggregate function over a set of rows
|
|
5
5
|
*
|
|
6
|
-
* @import { AggregateColumn, ExprNode,
|
|
6
|
+
* @import { AggregateColumn, ExprNode, AsyncRow } from '../types.js'
|
|
7
7
|
* @param {AggregateColumn} col - aggregate column definition
|
|
8
|
-
* @param {
|
|
9
|
-
* @returns {number | null} aggregated result
|
|
8
|
+
* @param {AsyncRow[]} rows - rows to aggregate
|
|
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,68 @@
|
|
|
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 { asyncRow, generatorSource, memorySource } from '../backend/dataSource.js'
|
|
4
|
+
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
5
|
+
import { evaluateHavingExpr } from './having.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* @import {
|
|
8
|
+
* @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Executes a SQL SELECT query against
|
|
12
|
+
* Executes a SQL SELECT query against named data sources
|
|
13
13
|
*
|
|
14
14
|
* @param {ExecuteSqlOptions} options - the execution options
|
|
15
|
-
* @returns {Record<string, any
|
|
15
|
+
* @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
|
|
16
16
|
*/
|
|
17
|
-
export function executeSql({
|
|
17
|
+
export async function* executeSql({ tables, query }) {
|
|
18
18
|
const select = parseSql(query)
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
// Check for unsupported operations
|
|
21
|
+
if (select.joins.length) {
|
|
22
|
+
throw new Error('JOIN is not supported')
|
|
23
|
+
}
|
|
24
|
+
if (!select.from) {
|
|
25
|
+
throw new Error('FROM clause is required')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Normalize tables: convert arrays to AsyncDataSource
|
|
29
|
+
/** @type {Record<string, AsyncDataSource>} */
|
|
30
|
+
const normalizedTables = {}
|
|
31
|
+
for (const [name, source] of Object.entries(tables)) {
|
|
32
|
+
if (Array.isArray(source)) {
|
|
33
|
+
normalizedTables[name] = memorySource(source)
|
|
34
|
+
} else {
|
|
35
|
+
normalizedTables[name] = source
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
yield* executeSelect(select, normalizedTables)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Executes a SELECT query against the provided tables
|
|
44
|
+
*
|
|
45
|
+
* @param {SelectStatement} select
|
|
46
|
+
* @param {Record<string, AsyncDataSource>} tables
|
|
47
|
+
* @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
|
|
48
|
+
*/
|
|
49
|
+
export async function* executeSelect(select, tables) {
|
|
50
|
+
/** @type {AsyncDataSource} */
|
|
51
|
+
let dataSource
|
|
52
|
+
|
|
53
|
+
if (typeof select.from === 'string') {
|
|
54
|
+
const table = tables[select.from]
|
|
55
|
+
if (table === undefined) {
|
|
56
|
+
throw new Error(`Table "${select.from}" not found`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
dataSource = table
|
|
60
|
+
} else {
|
|
61
|
+
// Nested subquery - recursively resolve
|
|
62
|
+
dataSource = generatorSource(executeSelect(select.from.query, tables))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
yield* evaluateSelectAst(select, dataSource, tables)
|
|
21
66
|
}
|
|
22
67
|
|
|
23
68
|
/**
|
|
@@ -112,32 +157,108 @@ function applyDistinct(rows, distinct) {
|
|
|
112
157
|
return result
|
|
113
158
|
}
|
|
114
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Applies ORDER BY sorting to RowSource array (before projection)
|
|
162
|
+
*
|
|
163
|
+
* @param {AsyncRow[]} rows - the input row sources
|
|
164
|
+
* @param {OrderByItem[]} orderBy - the sort specifications
|
|
165
|
+
* @param {Record<string, AsyncDataSource>} tables
|
|
166
|
+
* @returns {Promise<AsyncRow[]>} the sorted row sources
|
|
167
|
+
*/
|
|
168
|
+
async function sortRowSources(rows, orderBy, tables) {
|
|
169
|
+
if (!orderBy.length) return rows
|
|
170
|
+
|
|
171
|
+
// Pre-evaluate ORDER BY expressions for all rows
|
|
172
|
+
/** @type {SqlPrimitive[][]} */
|
|
173
|
+
const evaluatedValues = []
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
/** @type {SqlPrimitive[]} */
|
|
176
|
+
const rowValues = []
|
|
177
|
+
for (const term of orderBy) {
|
|
178
|
+
const value = await evaluateExpr({ node: term.expr, row, tables })
|
|
179
|
+
rowValues.push(value)
|
|
180
|
+
}
|
|
181
|
+
evaluatedValues.push(rowValues)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create index array and sort it
|
|
185
|
+
const indices = rows.map((_, i) => i)
|
|
186
|
+
indices.sort((aIdx, bIdx) => {
|
|
187
|
+
for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
|
|
188
|
+
const term = orderBy[termIdx]
|
|
189
|
+
const dir = term.direction
|
|
190
|
+
const av = evaluatedValues[aIdx][termIdx]
|
|
191
|
+
const bv = evaluatedValues[bIdx][termIdx]
|
|
192
|
+
|
|
193
|
+
// Handle NULLS FIRST / NULLS LAST
|
|
194
|
+
const aIsNull = av == null
|
|
195
|
+
const bIsNull = bv == null
|
|
196
|
+
|
|
197
|
+
if (aIsNull || bIsNull) {
|
|
198
|
+
if (aIsNull && bIsNull) continue
|
|
199
|
+
|
|
200
|
+
const nullsFirst = term.nulls === 'LAST' ? false : true
|
|
201
|
+
|
|
202
|
+
if (aIsNull) {
|
|
203
|
+
return nullsFirst ? -1 : 1
|
|
204
|
+
} else {
|
|
205
|
+
return nullsFirst ? 1 : -1
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const cmp = compareValues(av, bv)
|
|
210
|
+
if (cmp !== 0) {
|
|
211
|
+
return dir === 'DESC' ? -cmp : cmp
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return 0
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Return sorted rows
|
|
218
|
+
return indices.map(i => rows[i])
|
|
219
|
+
}
|
|
220
|
+
|
|
115
221
|
/**
|
|
116
222
|
* Applies ORDER BY sorting to rows
|
|
117
223
|
*
|
|
118
224
|
* @param {Record<string, any>[]} rows - the input rows
|
|
119
225
|
* @param {OrderByItem[]} orderBy - the sort specifications
|
|
120
|
-
* @
|
|
226
|
+
* @param {Record<string, AsyncDataSource>} tables
|
|
227
|
+
* @returns {Promise<Record<string, any>[]>} the sorted rows
|
|
121
228
|
*/
|
|
122
|
-
function applyOrderBy(rows, orderBy) {
|
|
123
|
-
if (!orderBy
|
|
229
|
+
async function applyOrderBy(rows, orderBy, tables) {
|
|
230
|
+
if (!orderBy.length) return rows
|
|
124
231
|
|
|
125
|
-
|
|
126
|
-
|
|
232
|
+
// Pre-evaluate ORDER BY expressions for all rows
|
|
233
|
+
/** @type {SqlPrimitive[][]} */
|
|
234
|
+
const evaluatedValues = []
|
|
235
|
+
for (const row of rows) {
|
|
236
|
+
/** @type {SqlPrimitive[]} */
|
|
237
|
+
const rowValues = []
|
|
127
238
|
for (const term of orderBy) {
|
|
239
|
+
const value = await evaluateExpr({ node: term.expr, row: asyncRow(row), tables })
|
|
240
|
+
rowValues.push(value)
|
|
241
|
+
}
|
|
242
|
+
evaluatedValues.push(rowValues)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Create index array and sort it
|
|
246
|
+
const indices = rows.map((_, i) => i)
|
|
247
|
+
indices.sort((aIdx, bIdx) => {
|
|
248
|
+
for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
|
|
249
|
+
const term = orderBy[termIdx]
|
|
128
250
|
const dir = term.direction
|
|
129
|
-
const av =
|
|
130
|
-
const bv =
|
|
251
|
+
const av = evaluatedValues[aIdx][termIdx]
|
|
252
|
+
const bv = evaluatedValues[bIdx][termIdx]
|
|
131
253
|
|
|
132
254
|
// Handle NULLS FIRST / NULLS LAST
|
|
133
255
|
const aIsNull = av == null
|
|
134
256
|
const bIsNull = bv == null
|
|
135
257
|
|
|
136
258
|
if (aIsNull || bIsNull) {
|
|
137
|
-
if (aIsNull && bIsNull) continue
|
|
259
|
+
if (aIsNull && bIsNull) continue
|
|
138
260
|
|
|
139
|
-
|
|
140
|
-
const nullsFirst = term.nulls === 'LAST' ? false : true // default is NULLS FIRST
|
|
261
|
+
const nullsFirst = term.nulls === 'LAST' ? false : true
|
|
141
262
|
|
|
142
263
|
if (aIsNull) {
|
|
143
264
|
return nullsFirst ? -1 : 1
|
|
@@ -154,59 +275,156 @@ function applyOrderBy(rows, orderBy) {
|
|
|
154
275
|
return 0
|
|
155
276
|
})
|
|
156
277
|
|
|
157
|
-
|
|
278
|
+
// Return sorted rows
|
|
279
|
+
return indices.map(i => rows[i])
|
|
158
280
|
}
|
|
159
281
|
|
|
160
282
|
/**
|
|
161
|
-
* Evaluates a
|
|
283
|
+
* Evaluates a select with a resolved FROM data source
|
|
162
284
|
*
|
|
163
285
|
* @param {SelectStatement} select - the parsed SQL AST
|
|
164
|
-
* @param {
|
|
165
|
-
* @
|
|
286
|
+
* @param {AsyncDataSource} dataSource - the async data source
|
|
287
|
+
* @param {Record<string, AsyncDataSource>} tables
|
|
288
|
+
* @returns {AsyncGenerator<Record<string, any>>} async generator yielding result rows
|
|
166
289
|
*/
|
|
167
|
-
function evaluateSelectAst(select, dataSource) {
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
290
|
+
async function* evaluateSelectAst(select, dataSource, tables) {
|
|
291
|
+
// SQL priority: from, where, group by, having, select, order by, offset, limit
|
|
292
|
+
|
|
293
|
+
const hasAggregate = select.columns.some(col => col.kind === 'aggregate')
|
|
294
|
+
const useGrouping = hasAggregate || select.groupBy.length > 0
|
|
295
|
+
const needsBuffering = useGrouping || select.orderBy.length > 0
|
|
172
296
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
297
|
+
if (needsBuffering) {
|
|
298
|
+
// BUFFERING PATH: Collect all rows, process, then yield
|
|
299
|
+
yield* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping)
|
|
300
|
+
} else {
|
|
301
|
+
// STREAMING PATH: Yield rows one by one
|
|
302
|
+
yield* evaluateStreaming(select, dataSource, tables)
|
|
176
303
|
}
|
|
304
|
+
}
|
|
177
305
|
|
|
178
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Streaming evaluation for simple queries (no ORDER BY or GROUP BY)
|
|
308
|
+
* Supports DISTINCT by tracking seen row keys without buffering full rows
|
|
309
|
+
*
|
|
310
|
+
* @param {SelectStatement} select
|
|
311
|
+
* @param {AsyncDataSource} dataSource
|
|
312
|
+
* @param {Record<string, AsyncDataSource>} tables
|
|
313
|
+
* @returns {AsyncGenerator<Record<string, any>>}
|
|
314
|
+
*/
|
|
315
|
+
async function* evaluateStreaming(select, dataSource, tables) {
|
|
316
|
+
let rowsYielded = 0
|
|
317
|
+
let rowsSkipped = 0
|
|
318
|
+
const offset = select.offset ?? 0
|
|
319
|
+
const limit = select.limit ?? Infinity
|
|
320
|
+
if (limit <= 0) return
|
|
321
|
+
|
|
322
|
+
// For DISTINCT, track seen row keys
|
|
323
|
+
/** @type {Set<string> | undefined} */
|
|
324
|
+
const seen = select.distinct ? new Set() : undefined
|
|
325
|
+
|
|
326
|
+
for await (const row of dataSource.getRows()) {
|
|
327
|
+
// WHERE filter
|
|
328
|
+
if (select.where) {
|
|
329
|
+
const pass = await evaluateExpr({ node: select.where, row, tables })
|
|
330
|
+
if (!pass) continue
|
|
331
|
+
}
|
|
179
332
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
333
|
+
// For non-DISTINCT queries, we can skip rows before projection (optimization)
|
|
334
|
+
if (!seen && rowsSkipped < offset) {
|
|
335
|
+
rowsSkipped++
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// SELECT projection
|
|
340
|
+
/** @type {Record<string, any>} */
|
|
341
|
+
const outRow = {}
|
|
342
|
+
for (const col of select.columns) {
|
|
343
|
+
if (col.kind === 'star') {
|
|
344
|
+
const keys = row.getKeys()
|
|
345
|
+
for (const key of keys) {
|
|
346
|
+
outRow[key] = row.getCell(key)
|
|
347
|
+
}
|
|
348
|
+
} else if (col.kind === 'derived') {
|
|
349
|
+
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
350
|
+
outRow[alias] = await evaluateExpr({ node: col.expr, row, tables })
|
|
351
|
+
} else if (col.kind === 'aggregate') {
|
|
352
|
+
throw new Error(
|
|
353
|
+
'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// DISTINCT: skip duplicate rows
|
|
359
|
+
if (seen) {
|
|
360
|
+
const key = stableRowKey(outRow)
|
|
361
|
+
if (seen.has(key)) continue
|
|
362
|
+
seen.add(key)
|
|
363
|
+
// OFFSET applies to distinct rows
|
|
364
|
+
if (rowsSkipped < offset) {
|
|
365
|
+
rowsSkipped++
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
yield outRow
|
|
371
|
+
rowsYielded++
|
|
372
|
+
if (rowsYielded >= limit) {
|
|
373
|
+
break
|
|
188
374
|
}
|
|
189
375
|
}
|
|
376
|
+
}
|
|
190
377
|
|
|
191
|
-
|
|
192
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Buffered evaluation for complex queries (with ORDER BY or GROUP BY)
|
|
380
|
+
*
|
|
381
|
+
* @param {SelectStatement} select
|
|
382
|
+
* @param {AsyncDataSource} dataSource
|
|
383
|
+
* @param {Record<string, AsyncDataSource>} tables
|
|
384
|
+
* @param {boolean} hasAggregate
|
|
385
|
+
* @param {boolean} useGrouping
|
|
386
|
+
* @returns {AsyncGenerator<Record<string, any>>}
|
|
387
|
+
*/
|
|
388
|
+
async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
|
|
389
|
+
// Step 1: Collect all rows from data source
|
|
390
|
+
/** @type {AsyncRow[]} */
|
|
391
|
+
const working = []
|
|
392
|
+
for await (const row of dataSource.getRows()) {
|
|
393
|
+
working.push(row)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Step 2: WHERE clause filtering
|
|
397
|
+
/** @type {AsyncRow[]} */
|
|
398
|
+
const filtered = []
|
|
193
399
|
|
|
400
|
+
for (const row of working) {
|
|
401
|
+
if (select.where) {
|
|
402
|
+
const passes = await evaluateExpr({ node: select.where, row, tables })
|
|
403
|
+
|
|
404
|
+
if (!passes) {
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
filtered.push(row)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Step 3: Projection (grouping vs non-grouping)
|
|
194
412
|
/** @type {Record<string, any>[]} */
|
|
195
|
-
|
|
413
|
+
let projected = []
|
|
196
414
|
|
|
197
415
|
if (useGrouping) {
|
|
198
416
|
// Grouping due to GROUP BY or aggregate functions
|
|
199
|
-
/** @type {
|
|
417
|
+
/** @type {AsyncRow[][]} */
|
|
200
418
|
const groups = []
|
|
201
419
|
|
|
202
|
-
if (select.groupBy
|
|
203
|
-
/** @type {Map<string,
|
|
420
|
+
if (select.groupBy.length) {
|
|
421
|
+
/** @type {Map<string, AsyncRow[]>} */
|
|
204
422
|
const map = new Map()
|
|
205
|
-
for (const row of
|
|
423
|
+
for (const row of filtered) {
|
|
206
424
|
/** @type {string[]} */
|
|
207
425
|
const keyParts = []
|
|
208
426
|
for (const expr of select.groupBy) {
|
|
209
|
-
const v = evaluateExpr(expr, row)
|
|
427
|
+
const v = await evaluateExpr({ node: expr, row, tables })
|
|
210
428
|
keyParts.push(JSON.stringify(v))
|
|
211
429
|
}
|
|
212
430
|
const key = keyParts.join('|')
|
|
@@ -219,7 +437,7 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
219
437
|
group.push(row)
|
|
220
438
|
}
|
|
221
439
|
} else {
|
|
222
|
-
groups.push(
|
|
440
|
+
groups.push(filtered)
|
|
223
441
|
}
|
|
224
442
|
|
|
225
443
|
const hasStar = select.columns.some(col => col.kind === 'star')
|
|
@@ -244,14 +462,18 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
244
462
|
|
|
245
463
|
if (col.kind === 'derived') {
|
|
246
464
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
247
|
-
|
|
248
|
-
|
|
465
|
+
if (group.length > 0) {
|
|
466
|
+
const value = await evaluateExpr({ node: col.expr, row: group[0], tables })
|
|
467
|
+
resultRow[alias] = value
|
|
468
|
+
} else {
|
|
469
|
+
resultRow[alias] = undefined
|
|
470
|
+
}
|
|
249
471
|
continue
|
|
250
472
|
}
|
|
251
473
|
|
|
252
474
|
if (col.kind === 'aggregate') {
|
|
253
475
|
const alias = col.alias ?? defaultAggregateAlias(col)
|
|
254
|
-
const value = evaluateAggregate(col, group)
|
|
476
|
+
const value = await evaluateAggregate(col, group)
|
|
255
477
|
resultRow[alias] = value
|
|
256
478
|
continue
|
|
257
479
|
}
|
|
@@ -259,9 +481,7 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
259
481
|
|
|
260
482
|
// Apply HAVING filter before adding to projected results
|
|
261
483
|
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)) {
|
|
484
|
+
if (!await evaluateHavingExpr(select.having, resultRow, group, tables)) {
|
|
265
485
|
continue
|
|
266
486
|
}
|
|
267
487
|
}
|
|
@@ -270,7 +490,19 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
270
490
|
}
|
|
271
491
|
} else {
|
|
272
492
|
// No grouping, simple projection
|
|
273
|
-
|
|
493
|
+
// Sort before projection so ORDER BY can access columns not in SELECT
|
|
494
|
+
const sorted = await sortRowSources(filtered, select.orderBy, tables)
|
|
495
|
+
|
|
496
|
+
// OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
|
|
497
|
+
// to avoid reading expensive cells for rows that won't be in the final result
|
|
498
|
+
let rowsToProject = sorted
|
|
499
|
+
if (!select.distinct) {
|
|
500
|
+
const start = select.offset ?? 0
|
|
501
|
+
const end = select.limit ? start + select.limit : sorted.length
|
|
502
|
+
rowsToProject = sorted.slice(start, end)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
for (const row of rowsToProject) {
|
|
274
506
|
/** @type {Record<string, any>} */
|
|
275
507
|
const outRow = {}
|
|
276
508
|
for (const col of select.columns) {
|
|
@@ -281,29 +513,34 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
281
513
|
}
|
|
282
514
|
} else if (col.kind === 'derived') {
|
|
283
515
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
284
|
-
const value = evaluateExpr(col.expr, row)
|
|
516
|
+
const value = await evaluateExpr({ node: col.expr, row, tables })
|
|
285
517
|
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
518
|
}
|
|
291
519
|
}
|
|
292
520
|
projected.push(outRow)
|
|
293
521
|
}
|
|
294
522
|
}
|
|
295
523
|
|
|
296
|
-
|
|
524
|
+
// Step 4: DISTINCT
|
|
525
|
+
projected = applyDistinct(projected, select.distinct)
|
|
297
526
|
|
|
298
|
-
|
|
299
|
-
|
|
527
|
+
// Step 5: ORDER BY (final sort for grouped queries)
|
|
528
|
+
projected = await applyOrderBy(projected, select.orderBy, tables)
|
|
300
529
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
530
|
+
// Step 6: OFFSET and LIMIT
|
|
531
|
+
// For non-DISTINCT, non-grouping queries, OFFSET/LIMIT was already applied before projection
|
|
532
|
+
if (select.distinct || useGrouping) {
|
|
533
|
+
const start = select.offset ?? 0
|
|
534
|
+
const end = select.limit ? start + select.limit : projected.length
|
|
307
535
|
|
|
308
|
-
|
|
536
|
+
// Step 7: Yield results
|
|
537
|
+
for (let i = start; i < end && i < projected.length; i++) {
|
|
538
|
+
yield projected[i]
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
// Already limited, yield all projected rows
|
|
542
|
+
for (const row of projected) {
|
|
543
|
+
yield row
|
|
544
|
+
}
|
|
545
|
+
}
|
|
309
546
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
import { executeSelect } from './execute.js'
|
|
2
|
+
import { collect } from './utils.js'
|
|
1
3
|
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
5
|
+
* @import { ExprNode, AsyncRow, 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 {
|
|
8
|
-
* @
|
|
11
|
+
* @param {Object} params
|
|
12
|
+
* @param {ExprNode} params.node - The expression node to evaluate
|
|
13
|
+
* @param {AsyncRow} 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
|
}
|
|
@@ -16,19 +23,29 @@ export function evaluateExpr(node, row) {
|
|
|
16
23
|
return row.getCell(node.name)
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
// Scalar subquery - returns a single value
|
|
27
|
+
if (node.type === 'subquery') {
|
|
28
|
+
const results = await collect(executeSelect(node.subquery, tables))
|
|
29
|
+
if (results.length === 0) return null
|
|
30
|
+
// Return the first column of the first row
|
|
31
|
+
const firstRow = results[0]
|
|
32
|
+
const firstKey = Object.keys(firstRow)[0]
|
|
33
|
+
return firstRow[firstKey]
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
// Unary operators
|
|
20
37
|
if (node.type === 'unary') {
|
|
21
38
|
if (node.op === 'NOT') {
|
|
22
|
-
return !evaluateExpr(node.argument, row)
|
|
39
|
+
return !await evaluateExpr({ node: node.argument, row, tables })
|
|
23
40
|
}
|
|
24
41
|
if (node.op === 'IS NULL') {
|
|
25
|
-
return evaluateExpr(node.argument, row) == null
|
|
42
|
+
return await evaluateExpr({ node: node.argument, row, tables }) == null
|
|
26
43
|
}
|
|
27
44
|
if (node.op === 'IS NOT NULL') {
|
|
28
|
-
return evaluateExpr(node.argument, row) != null
|
|
45
|
+
return await evaluateExpr({ node: node.argument, row, tables }) != null
|
|
29
46
|
}
|
|
30
47
|
if (node.op === '-') {
|
|
31
|
-
const val = evaluateExpr(node.argument, row)
|
|
48
|
+
const val = await evaluateExpr({ node: node.argument, row, tables })
|
|
32
49
|
if (val == null) return null
|
|
33
50
|
return -Number(val)
|
|
34
51
|
}
|
|
@@ -37,19 +54,19 @@ export function evaluateExpr(node, row) {
|
|
|
37
54
|
// Binary operators
|
|
38
55
|
if (node.type === 'binary') {
|
|
39
56
|
if (node.op === 'AND') {
|
|
40
|
-
const leftVal = evaluateExpr(node.left, row)
|
|
57
|
+
const leftVal = await evaluateExpr({ node: node.left, row, tables })
|
|
41
58
|
if (!leftVal) return false
|
|
42
|
-
return Boolean(evaluateExpr(node.right, row))
|
|
59
|
+
return Boolean(await evaluateExpr({ node: node.right, row, tables }))
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
if (node.op === 'OR') {
|
|
46
|
-
const leftVal = evaluateExpr(node.left, row)
|
|
63
|
+
const leftVal = await evaluateExpr({ node: node.left, row, tables })
|
|
47
64
|
if (leftVal) return true
|
|
48
|
-
return Boolean(evaluateExpr(node.right, row))
|
|
65
|
+
return Boolean(await evaluateExpr({ node: node.right, row, tables }))
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
const left = evaluateExpr(node.left, row)
|
|
52
|
-
const right = evaluateExpr(node.right, row)
|
|
68
|
+
const left = await evaluateExpr({ node: node.left, row, tables })
|
|
69
|
+
const right = await evaluateExpr({ node: node.right, row, tables })
|
|
53
70
|
|
|
54
71
|
// In SQL, NULL comparisons with =, !=, <> always return false (unknown)
|
|
55
72
|
// You must use IS NULL or IS NOT NULL to check for NULL
|
|
@@ -83,9 +100,9 @@ export function evaluateExpr(node, row) {
|
|
|
83
100
|
|
|
84
101
|
// BETWEEN and NOT BETWEEN
|
|
85
102
|
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)
|
|
103
|
+
const expr = await evaluateExpr({ node: node.expr, row, tables })
|
|
104
|
+
const lower = await evaluateExpr({ node: node.lower, row, tables })
|
|
105
|
+
const upper = await evaluateExpr({ node: node.upper, row, tables })
|
|
89
106
|
|
|
90
107
|
// If any value is NULL, return false (SQL behavior)
|
|
91
108
|
if (expr == null || lower == null || upper == null) {
|
|
@@ -99,7 +116,7 @@ export function evaluateExpr(node, row) {
|
|
|
99
116
|
// Function calls
|
|
100
117
|
if (node.type === 'function') {
|
|
101
118
|
const funcName = node.name.toUpperCase()
|
|
102
|
-
const args = node.args.map(arg => evaluateExpr(arg, row))
|
|
119
|
+
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
|
|
103
120
|
|
|
104
121
|
if (funcName === 'UPPER') {
|
|
105
122
|
if (args.length !== 1) throw new Error('UPPER requires exactly 1 argument')
|
|
@@ -161,11 +178,21 @@ export function evaluateExpr(node, row) {
|
|
|
161
178
|
return String(val).trim()
|
|
162
179
|
}
|
|
163
180
|
|
|
181
|
+
if (funcName === 'REPLACE') {
|
|
182
|
+
if (args.length !== 3) throw new Error('REPLACE requires exactly 3 arguments')
|
|
183
|
+
const str = args[0]
|
|
184
|
+
const searchStr = args[1]
|
|
185
|
+
const replaceStr = args[2]
|
|
186
|
+
// SQL REPLACE returns NULL if any argument is NULL
|
|
187
|
+
if (str == null || searchStr == null || replaceStr == null) return null
|
|
188
|
+
return String(str).replaceAll(String(searchStr), String(replaceStr))
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
throw new Error('Unsupported function ' + funcName)
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
if (node.type === 'cast') {
|
|
168
|
-
const val = evaluateExpr(node.expr, row)
|
|
195
|
+
const val = await evaluateExpr({ node: node.expr, row, tables })
|
|
169
196
|
if (val == null) return null
|
|
170
197
|
const toType = node.toType.toUpperCase()
|
|
171
198
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
@@ -192,17 +219,17 @@ export function evaluateExpr(node, row) {
|
|
|
192
219
|
|
|
193
220
|
// IN and NOT IN with value lists
|
|
194
221
|
if (node.type === 'in valuelist') {
|
|
195
|
-
const exprVal = evaluateExpr(node.expr, row)
|
|
222
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
196
223
|
for (const valueNode of node.values) {
|
|
197
|
-
const val = evaluateExpr(valueNode, row)
|
|
224
|
+
const val = await evaluateExpr({ node: valueNode, row, tables })
|
|
198
225
|
if (exprVal === val) return true
|
|
199
226
|
}
|
|
200
227
|
return false
|
|
201
228
|
}
|
|
202
229
|
if (node.type === 'not in valuelist') {
|
|
203
|
-
const exprVal = evaluateExpr(node.expr, row)
|
|
230
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
204
231
|
for (const valueNode of node.values) {
|
|
205
|
-
const val = evaluateExpr(valueNode, row)
|
|
232
|
+
const val = await evaluateExpr({ node: valueNode, row, tables })
|
|
206
233
|
if (exprVal === val) return false
|
|
207
234
|
}
|
|
208
235
|
return true
|
|
@@ -210,45 +237,57 @@ export function evaluateExpr(node, row) {
|
|
|
210
237
|
|
|
211
238
|
// IN and NOT IN with subqueries
|
|
212
239
|
if (node.type === 'in') {
|
|
213
|
-
|
|
240
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
241
|
+
const results = await collect(executeSelect(node.subquery, tables))
|
|
242
|
+
if (results.length === 0) return false
|
|
243
|
+
const firstKey = Object.keys(results[0])[0]
|
|
244
|
+
const values = results.map(r => r[firstKey])
|
|
245
|
+
return values.includes(exprVal)
|
|
214
246
|
}
|
|
215
247
|
if (node.type === 'not in') {
|
|
216
|
-
|
|
248
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
249
|
+
const results = await collect(executeSelect(node.subquery, tables))
|
|
250
|
+
if (results.length === 0) return true
|
|
251
|
+
const firstKey = Object.keys(results[0])[0]
|
|
252
|
+
const values = results.map(r => r[firstKey])
|
|
253
|
+
return !values.includes(exprVal)
|
|
217
254
|
}
|
|
218
255
|
|
|
219
256
|
// EXISTS and NOT EXISTS with subqueries
|
|
220
257
|
if (node.type === 'exists') {
|
|
221
|
-
|
|
258
|
+
const results = await collect(executeSelect(node.subquery, tables))
|
|
259
|
+
return results.length > 0
|
|
222
260
|
}
|
|
223
261
|
if (node.type === 'not exists') {
|
|
224
|
-
|
|
262
|
+
const results = await collect(executeSelect(node.subquery, tables))
|
|
263
|
+
return results.length === 0
|
|
225
264
|
}
|
|
226
265
|
|
|
227
266
|
// CASE expressions
|
|
228
267
|
if (node.type === 'case') {
|
|
229
268
|
// For simple CASE: evaluate the case expression once
|
|
230
|
-
const caseValue = node.caseExpr ? evaluateExpr(node.caseExpr, row) : undefined
|
|
269
|
+
const caseValue = node.caseExpr ? await evaluateExpr({ node: node.caseExpr, row, tables }) : undefined
|
|
231
270
|
|
|
232
271
|
// Iterate through WHEN clauses
|
|
233
272
|
for (const whenClause of node.whenClauses) {
|
|
234
273
|
let conditionResult
|
|
235
274
|
if (caseValue !== undefined) {
|
|
236
275
|
// Simple CASE: compare caseValue with condition
|
|
237
|
-
const whenValue = evaluateExpr(whenClause.condition, row)
|
|
276
|
+
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables })
|
|
238
277
|
conditionResult = caseValue === whenValue
|
|
239
278
|
} else {
|
|
240
279
|
// Searched CASE: evaluate condition as boolean
|
|
241
|
-
conditionResult = evaluateExpr(whenClause.condition, row)
|
|
280
|
+
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables })
|
|
242
281
|
}
|
|
243
282
|
|
|
244
283
|
if (conditionResult) {
|
|
245
|
-
return evaluateExpr(whenClause.result, row)
|
|
284
|
+
return evaluateExpr({ node: whenClause.result, row, tables })
|
|
246
285
|
}
|
|
247
286
|
}
|
|
248
287
|
|
|
249
288
|
// No WHEN clause matched, return ELSE result or NULL
|
|
250
289
|
if (node.elseResult) {
|
|
251
|
-
return evaluateExpr(node.elseResult, row)
|
|
290
|
+
return evaluateExpr({ node: node.elseResult, row, tables })
|
|
252
291
|
}
|
|
253
292
|
return null
|
|
254
293
|
}
|
package/src/execute/having.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { AggregateFunc, ExprNode,
|
|
2
|
+
* @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isAggregateFunc } from '../validation.js'
|
|
@@ -9,8 +9,8 @@ import { evaluateExpr } from './expression.js'
|
|
|
9
9
|
* Creates a context for evaluating HAVING expressions
|
|
10
10
|
*
|
|
11
11
|
* @param {Record<string, any>} resultRow - the aggregated result row
|
|
12
|
-
* @param {
|
|
13
|
-
* @returns {
|
|
12
|
+
* @param {AsyncRow[]} group - the group of rows
|
|
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)
|
|
@@ -42,10 +42,11 @@ function createHavingContext(resultRow, group) {
|
|
|
42
42
|
*
|
|
43
43
|
* @param {ExprNode} expr - the HAVING expression
|
|
44
44
|
* @param {Record<string, any>} row - the aggregated result row
|
|
45
|
-
* @param {
|
|
46
|
-
* @
|
|
45
|
+
* @param {AsyncRow[]} group - the group of rows for re-evaluating aggregates
|
|
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,31 +122,32 @@ 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
|
/**
|
|
128
129
|
* Evaluates a value in a HAVING expression
|
|
129
130
|
*
|
|
130
131
|
* @param {ExprNode} expr
|
|
131
|
-
* @param {
|
|
132
|
-
* @param {
|
|
133
|
-
* @
|
|
132
|
+
* @param {AsyncRow} context - the context row
|
|
133
|
+
* @param {AsyncRow[]} group - the group of rows
|
|
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
|
/**
|
|
@@ -153,10 +155,11 @@ function evaluateHavingValue(expr, context, group) {
|
|
|
153
155
|
*
|
|
154
156
|
* @param {AggregateFunc} funcName - aggregate function name
|
|
155
157
|
* @param {ExprNode[]} args - function arguments
|
|
156
|
-
* @param {
|
|
157
|
-
* @
|
|
158
|
+
* @param {AsyncRow[]} group - the group of rows
|
|
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/parse/expression.js
CHANGED
|
@@ -20,6 +20,17 @@ function parsePrimary(c) {
|
|
|
20
20
|
const tok = c.current()
|
|
21
21
|
|
|
22
22
|
if (tok.type === 'paren' && tok.value === '(') {
|
|
23
|
+
// Peek ahead to see if this is a scalar subquery
|
|
24
|
+
const nextTok = c.peek(1)
|
|
25
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'SELECT') {
|
|
26
|
+
// It's a scalar subquery
|
|
27
|
+
const subquery = c.parseSubquery()
|
|
28
|
+
return {
|
|
29
|
+
type: 'subquery',
|
|
30
|
+
subquery,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Regular grouped expression
|
|
23
34
|
c.consume()
|
|
24
35
|
const expr = parseExpression(c)
|
|
25
36
|
c.expect('paren', ')')
|
|
@@ -132,9 +143,6 @@ function parsePrimary(c) {
|
|
|
132
143
|
}
|
|
133
144
|
if (tok.value === 'EXISTS') {
|
|
134
145
|
c.consume() // EXISTS
|
|
135
|
-
if (!c.parseSubquery) {
|
|
136
|
-
throw new Error('Subquery parsing not available in this context')
|
|
137
|
-
}
|
|
138
146
|
const subquery = c.parseSubquery()
|
|
139
147
|
return {
|
|
140
148
|
type: 'exists',
|
package/src/types.d.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
|
|
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<AsyncRow>
|
|
8
|
+
}
|
|
9
|
+
export interface AsyncRow {
|
|
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'
|
|
@@ -117,6 +122,11 @@ export interface CaseNode {
|
|
|
117
122
|
elseResult?: ExprNode
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
export interface SubqueryNode {
|
|
126
|
+
type: 'subquery'
|
|
127
|
+
subquery: SelectStatement
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
export type ExprNode =
|
|
121
131
|
| LiteralNode
|
|
122
132
|
| IdentifierNode
|
|
@@ -129,6 +139,7 @@ export type ExprNode =
|
|
|
129
139
|
| InValuesNode
|
|
130
140
|
| ExistsNode
|
|
131
141
|
| CaseNode
|
|
142
|
+
| SubqueryNode
|
|
132
143
|
|
|
133
144
|
export interface StarColumn {
|
|
134
145
|
kind: 'star'
|
|
@@ -138,7 +149,7 @@ export interface StarColumn {
|
|
|
138
149
|
|
|
139
150
|
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
|
|
140
151
|
|
|
141
|
-
export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM'
|
|
152
|
+
export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM' | 'REPLACE'
|
|
142
153
|
|
|
143
154
|
export interface AggregateArgStar {
|
|
144
155
|
kind: 'star'
|
|
@@ -192,7 +203,7 @@ export interface ExprCursor {
|
|
|
192
203
|
match(type: TokenType, value?: string): boolean
|
|
193
204
|
expect(type: TokenType, value: string): Token
|
|
194
205
|
expectIdentifier(): Token
|
|
195
|
-
parseSubquery
|
|
206
|
+
parseSubquery: () => SelectStatement
|
|
196
207
|
}
|
|
197
208
|
|
|
198
209
|
// Tokenizer types
|
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
|
}
|
package/src/backend/memory.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { DataSource, RowSource } from '../types.js'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a row accessor that wraps a plain JavaScript object
|
|
7
|
-
*
|
|
8
|
-
* @param {Record<string, any>} obj - the plain object
|
|
9
|
-
* @returns {RowSource} a row accessor interface
|
|
10
|
-
*/
|
|
11
|
-
export function createRowAccessor(obj) {
|
|
12
|
-
return {
|
|
13
|
-
getCell(name) {
|
|
14
|
-
return obj[name]
|
|
15
|
-
},
|
|
16
|
-
getKeys() {
|
|
17
|
-
return Object.keys(obj)
|
|
18
|
-
},
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Creates a memory-backed data source from an array of plain objects
|
|
24
|
-
*
|
|
25
|
-
* @param {Record<string, any>[]} data - array of plain objects
|
|
26
|
-
* @returns {DataSource} a data source interface
|
|
27
|
-
*/
|
|
28
|
-
export function createMemorySource(data) {
|
|
29
|
-
return {
|
|
30
|
-
getNumRows() {
|
|
31
|
-
return data.length
|
|
32
|
-
},
|
|
33
|
-
getRow(index) {
|
|
34
|
-
return createRowAccessor(data[index])
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
}
|