squirreling 0.3.1 → 0.4.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 +2 -2
- package/package.json +1 -1
- package/src/backend/dataSource.js +48 -15
- package/src/execute/aggregates.js +12 -34
- package/src/execute/execute.js +60 -87
- package/src/execute/expression.js +41 -21
- package/src/execute/having.js +4 -35
- package/src/execute/join.js +357 -0
- package/src/execute/utils.js +44 -6
- package/src/index.d.ts +4 -4
- package/src/parse/expression.js +4 -4
- package/src/parse/parse.js +59 -16
- package/src/types.d.ts +13 -8
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
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
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.
|
|
@@ -22,8 +22,8 @@ Squirreling is a streaming async SQL engine for JavaScript. It is designed to pr
|
|
|
22
22
|
- Constant memory usage for simple queries with LIMIT
|
|
23
23
|
- Robust error handling and validation designed for LLM tool use
|
|
24
24
|
- In-memory data option for simple use cases
|
|
25
|
+
- Late materialization for efficiency
|
|
25
26
|
- Select only
|
|
26
|
-
- No joins (yet)
|
|
27
27
|
|
|
28
28
|
## Usage
|
|
29
29
|
|
package/package.json
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { AsyncDataSource, AsyncRow } from '../types.js'
|
|
2
|
+
* @import { AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Wraps an async generator of plain objects into an AsyncDataSource
|
|
8
8
|
*
|
|
9
|
-
* @param {AsyncGenerator<
|
|
9
|
+
* @param {AsyncGenerator<AsyncRow>} gen
|
|
10
10
|
* @returns {AsyncDataSource}
|
|
11
11
|
*/
|
|
12
12
|
export function generatorSource(gen) {
|
|
13
13
|
return {
|
|
14
14
|
async *getRows() {
|
|
15
|
-
|
|
16
|
-
yield asyncRow(row)
|
|
17
|
-
}
|
|
15
|
+
yield* gen
|
|
18
16
|
},
|
|
19
17
|
}
|
|
20
18
|
}
|
|
@@ -22,24 +20,22 @@ export function generatorSource(gen) {
|
|
|
22
20
|
/**
|
|
23
21
|
* Creates an async row accessor that wraps a plain JavaScript object
|
|
24
22
|
*
|
|
25
|
-
* @param {Record<string,
|
|
23
|
+
* @param {Record<string, SqlPrimitive>} obj - the plain object
|
|
26
24
|
* @returns {AsyncRow} a row accessor interface
|
|
27
25
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
getKeys() {
|
|
34
|
-
return Object.keys(obj)
|
|
35
|
-
},
|
|
26
|
+
function asyncRow(obj) {
|
|
27
|
+
/** @type {AsyncRow} */
|
|
28
|
+
const row = {}
|
|
29
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
+
row[key] = () => Promise.resolve(value)
|
|
36
31
|
}
|
|
32
|
+
return row
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
/**
|
|
40
36
|
* Creates an async memory-backed data source from an array of plain objects
|
|
41
37
|
*
|
|
42
|
-
* @param {Record<string,
|
|
38
|
+
* @param {Record<string, SqlPrimitive>[]} data - array of plain objects
|
|
43
39
|
* @returns {AsyncDataSource} an async data source interface
|
|
44
40
|
*/
|
|
45
41
|
export function memorySource(data) {
|
|
@@ -51,3 +47,40 @@ export function memorySource(data) {
|
|
|
51
47
|
},
|
|
52
48
|
}
|
|
53
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wraps a data source that caches all accessed rows in memory
|
|
53
|
+
* @param {AsyncDataSource} source
|
|
54
|
+
* @returns {AsyncDataSource}
|
|
55
|
+
*/
|
|
56
|
+
export function cachedDataSource(source) {
|
|
57
|
+
/** @type {Map<string, Promise<SqlPrimitive>>} */
|
|
58
|
+
const cache = new Map()
|
|
59
|
+
return {
|
|
60
|
+
/**
|
|
61
|
+
* @yields {AsyncRow}
|
|
62
|
+
*/
|
|
63
|
+
async *getRows() {
|
|
64
|
+
let index = 0
|
|
65
|
+
for await (const row of source.getRows()) {
|
|
66
|
+
const rowIndex = index
|
|
67
|
+
/** @type {AsyncRow} */
|
|
68
|
+
const out = {}
|
|
69
|
+
for (const [key, cell] of Object.entries(row)) {
|
|
70
|
+
// Wrap the cell to cache accesses
|
|
71
|
+
out[key] = () => {
|
|
72
|
+
const cacheKey = `${rowIndex}:${key}`
|
|
73
|
+
let value = cache.get(cacheKey)
|
|
74
|
+
if (!value) {
|
|
75
|
+
value = cell()
|
|
76
|
+
cache.set(cacheKey, value)
|
|
77
|
+
}
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
yield out
|
|
82
|
+
index++
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { evaluateExpr } from './expression.js'
|
|
2
|
+
import { defaultDerivedAlias } from './utils.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Evaluates an aggregate function over a set of rows
|
|
5
6
|
*
|
|
6
|
-
* @import { AggregateColumn, ExprNode, AsyncRow } from '../types.js'
|
|
7
|
-
* @param {
|
|
8
|
-
* @param {
|
|
7
|
+
* @import { AggregateColumn, AsyncDataSource, ExprNode, AsyncRow } from '../types.js'
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {AggregateColumn} options.col - aggregate column definition
|
|
10
|
+
* @param {AsyncRow[]} options.rows - rows to aggregate
|
|
11
|
+
* @param {Record<string, AsyncDataSource>} options.tables
|
|
9
12
|
* @returns {Promise<number | null>} aggregated result
|
|
10
13
|
*/
|
|
11
|
-
export async function evaluateAggregate(col, rows) {
|
|
14
|
+
export async function evaluateAggregate({ col, rows, tables }) {
|
|
12
15
|
const { arg, func } = col
|
|
13
16
|
|
|
14
17
|
if (func === 'COUNT') {
|
|
15
18
|
if (arg.kind === 'star') return rows.length
|
|
16
19
|
let count = 0
|
|
17
|
-
for (
|
|
18
|
-
const v = await evaluateExpr({ node: arg.expr, row
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
19
22
|
if (v !== null && v !== undefined) {
|
|
20
23
|
count += 1
|
|
21
24
|
}
|
|
@@ -34,8 +37,8 @@ export async function evaluateAggregate(col, rows) {
|
|
|
34
37
|
/** @type {number | null} */
|
|
35
38
|
let max = null
|
|
36
39
|
|
|
37
|
-
for (
|
|
38
|
-
const raw = await evaluateExpr({ node: arg.expr, row
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
const raw = await evaluateExpr({ node: arg.expr, row, tables })
|
|
39
42
|
if (raw == null) continue
|
|
40
43
|
const num = Number(raw)
|
|
41
44
|
if (!Number.isFinite(num)) continue
|
|
@@ -70,30 +73,5 @@ export async function evaluateAggregate(col, rows) {
|
|
|
70
73
|
export function defaultAggregateAlias(col) {
|
|
71
74
|
const base = col.func.toLowerCase()
|
|
72
75
|
if (col.arg.kind === 'star') return base + '_all'
|
|
73
|
-
return base + '_' +
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @param {ExprNode} expr
|
|
78
|
-
* @returns {string}
|
|
79
|
-
*/
|
|
80
|
-
export function defaultAggregateAliasExpr(expr) {
|
|
81
|
-
if (expr.type === 'identifier') {
|
|
82
|
-
return expr.name
|
|
83
|
-
}
|
|
84
|
-
if (expr.type === 'literal') {
|
|
85
|
-
return String(expr.value)
|
|
86
|
-
}
|
|
87
|
-
if (expr.type === 'cast') {
|
|
88
|
-
return defaultAggregateAliasExpr(expr.expr) + '_as_' + expr.toType
|
|
89
|
-
}
|
|
90
|
-
if (expr.type === 'unary') {
|
|
91
|
-
return expr.op + '_' + defaultAggregateAliasExpr(expr.argument)
|
|
92
|
-
}
|
|
93
|
-
if (expr.type === 'binary') {
|
|
94
|
-
return defaultAggregateAliasExpr(expr.left) + '_' + expr.op + '_' + defaultAggregateAliasExpr(expr.right)
|
|
95
|
-
}
|
|
96
|
-
if (expr.type === 'function') {
|
|
97
|
-
return expr.name.toLowerCase() + '_' + expr.args.map(defaultAggregateAliasExpr).join('_')
|
|
98
|
-
}
|
|
76
|
+
return base + '_' + defaultDerivedAlias(col.arg.expr)
|
|
99
77
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generatorSource, memorySource } from '../backend/dataSource.js'
|
|
2
2
|
import { parseSql } from '../parse/parse.js'
|
|
3
|
-
import { asyncRow, generatorSource, memorySource } from '../backend/dataSource.js'
|
|
4
3
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
4
|
+
import { evaluateExpr } from './expression.js'
|
|
5
5
|
import { evaluateHavingExpr } from './having.js'
|
|
6
|
+
import { executeJoins } from './join.js'
|
|
7
|
+
import { defaultDerivedAlias } from './utils.js'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
|
|
@@ -12,15 +14,12 @@ import { evaluateHavingExpr } from './having.js'
|
|
|
12
14
|
* Executes a SQL SELECT query against named data sources
|
|
13
15
|
*
|
|
14
16
|
* @param {ExecuteSqlOptions} options - the execution options
|
|
15
|
-
* @
|
|
17
|
+
* @yields {AsyncRow} async generator yielding result rows
|
|
16
18
|
*/
|
|
17
19
|
export async function* executeSql({ tables, query }) {
|
|
18
20
|
const select = parseSql(query)
|
|
19
21
|
|
|
20
22
|
// Check for unsupported operations
|
|
21
|
-
if (select.joins.length) {
|
|
22
|
-
throw new Error('JOIN is not supported')
|
|
23
|
-
}
|
|
24
23
|
if (!select.from) {
|
|
25
24
|
throw new Error('FROM clause is required')
|
|
26
25
|
}
|
|
@@ -44,67 +43,47 @@ export async function* executeSql({ tables, query }) {
|
|
|
44
43
|
*
|
|
45
44
|
* @param {SelectStatement} select
|
|
46
45
|
* @param {Record<string, AsyncDataSource>} tables
|
|
47
|
-
* @
|
|
46
|
+
* @yields {AsyncRow}
|
|
48
47
|
*/
|
|
49
48
|
export async function* executeSelect(select, tables) {
|
|
50
49
|
/** @type {AsyncDataSource} */
|
|
51
50
|
let dataSource
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
/** @type {string} */
|
|
52
|
+
let fromTableName
|
|
53
|
+
|
|
54
|
+
if (select.from.kind === 'table') {
|
|
55
|
+
// Use alias for column prefixing, but look up the actual table name
|
|
56
|
+
fromTableName = select.from.alias ?? select.from.table
|
|
57
|
+
dataSource = tables[select.from.table]
|
|
58
|
+
if (dataSource === undefined) {
|
|
59
|
+
throw new Error(`Table "${select.from.table}" not found`)
|
|
57
60
|
}
|
|
58
|
-
|
|
59
|
-
dataSource = table
|
|
60
61
|
} else {
|
|
61
62
|
// Nested subquery - recursively resolve
|
|
63
|
+
fromTableName = select.from.alias
|
|
62
64
|
dataSource = generatorSource(executeSelect(select.from.query, tables))
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Generates a default alias for a derived column expression
|
|
70
|
-
*
|
|
71
|
-
* @param {ExprNode} expr - the expression node
|
|
72
|
-
* @returns {string} the generated alias
|
|
73
|
-
*/
|
|
74
|
-
function defaultDerivedAlias(expr) {
|
|
75
|
-
if (expr.type === 'identifier') {
|
|
76
|
-
return expr.name
|
|
77
|
-
}
|
|
78
|
-
if (expr.type === 'function') {
|
|
79
|
-
const base = expr.name.toLowerCase()
|
|
80
|
-
// Try to extract column names from identifier arguments
|
|
81
|
-
const columnNames = expr.args
|
|
82
|
-
.filter(arg => arg.type === 'identifier')
|
|
83
|
-
.map(arg => arg.name)
|
|
84
|
-
if (columnNames.length > 0) {
|
|
85
|
-
return base + '_' + columnNames.join('_')
|
|
86
|
-
}
|
|
87
|
-
return base
|
|
88
|
-
}
|
|
89
|
-
if (expr.type === 'cast') return 'cast_expr'
|
|
90
|
-
if (expr.type === 'unary' && expr.argument.type === 'identifier') {
|
|
91
|
-
return expr.op === '-' ? 'neg_' + expr.argument.name : 'expr'
|
|
67
|
+
// Execute JOINs if present
|
|
68
|
+
if (select.joins.length) {
|
|
69
|
+
dataSource = await executeJoins(dataSource, select.joins, fromTableName, tables)
|
|
92
70
|
}
|
|
93
|
-
|
|
71
|
+
|
|
72
|
+
yield* evaluateSelectAst(select, dataSource, tables)
|
|
94
73
|
}
|
|
95
74
|
|
|
96
75
|
/**
|
|
97
76
|
* Creates a stable string key for a row to enable deduplication
|
|
98
77
|
*
|
|
99
|
-
* @param {
|
|
100
|
-
* @returns {string} a stable string representation of the row
|
|
78
|
+
* @param {AsyncRow} row
|
|
79
|
+
* @returns {Promise<string>} a stable string representation of the row
|
|
101
80
|
*/
|
|
102
|
-
function stableRowKey(row) {
|
|
81
|
+
async function stableRowKey(row) {
|
|
103
82
|
const keys = Object.keys(row).sort()
|
|
104
83
|
/** @type {string[]} */
|
|
105
84
|
const parts = []
|
|
106
85
|
for (const k of keys) {
|
|
107
|
-
const v = row[k]
|
|
86
|
+
const v = await row[k]()
|
|
108
87
|
parts.push(k + ':' + JSON.stringify(v))
|
|
109
88
|
}
|
|
110
89
|
return parts.join('|')
|
|
@@ -128,28 +107,28 @@ function compareValues(a, b) {
|
|
|
128
107
|
return 0
|
|
129
108
|
}
|
|
130
109
|
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
if (
|
|
110
|
+
const aa = String(a)
|
|
111
|
+
const bb = String(b)
|
|
112
|
+
if (aa < bb) return -1
|
|
113
|
+
if (aa > bb) return 1
|
|
135
114
|
return 0
|
|
136
115
|
}
|
|
137
116
|
|
|
138
117
|
/**
|
|
139
118
|
* Applies DISTINCT filtering to remove duplicate rows
|
|
140
119
|
*
|
|
141
|
-
* @param {
|
|
142
|
-
* @param {boolean} distinct -
|
|
143
|
-
* @returns {
|
|
120
|
+
* @param {AsyncRow[]} rows - the input rows
|
|
121
|
+
* @param {boolean} distinct - whether to apply deduplication
|
|
122
|
+
* @returns {Promise<AsyncRow[]>} the deduplicated rows
|
|
144
123
|
*/
|
|
145
|
-
function applyDistinct(rows, distinct) {
|
|
124
|
+
async function applyDistinct(rows, distinct) {
|
|
146
125
|
if (!distinct) return rows
|
|
147
126
|
/** @type {Set<string>} */
|
|
148
127
|
const seen = new Set()
|
|
149
|
-
/** @type {
|
|
128
|
+
/** @type {AsyncRow[]} */
|
|
150
129
|
const result = []
|
|
151
130
|
for (const row of rows) {
|
|
152
|
-
const key = stableRowKey(row)
|
|
131
|
+
const key = await stableRowKey(row)
|
|
153
132
|
if (seen.has(key)) continue
|
|
154
133
|
seen.add(key)
|
|
155
134
|
result.push(row)
|
|
@@ -221,10 +200,10 @@ async function sortRowSources(rows, orderBy, tables) {
|
|
|
221
200
|
/**
|
|
222
201
|
* Applies ORDER BY sorting to rows
|
|
223
202
|
*
|
|
224
|
-
* @param {
|
|
203
|
+
* @param {AsyncRow[]} rows - the input rows
|
|
225
204
|
* @param {OrderByItem[]} orderBy - the sort specifications
|
|
226
205
|
* @param {Record<string, AsyncDataSource>} tables
|
|
227
|
-
* @returns {Promise<
|
|
206
|
+
* @returns {Promise<AsyncRow[]>} the sorted rows
|
|
228
207
|
*/
|
|
229
208
|
async function applyOrderBy(rows, orderBy, tables) {
|
|
230
209
|
if (!orderBy.length) return rows
|
|
@@ -236,7 +215,7 @@ async function applyOrderBy(rows, orderBy, tables) {
|
|
|
236
215
|
/** @type {SqlPrimitive[]} */
|
|
237
216
|
const rowValues = []
|
|
238
217
|
for (const term of orderBy) {
|
|
239
|
-
const value = await evaluateExpr({ node: term.expr, row
|
|
218
|
+
const value = await evaluateExpr({ node: term.expr, row, tables })
|
|
240
219
|
rowValues.push(value)
|
|
241
220
|
}
|
|
242
221
|
evaluatedValues.push(rowValues)
|
|
@@ -282,10 +261,10 @@ async function applyOrderBy(rows, orderBy, tables) {
|
|
|
282
261
|
/**
|
|
283
262
|
* Evaluates a select with a resolved FROM data source
|
|
284
263
|
*
|
|
285
|
-
* @param {SelectStatement} select
|
|
286
|
-
* @param {AsyncDataSource} dataSource
|
|
264
|
+
* @param {SelectStatement} select
|
|
265
|
+
* @param {AsyncDataSource} dataSource
|
|
287
266
|
* @param {Record<string, AsyncDataSource>} tables
|
|
288
|
-
* @
|
|
267
|
+
* @yields {AsyncRow}
|
|
289
268
|
*/
|
|
290
269
|
async function* evaluateSelectAst(select, dataSource, tables) {
|
|
291
270
|
// SQL priority: from, where, group by, having, select, order by, offset, limit
|
|
@@ -310,7 +289,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
|
|
|
310
289
|
* @param {SelectStatement} select
|
|
311
290
|
* @param {AsyncDataSource} dataSource
|
|
312
291
|
* @param {Record<string, AsyncDataSource>} tables
|
|
313
|
-
* @
|
|
292
|
+
* @yields {AsyncRow}
|
|
314
293
|
*/
|
|
315
294
|
async function* evaluateStreaming(select, dataSource, tables) {
|
|
316
295
|
let rowsYielded = 0
|
|
@@ -337,17 +316,16 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
337
316
|
}
|
|
338
317
|
|
|
339
318
|
// SELECT projection
|
|
340
|
-
/** @type {
|
|
319
|
+
/** @type {AsyncRow} */
|
|
341
320
|
const outRow = {}
|
|
342
321
|
for (const col of select.columns) {
|
|
343
322
|
if (col.kind === 'star') {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
outRow[key] = row.getCell(key)
|
|
323
|
+
for (const [key, cell] of Object.entries(row)) {
|
|
324
|
+
outRow[key] = cell
|
|
347
325
|
}
|
|
348
326
|
} else if (col.kind === 'derived') {
|
|
349
327
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
350
|
-
outRow[alias] =
|
|
328
|
+
outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
|
|
351
329
|
} else if (col.kind === 'aggregate') {
|
|
352
330
|
throw new Error(
|
|
353
331
|
'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
|
|
@@ -357,7 +335,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
357
335
|
|
|
358
336
|
// DISTINCT: skip duplicate rows
|
|
359
337
|
if (seen) {
|
|
360
|
-
const key = stableRowKey(outRow)
|
|
338
|
+
const key = await stableRowKey(outRow)
|
|
361
339
|
if (seen.has(key)) continue
|
|
362
340
|
seen.add(key)
|
|
363
341
|
// OFFSET applies to distinct rows
|
|
@@ -383,7 +361,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
383
361
|
* @param {Record<string, AsyncDataSource>} tables
|
|
384
362
|
* @param {boolean} hasAggregate
|
|
385
363
|
* @param {boolean} useGrouping
|
|
386
|
-
* @
|
|
364
|
+
* @yields {AsyncRow}
|
|
387
365
|
*/
|
|
388
366
|
async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
|
|
389
367
|
// Step 1: Collect all rows from data source
|
|
@@ -409,7 +387,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
409
387
|
}
|
|
410
388
|
|
|
411
389
|
// Step 3: Projection (grouping vs non-grouping)
|
|
412
|
-
/** @type {
|
|
390
|
+
/** @type {AsyncRow[]} */
|
|
413
391
|
let projected = []
|
|
414
392
|
|
|
415
393
|
if (useGrouping) {
|
|
@@ -446,15 +424,14 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
446
424
|
}
|
|
447
425
|
|
|
448
426
|
for (const group of groups) {
|
|
449
|
-
/** @type {
|
|
427
|
+
/** @type {AsyncRow} */
|
|
450
428
|
const resultRow = {}
|
|
451
429
|
for (const col of select.columns) {
|
|
452
430
|
if (col.kind === 'star') {
|
|
453
431
|
const firstRow = group[0]
|
|
454
432
|
if (firstRow) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
resultRow[key] = firstRow.getCell(key)
|
|
433
|
+
for (const [key, cell] of Object.entries(firstRow)) {
|
|
434
|
+
resultRow[key] = cell
|
|
458
435
|
}
|
|
459
436
|
}
|
|
460
437
|
continue
|
|
@@ -463,18 +440,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
463
440
|
if (col.kind === 'derived') {
|
|
464
441
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
465
442
|
if (group.length > 0) {
|
|
466
|
-
|
|
467
|
-
resultRow[alias] = value
|
|
443
|
+
resultRow[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
|
|
468
444
|
} else {
|
|
469
|
-
resultRow[alias]
|
|
445
|
+
delete resultRow[alias]
|
|
470
446
|
}
|
|
471
447
|
continue
|
|
472
448
|
}
|
|
473
449
|
|
|
474
450
|
if (col.kind === 'aggregate') {
|
|
475
451
|
const alias = col.alias ?? defaultAggregateAlias(col)
|
|
476
|
-
|
|
477
|
-
resultRow[alias] = value
|
|
452
|
+
resultRow[alias] = () => evaluateAggregate({ col, rows: group, tables })
|
|
478
453
|
continue
|
|
479
454
|
}
|
|
480
455
|
}
|
|
@@ -503,18 +478,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
503
478
|
}
|
|
504
479
|
|
|
505
480
|
for (const row of rowsToProject) {
|
|
506
|
-
/** @type {
|
|
481
|
+
/** @type {AsyncRow} */
|
|
507
482
|
const outRow = {}
|
|
508
483
|
for (const col of select.columns) {
|
|
509
484
|
if (col.kind === 'star') {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
outRow[key] = row.getCell(key)
|
|
485
|
+
for (const [key, cell] of Object.entries(row)) {
|
|
486
|
+
outRow[key] = cell
|
|
513
487
|
}
|
|
514
488
|
} else if (col.kind === 'derived') {
|
|
515
489
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
516
|
-
|
|
517
|
-
outRow[alias] = value
|
|
490
|
+
outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
|
|
518
491
|
}
|
|
519
492
|
}
|
|
520
493
|
projected.push(outRow)
|
|
@@ -522,7 +495,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
522
495
|
}
|
|
523
496
|
|
|
524
497
|
// Step 4: DISTINCT
|
|
525
|
-
projected = applyDistinct(projected, select.distinct)
|
|
498
|
+
projected = await applyDistinct(projected, select.distinct)
|
|
526
499
|
|
|
527
500
|
// Step 5: ORDER BY (final sort for grouped queries)
|
|
528
501
|
projected = await applyOrderBy(projected, select.orderBy, tables)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { executeSelect } from './execute.js'
|
|
2
|
-
import { collect } from './utils.js'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
|
|
@@ -11,7 +10,7 @@ import { collect } from './utils.js'
|
|
|
11
10
|
* @param {Object} params
|
|
12
11
|
* @param {ExprNode} params.node - The expression node to evaluate
|
|
13
12
|
* @param {AsyncRow} params.row - The data row to evaluate against
|
|
14
|
-
* @param {Record<string, AsyncDataSource>}
|
|
13
|
+
* @param {Record<string, AsyncDataSource>} params.tables
|
|
15
14
|
* @returns {Promise<SqlPrimitive>} The result of the evaluation
|
|
16
15
|
*/
|
|
17
16
|
export async function evaluateExpr({ node, row, tables }) {
|
|
@@ -20,17 +19,30 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
if (node.type === 'identifier') {
|
|
23
|
-
|
|
22
|
+
// Try exact match first (handles both qualified and unqualified names)
|
|
23
|
+
if (row[node.name]) {
|
|
24
|
+
return row[node.name]()
|
|
25
|
+
}
|
|
26
|
+
// For qualified names like 'users.id', also try just the column part
|
|
27
|
+
if (node.name.includes('.')) {
|
|
28
|
+
const colName = node.name.split('.').pop()
|
|
29
|
+
if (colName && row[colName]) {
|
|
30
|
+
return row[colName]()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
// Scalar subquery - returns a single value
|
|
27
37
|
if (node.type === 'subquery') {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
38
|
+
const gen = executeSelect(node.subquery, tables)
|
|
39
|
+
const first = await gen.next() // Start the generator
|
|
40
|
+
gen.return(undefined) // Stop further execution
|
|
41
|
+
if (!first.value) return null
|
|
42
|
+
/** @type {AsyncRow} */
|
|
43
|
+
const firstRow = first.value
|
|
32
44
|
const firstKey = Object.keys(firstRow)[0]
|
|
33
|
-
return firstRow[firstKey]
|
|
45
|
+
return firstRow[firstKey]()
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
// Unary operators
|
|
@@ -238,35 +250,43 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
238
250
|
// IN and NOT IN with subqueries
|
|
239
251
|
if (node.type === 'in') {
|
|
240
252
|
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
241
|
-
const results =
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
253
|
+
const results = executeSelect(node.subquery, tables)
|
|
254
|
+
/** @type {SqlPrimitive[]} */
|
|
255
|
+
const values = []
|
|
256
|
+
for await (const resRow of results) {
|
|
257
|
+
const firstKey = Object.keys(resRow)[0]
|
|
258
|
+
const val = await resRow[firstKey]()
|
|
259
|
+
values.push(val)
|
|
260
|
+
}
|
|
245
261
|
return values.includes(exprVal)
|
|
246
262
|
}
|
|
247
263
|
if (node.type === 'not in') {
|
|
248
264
|
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
249
|
-
const results =
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
const
|
|
265
|
+
const results = executeSelect(node.subquery, tables)
|
|
266
|
+
/** @type {SqlPrimitive[]} */
|
|
267
|
+
const values = []
|
|
268
|
+
for await (const resRow of results) {
|
|
269
|
+
const firstKey = Object.keys(resRow)[0]
|
|
270
|
+
const val = await resRow[firstKey]()
|
|
271
|
+
values.push(val)
|
|
272
|
+
}
|
|
253
273
|
return !values.includes(exprVal)
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
// EXISTS and NOT EXISTS with subqueries
|
|
257
277
|
if (node.type === 'exists') {
|
|
258
|
-
const results = await
|
|
259
|
-
return results.
|
|
278
|
+
const results = await executeSelect(node.subquery, tables).next()
|
|
279
|
+
return results.done === false
|
|
260
280
|
}
|
|
261
281
|
if (node.type === 'not exists') {
|
|
262
|
-
const results = await
|
|
263
|
-
return results.
|
|
282
|
+
const results = await executeSelect(node.subquery, tables).next()
|
|
283
|
+
return results.done === true
|
|
264
284
|
}
|
|
265
285
|
|
|
266
286
|
// CASE expressions
|
|
267
287
|
if (node.type === 'case') {
|
|
268
288
|
// For simple CASE: evaluate the case expression once
|
|
269
|
-
const caseValue = node.caseExpr
|
|
289
|
+
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
|
|
270
290
|
|
|
271
291
|
// Iterate through WHEN clauses
|
|
272
292
|
for (const whenClause of node.whenClauses) {
|