squirreling 0.4.0 → 0.4.2
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 +3 -3
- package/src/backend/dataSource.js +1 -1
- package/src/execute/aggregates.js +2 -23
- package/src/execute/execute.js +92 -174
- package/src/execute/expression.js +21 -41
- package/src/execute/having.js +4 -34
- package/src/execute/join.js +357 -0
- package/src/execute/utils.js +70 -1
- package/src/index.d.ts +1 -0
- package/src/parse/expression.js +49 -20
- package/src/parse/parse.js +59 -16
- package/src/types.d.ts +11 -12
- package/src/validation.js +1 -1
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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.15",
|
|
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.15"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
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
|
|
@@ -72,27 +73,5 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
72
73
|
export function defaultAggregateAlias(col) {
|
|
73
74
|
const base = col.func.toLowerCase()
|
|
74
75
|
if (col.arg.kind === 'star') return base + '_all'
|
|
75
|
-
return base + '_' +
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {ExprNode} expr
|
|
80
|
-
* @returns {string}
|
|
81
|
-
*/
|
|
82
|
-
export function defaultAggregateAliasExpr(expr) {
|
|
83
|
-
if (expr.type === 'identifier') {
|
|
84
|
-
return expr.name
|
|
85
|
-
} else if (expr.type === 'literal') {
|
|
86
|
-
return String(expr.value)
|
|
87
|
-
} else if (expr.type === 'cast') {
|
|
88
|
-
return defaultAggregateAliasExpr(expr.expr) + '_as_' + expr.toType
|
|
89
|
-
} else if (expr.type === 'unary') {
|
|
90
|
-
return expr.op + '_' + defaultAggregateAliasExpr(expr.argument)
|
|
91
|
-
} else if (expr.type === 'binary') {
|
|
92
|
-
return defaultAggregateAliasExpr(expr.left) + '_' + expr.op + '_' + defaultAggregateAliasExpr(expr.right)
|
|
93
|
-
} else if (expr.type === 'function') {
|
|
94
|
-
return expr.name.toLowerCase() + '_' + expr.args.map(defaultAggregateAliasExpr).join('_')
|
|
95
|
-
} else {
|
|
96
|
-
return 'expr'
|
|
97
|
-
}
|
|
76
|
+
return base + '_' + defaultDerivedAlias(col.arg.expr)
|
|
98
77
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { evaluateExpr } from './expression.js'
|
|
2
|
-
import { parseSql } from '../parse/parse.js'
|
|
3
1
|
import { generatorSource, memorySource } from '../backend/dataSource.js'
|
|
2
|
+
import { parseSql } from '../parse/parse.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 { compareForTerm, 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,53 +43,33 @@ 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
|
/**
|
|
@@ -110,31 +89,6 @@ async function stableRowKey(row) {
|
|
|
110
89
|
return parts.join('|')
|
|
111
90
|
}
|
|
112
91
|
|
|
113
|
-
/**
|
|
114
|
-
* Compares two SQL values for sorting
|
|
115
|
-
*
|
|
116
|
-
* @param {SqlPrimitive} a
|
|
117
|
-
* @param {SqlPrimitive} b
|
|
118
|
-
* @returns {number} negative if a < b, positive if a > b, 0 if equal
|
|
119
|
-
*/
|
|
120
|
-
function compareValues(a, b) {
|
|
121
|
-
if (a === b) return 0
|
|
122
|
-
if (a == null) return -1
|
|
123
|
-
if (b == null) return 1
|
|
124
|
-
|
|
125
|
-
if (typeof a === 'number' && typeof b === 'number') {
|
|
126
|
-
if (a < b) return -1
|
|
127
|
-
if (a > b) return 1
|
|
128
|
-
return 0
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const as = String(a)
|
|
132
|
-
const bs = String(b)
|
|
133
|
-
if (as < bs) return -1
|
|
134
|
-
if (as > bs) return 1
|
|
135
|
-
return 0
|
|
136
|
-
}
|
|
137
|
-
|
|
138
92
|
/**
|
|
139
93
|
* Applies DISTINCT filtering to remove duplicate rows
|
|
140
94
|
*
|
|
@@ -156,127 +110,89 @@ async function applyDistinct(rows, distinct) {
|
|
|
156
110
|
}
|
|
157
111
|
return result
|
|
158
112
|
}
|
|
159
|
-
|
|
160
113
|
/**
|
|
161
|
-
* Applies ORDER BY sorting to
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Applies ORDER BY sorting to rows
|
|
114
|
+
* Applies ORDER BY sorting to rows using multi-pass lazy evaluation.
|
|
115
|
+
* Secondary ORDER BY columns are only evaluated for rows that tie on
|
|
116
|
+
* previous columns, reducing expensive cell evaluations.
|
|
223
117
|
*
|
|
224
118
|
* @param {AsyncRow[]} rows - the input rows
|
|
225
119
|
* @param {OrderByItem[]} orderBy - the sort specifications
|
|
226
120
|
* @param {Record<string, AsyncDataSource>} tables
|
|
227
121
|
* @returns {Promise<AsyncRow[]>} the sorted rows
|
|
228
122
|
*/
|
|
229
|
-
async function
|
|
123
|
+
async function sortRows(rows, orderBy, tables) {
|
|
230
124
|
if (!orderBy.length) return rows
|
|
231
125
|
|
|
232
|
-
//
|
|
233
|
-
/** @type {SqlPrimitive[][]} */
|
|
234
|
-
const evaluatedValues =
|
|
235
|
-
for (const row of rows) {
|
|
236
|
-
/** @type {SqlPrimitive[]} */
|
|
237
|
-
const rowValues = []
|
|
238
|
-
for (const term of orderBy) {
|
|
239
|
-
const value = await evaluateExpr({ node: term.expr, 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]
|
|
250
|
-
const dir = term.direction
|
|
251
|
-
const av = evaluatedValues[aIdx][termIdx]
|
|
252
|
-
const bv = evaluatedValues[bIdx][termIdx]
|
|
126
|
+
// Cache for evaluated values: evaluatedValues[rowIdx][colIdx]
|
|
127
|
+
/** @type {(SqlPrimitive | undefined)[][]} */
|
|
128
|
+
const evaluatedValues = rows.map(() => Array(orderBy.length))
|
|
253
129
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
130
|
+
// Start with all indices in one group
|
|
131
|
+
/** @type {number[][]} */
|
|
132
|
+
let groups = [rows.map((_, i) => i)]
|
|
257
133
|
|
|
258
|
-
|
|
259
|
-
|
|
134
|
+
// Process each ORDER BY column incrementally
|
|
135
|
+
for (let orderByIdx = 0; orderByIdx < orderBy.length; orderByIdx++) {
|
|
136
|
+
const term = orderBy[orderByIdx]
|
|
137
|
+
/** @type {number[][]} */
|
|
138
|
+
const nextGroups = []
|
|
260
139
|
|
|
261
|
-
|
|
140
|
+
for (const group of groups) {
|
|
141
|
+
// Single-element groups don't need sorting or evaluation
|
|
142
|
+
if (group.length <= 1) {
|
|
143
|
+
nextGroups.push(group)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
262
146
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
147
|
+
// Evaluate this column for all rows in the group
|
|
148
|
+
for (const idx of group) {
|
|
149
|
+
if (evaluatedValues[idx][orderByIdx] === undefined) {
|
|
150
|
+
evaluatedValues[idx][orderByIdx] = await evaluateExpr({
|
|
151
|
+
node: term.expr,
|
|
152
|
+
row: rows[idx],
|
|
153
|
+
tables,
|
|
154
|
+
})
|
|
267
155
|
}
|
|
268
156
|
}
|
|
269
157
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
158
|
+
// Sort the group by this column
|
|
159
|
+
group.sort((aIdx, bIdx) => {
|
|
160
|
+
const av = evaluatedValues[aIdx][orderByIdx]
|
|
161
|
+
const bv = evaluatedValues[bIdx][orderByIdx]
|
|
162
|
+
return compareForTerm(av, bv, term)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Split into sub-groups based on ties (for next column)
|
|
166
|
+
if (orderByIdx < orderBy.length - 1) {
|
|
167
|
+
/** @type {number[]} */
|
|
168
|
+
let currentSubGroup = [group[0]]
|
|
169
|
+
for (let i = 1; i < group.length; i++) {
|
|
170
|
+
const prevIdx = group[i - 1]
|
|
171
|
+
const currIdx = group[i]
|
|
172
|
+
const prevVal = evaluatedValues[prevIdx][orderByIdx]
|
|
173
|
+
const currVal = evaluatedValues[currIdx][orderByIdx]
|
|
174
|
+
|
|
175
|
+
if (compareForTerm(prevVal, currVal, term) === 0) {
|
|
176
|
+
// Same value, extend current sub-group
|
|
177
|
+
currentSubGroup.push(currIdx)
|
|
178
|
+
} else {
|
|
179
|
+
// Different value, start new sub-group
|
|
180
|
+
nextGroups.push(currentSubGroup)
|
|
181
|
+
currentSubGroup = [currIdx]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
nextGroups.push(currentSubGroup)
|
|
185
|
+
} else {
|
|
186
|
+
// Last column, no need to split
|
|
187
|
+
nextGroups.push(group)
|
|
273
188
|
}
|
|
274
189
|
}
|
|
275
|
-
return 0
|
|
276
|
-
})
|
|
277
190
|
|
|
278
|
-
|
|
279
|
-
|
|
191
|
+
groups = nextGroups
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Flatten groups to get final sorted indices
|
|
195
|
+
return groups.flat().map(i => rows[i])
|
|
280
196
|
}
|
|
281
197
|
|
|
282
198
|
/**
|
|
@@ -285,7 +201,7 @@ async function applyOrderBy(rows, orderBy, tables) {
|
|
|
285
201
|
* @param {SelectStatement} select
|
|
286
202
|
* @param {AsyncDataSource} dataSource
|
|
287
203
|
* @param {Record<string, AsyncDataSource>} tables
|
|
288
|
-
* @
|
|
204
|
+
* @yields {AsyncRow}
|
|
289
205
|
*/
|
|
290
206
|
async function* evaluateSelectAst(select, dataSource, tables) {
|
|
291
207
|
// SQL priority: from, where, group by, having, select, order by, offset, limit
|
|
@@ -310,7 +226,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
|
|
|
310
226
|
* @param {SelectStatement} select
|
|
311
227
|
* @param {AsyncDataSource} dataSource
|
|
312
228
|
* @param {Record<string, AsyncDataSource>} tables
|
|
313
|
-
* @
|
|
229
|
+
* @yields {AsyncRow}
|
|
314
230
|
*/
|
|
315
231
|
async function* evaluateStreaming(select, dataSource, tables) {
|
|
316
232
|
let rowsYielded = 0
|
|
@@ -382,7 +298,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
382
298
|
* @param {Record<string, AsyncDataSource>} tables
|
|
383
299
|
* @param {boolean} hasAggregate
|
|
384
300
|
* @param {boolean} useGrouping
|
|
385
|
-
* @
|
|
301
|
+
* @yields {AsyncRow}
|
|
386
302
|
*/
|
|
387
303
|
async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
|
|
388
304
|
// Step 1: Collect all rows from data source
|
|
@@ -487,7 +403,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
487
403
|
} else {
|
|
488
404
|
// No grouping, simple projection
|
|
489
405
|
// Sort before projection so ORDER BY can access columns not in SELECT
|
|
490
|
-
const sorted = await
|
|
406
|
+
const sorted = await sortRows(filtered, select.orderBy, tables)
|
|
491
407
|
|
|
492
408
|
// OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
|
|
493
409
|
// to avoid reading expensive cells for rows that won't be in the final result
|
|
@@ -519,7 +435,9 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
519
435
|
projected = await applyDistinct(projected, select.distinct)
|
|
520
436
|
|
|
521
437
|
// Step 5: ORDER BY (final sort for grouped queries)
|
|
522
|
-
|
|
438
|
+
if (useGrouping) {
|
|
439
|
+
projected = await sortRows(projected, select.orderBy, tables)
|
|
440
|
+
}
|
|
523
441
|
|
|
524
442
|
// Step 6: OFFSET and LIMIT
|
|
525
443
|
// For non-DISTINCT, non-grouping queries, OFFSET/LIMIT was already applied before projection
|
|
@@ -19,15 +19,26 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
if (node.type === 'identifier') {
|
|
22
|
-
|
|
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
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
// Scalar subquery - returns a single value
|
|
26
37
|
if (node.type === 'subquery') {
|
|
27
38
|
const gen = executeSelect(node.subquery, tables)
|
|
28
39
|
const first = await gen.next() // Start the generator
|
|
29
|
-
gen.return() // Stop further execution
|
|
30
|
-
if (first.
|
|
40
|
+
gen.return(undefined) // Stop further execution
|
|
41
|
+
if (!first.value) return null
|
|
31
42
|
/** @type {AsyncRow} */
|
|
32
43
|
const firstRow = first.value
|
|
33
44
|
const firstKey = Object.keys(firstRow)[0]
|
|
@@ -99,21 +110,6 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
// BETWEEN and NOT BETWEEN
|
|
103
|
-
if (node.type === 'between' || node.type === 'not between') {
|
|
104
|
-
const expr = await evaluateExpr({ node: node.expr, row, tables })
|
|
105
|
-
const lower = await evaluateExpr({ node: node.lower, row, tables })
|
|
106
|
-
const upper = await evaluateExpr({ node: node.upper, row, tables })
|
|
107
|
-
|
|
108
|
-
// If any value is NULL, return false (SQL behavior)
|
|
109
|
-
if (expr == null || lower == null || upper == null) {
|
|
110
|
-
return false
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const isBetween = expr >= lower && expr <= upper
|
|
114
|
-
return node.type === 'between' ? isBetween : !isBetween
|
|
115
|
-
}
|
|
116
|
-
|
|
117
113
|
// Function calls
|
|
118
114
|
if (node.type === 'function') {
|
|
119
115
|
const funcName = node.name.toUpperCase()
|
|
@@ -189,6 +185,11 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
189
185
|
return String(str).replaceAll(String(searchStr), String(replaceStr))
|
|
190
186
|
}
|
|
191
187
|
|
|
188
|
+
if (funcName === 'RANDOM' || funcName === 'RAND') {
|
|
189
|
+
if (args.length !== 0) throw new Error(`${funcName} takes no arguments`)
|
|
190
|
+
return Math.random()
|
|
191
|
+
}
|
|
192
|
+
|
|
192
193
|
throw new Error('Unsupported function ' + funcName)
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -227,16 +228,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
227
228
|
}
|
|
228
229
|
return false
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
232
|
-
for (const valueNode of node.values) {
|
|
233
|
-
const val = await evaluateExpr({ node: valueNode, row, tables })
|
|
234
|
-
if (exprVal === val) return false
|
|
235
|
-
}
|
|
236
|
-
return true
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// IN and NOT IN with subqueries
|
|
231
|
+
// IN with subqueries
|
|
240
232
|
if (node.type === 'in') {
|
|
241
233
|
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
242
234
|
const results = executeSelect(node.subquery, tables)
|
|
@@ -249,18 +241,6 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
249
241
|
}
|
|
250
242
|
return values.includes(exprVal)
|
|
251
243
|
}
|
|
252
|
-
if (node.type === 'not in') {
|
|
253
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
254
|
-
const results = executeSelect(node.subquery, tables)
|
|
255
|
-
/** @type {SqlPrimitive[]} */
|
|
256
|
-
const values = []
|
|
257
|
-
for await (const resRow of results) {
|
|
258
|
-
const firstKey = Object.keys(resRow)[0]
|
|
259
|
-
const val = await resRow[firstKey]()
|
|
260
|
-
values.push(val)
|
|
261
|
-
}
|
|
262
|
-
return !values.includes(exprVal)
|
|
263
|
-
}
|
|
264
244
|
|
|
265
245
|
// EXISTS and NOT EXISTS with subqueries
|
|
266
246
|
if (node.type === 'exists') {
|
|
@@ -275,7 +255,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
275
255
|
// CASE expressions
|
|
276
256
|
if (node.type === 'case') {
|
|
277
257
|
// For simple CASE: evaluate the case expression once
|
|
278
|
-
const caseValue = node.caseExpr
|
|
258
|
+
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
|
|
279
259
|
|
|
280
260
|
// Iterate through WHEN clauses
|
|
281
261
|
for (const whenClause of node.whenClauses) {
|
package/src/execute/having.js
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import { isAggregateFunc } from '../validation.js'
|
|
6
2
|
import { evaluateExpr } from './expression.js'
|
|
7
3
|
|
|
8
4
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param {AsyncRow} resultRow - the aggregated result row
|
|
12
|
-
* @param {AsyncRow[]} group - the group of rows
|
|
13
|
-
* @returns {AsyncRow} a context row for HAVING evaluation
|
|
5
|
+
* @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
|
|
14
6
|
*/
|
|
15
|
-
function createHavingContext(resultRow, group) {
|
|
16
|
-
// Include the first row of the group (for GROUP BY columns)
|
|
17
|
-
const firstRow = group[0]
|
|
18
|
-
if (firstRow) {
|
|
19
|
-
return { ...firstRow, ...resultRow }
|
|
20
|
-
} else {
|
|
21
|
-
return resultRow
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
7
|
|
|
25
8
|
/**
|
|
26
9
|
* Evaluates a HAVING expression with support for aggregate functions
|
|
@@ -32,7 +15,8 @@ function createHavingContext(resultRow, group) {
|
|
|
32
15
|
* @returns {Promise<boolean>} whether the HAVING condition is satisfied
|
|
33
16
|
*/
|
|
34
17
|
export async function evaluateHavingExpr(expr, row, group, tables) {
|
|
35
|
-
|
|
18
|
+
// Having context
|
|
19
|
+
const context = { ...group[0] ?? {}, ...row }
|
|
36
20
|
|
|
37
21
|
// For HAVING, we need special handling of aggregate functions
|
|
38
22
|
// They need to be re-evaluated against the group
|
|
@@ -92,20 +76,6 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
|
|
|
92
76
|
}
|
|
93
77
|
}
|
|
94
78
|
|
|
95
|
-
if (expr.type === 'between' || expr.type === 'not between') {
|
|
96
|
-
const exprVal = await evaluateHavingValue(expr.expr, context, group, tables)
|
|
97
|
-
const lower = await evaluateHavingValue(expr.lower, context, group, tables)
|
|
98
|
-
const upper = await evaluateHavingValue(expr.upper, context, group, tables)
|
|
99
|
-
|
|
100
|
-
// If any value is NULL, return false (SQL behavior)
|
|
101
|
-
if (exprVal == null || lower == null || upper == null) {
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const isBetween = exprVal >= lower && exprVal <= upper
|
|
106
|
-
return expr.type === 'between' ? isBetween : !isBetween
|
|
107
|
-
}
|
|
108
|
-
|
|
109
79
|
// For other expression types, use the context row
|
|
110
80
|
return Boolean(await evaluateExpr({ node: expr, row: context, tables }))
|
|
111
81
|
}
|
|
@@ -128,7 +98,7 @@ function evaluateHavingValue(expr, context, group, tables) {
|
|
|
128
98
|
}
|
|
129
99
|
|
|
130
100
|
// For binary expressions, we need to use evaluateHavingExpr to properly handle aggregates
|
|
131
|
-
if (expr.type === 'binary' || expr.type === 'unary'
|
|
101
|
+
if (expr.type === 'binary' || expr.type === 'unary') {
|
|
132
102
|
return evaluateHavingExpr(expr, context, group, tables)
|
|
133
103
|
}
|
|
134
104
|
|