squirreling 0.7.4 → 0.7.6
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/package.json +3 -3
- package/src/execute/execute.js +14 -3
- package/src/execute/expression.js +26 -14
- package/src/execute/having.js +1 -1
- package/src/execute/utils.js +1 -1
- package/src/executionErrors.js +21 -4
- package/src/parse/parse.js +22 -19
- package/src/types.d.ts +1 -1
- package/src/validation.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Squirreling SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.0.
|
|
40
|
+
"@types/node": "25.0.6",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.16",
|
|
42
42
|
"eslint": "9.39.2",
|
|
43
|
-
"eslint-plugin-jsdoc": "
|
|
43
|
+
"eslint-plugin-jsdoc": "62.0.0",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
45
|
"vitest": "4.0.16"
|
|
46
46
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveTableSource } from './tableSource.js'
|
|
|
10
10
|
import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @import { AsyncCells, AsyncDataSource, AsyncRow,
|
|
13
|
+
* @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, ExprNode, OrderByItem, QueryHints, SelectColumn, SelectStatement, SqlPrimitive, UserDefinedFunction, WithClause } from '../types.js'
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -143,9 +143,10 @@ async function applyDistinct(rows, distinct) {
|
|
|
143
143
|
* @param {OrderByItem[]} options.orderBy - the sort specifications
|
|
144
144
|
* @param {Record<string, AsyncDataSource>} options.tables
|
|
145
145
|
* @param {Record<string, UserDefinedFunction>} [options.functions]
|
|
146
|
+
* @param {Map<string, ExprNode>} [options.aliases] - SELECT column aliases for ORDER BY resolution
|
|
146
147
|
* @returns {Promise<AsyncRow[]>} the sorted rows
|
|
147
148
|
*/
|
|
148
|
-
async function sortRows({ rows, orderBy, tables, functions }) {
|
|
149
|
+
async function sortRows({ rows, orderBy, tables, functions, aliases }) {
|
|
149
150
|
if (!orderBy.length) return rows
|
|
150
151
|
|
|
151
152
|
// Cache for evaluated values: evaluatedValues[rowIdx][colIdx]
|
|
@@ -177,6 +178,7 @@ async function sortRows({ rows, orderBy, tables, functions }) {
|
|
|
177
178
|
row: rows[idx],
|
|
178
179
|
tables,
|
|
179
180
|
functions,
|
|
181
|
+
aliases,
|
|
180
182
|
})
|
|
181
183
|
}
|
|
182
184
|
}
|
|
@@ -459,7 +461,16 @@ async function* evaluateBuffered({ select, dataSource, tables, functions, hasAgg
|
|
|
459
461
|
} else {
|
|
460
462
|
// No grouping, simple projection
|
|
461
463
|
// Sort before projection so ORDER BY can access columns not in SELECT
|
|
462
|
-
|
|
464
|
+
|
|
465
|
+
// Pass aliases so ORDER BY can reference SELECT column aliases
|
|
466
|
+
/** @type {Map<string, ExprNode>} */
|
|
467
|
+
const aliases = new Map()
|
|
468
|
+
for (const col of select.columns) {
|
|
469
|
+
if (col.kind === 'derived' && col.alias) {
|
|
470
|
+
aliases.set(col.alias, col.expr)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const sorted = await sortRows({ rows: filtered, orderBy: select.orderBy, tables, functions, aliases })
|
|
463
474
|
|
|
464
475
|
// OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
|
|
465
476
|
// to avoid reading expensive cells for rows that won't be in the final result
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
|
-
import { invalidContextError } from '../executionErrors.js'
|
|
2
|
+
import { columnNotFoundError, invalidContextError } from '../executionErrors.js'
|
|
3
3
|
import { aggregateError, argValueError, castError } from '../validationErrors.js'
|
|
4
4
|
import { isAggregateFunc, isMathFunc, isRegexpFunc, isStringFunc } from '../validation.js'
|
|
5
5
|
import { applyIntervalToDate } from './date.js'
|
|
@@ -23,26 +23,38 @@ import { applyBinaryOp, stringify } from './utils.js'
|
|
|
23
23
|
* @param {Record<string, UserDefinedFunction>} [params.functions] - User-defined functions
|
|
24
24
|
* @param {number} [params.rowIndex] - 1-based row index for error reporting
|
|
25
25
|
* @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
|
|
26
|
+
* @param {Map<string, ExprNode>} [params.aliases] - SELECT column aliases for ORDER BY resolution
|
|
26
27
|
* @returns {Promise<SqlPrimitive>} The result of the evaluation
|
|
27
28
|
*/
|
|
28
|
-
export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows }) {
|
|
29
|
+
export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases }) {
|
|
29
30
|
if (node.type === 'literal') {
|
|
30
31
|
return node.value
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
if (node.type === 'identifier') {
|
|
34
35
|
// Try exact match first (handles both qualified and unqualified names)
|
|
35
|
-
if (row.cells
|
|
36
|
+
if (node.name in row.cells) {
|
|
36
37
|
return row.cells[node.name]()
|
|
37
38
|
}
|
|
38
39
|
// For qualified names like 'users.id', also try just the column part
|
|
39
40
|
if (node.name.includes('.')) {
|
|
40
41
|
const colName = node.name.split('.').pop()
|
|
41
|
-
if (colName && row.cells
|
|
42
|
+
if (colName && colName in row.cells) {
|
|
42
43
|
return row.cells[colName]()
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
// Check if it's a SELECT alias (for ORDER BY)
|
|
47
|
+
if (aliases?.has(node.name)) {
|
|
48
|
+
return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases })
|
|
49
|
+
}
|
|
50
|
+
// Unknown identifier
|
|
51
|
+
throw columnNotFoundError({
|
|
52
|
+
columnName: node.name,
|
|
53
|
+
availableColumns: Object.keys(row.cells),
|
|
54
|
+
positionStart: node.positionStart,
|
|
55
|
+
positionEnd: node.positionEnd,
|
|
56
|
+
rowNumber: rowIndex,
|
|
57
|
+
})
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
// Scalar subquery - returns a single value
|
|
@@ -57,16 +69,16 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
57
69
|
// Unary operators
|
|
58
70
|
if (node.type === 'unary') {
|
|
59
71
|
if (node.op === 'NOT') {
|
|
60
|
-
return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
|
|
72
|
+
return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
|
|
61
73
|
}
|
|
62
74
|
if (node.op === 'IS NULL') {
|
|
63
|
-
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) == null
|
|
75
|
+
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) == null
|
|
64
76
|
}
|
|
65
77
|
if (node.op === 'IS NOT NULL') {
|
|
66
|
-
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) != null
|
|
78
|
+
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) != null
|
|
67
79
|
}
|
|
68
80
|
if (node.op === '-') {
|
|
69
|
-
const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
|
|
81
|
+
const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
|
|
70
82
|
if (val == null) return null
|
|
71
83
|
return -val
|
|
72
84
|
}
|
|
@@ -76,15 +88,15 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
76
88
|
if (node.type === 'binary') {
|
|
77
89
|
// Handle date +/- interval at AST level
|
|
78
90
|
if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
|
|
79
|
-
const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
|
|
91
|
+
const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
|
|
80
92
|
return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
|
|
81
93
|
}
|
|
82
94
|
if (node.op === '+' && node.left.type === 'interval') {
|
|
83
|
-
const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
|
|
95
|
+
const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
|
|
84
96
|
return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
|
|
99
|
+
const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
|
|
88
100
|
|
|
89
101
|
// Short-circuit evaluation for AND and OR
|
|
90
102
|
if (node.op === 'AND') {
|
|
@@ -94,7 +106,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
94
106
|
if (left) return true
|
|
95
107
|
}
|
|
96
108
|
|
|
97
|
-
const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
|
|
109
|
+
const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
|
|
98
110
|
return applyBinaryOp(node.op, left, right)
|
|
99
111
|
}
|
|
100
112
|
|
|
@@ -196,7 +208,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
/** @type {SqlPrimitive[]} */
|
|
199
|
-
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows })))
|
|
211
|
+
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases })))
|
|
200
212
|
|
|
201
213
|
if (isStringFunc(funcName)) {
|
|
202
214
|
return evaluateStringFunc({
|
package/src/execute/having.js
CHANGED
|
@@ -20,7 +20,7 @@ import { applyBinaryOp } from './utils.js'
|
|
|
20
20
|
*/
|
|
21
21
|
export async function evaluateHavingExpr({ expr, row, group, tables, functions }) {
|
|
22
22
|
// Having context
|
|
23
|
-
const context = { ...group[0]
|
|
23
|
+
const context = { ...group[0], ...row }
|
|
24
24
|
|
|
25
25
|
// For HAVING, we need special handling of aggregate functions
|
|
26
26
|
// They need to be re-evaluated against the group
|
package/src/execute/utils.js
CHANGED
package/src/executionErrors.js
CHANGED
|
@@ -24,8 +24,6 @@ export class ExecutionError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Error for missing table.
|
|
28
|
-
*
|
|
29
27
|
* @param {Object} options
|
|
30
28
|
* @param {string} options.tableName - The missing table name
|
|
31
29
|
* @returns {Error}
|
|
@@ -50,8 +48,6 @@ export function invalidContextError({ item, validContext, positionStart, positio
|
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
/**
|
|
53
|
-
* Error for unsupported operation combinations.
|
|
54
|
-
*
|
|
55
51
|
* @param {Object} options
|
|
56
52
|
* @param {string} options.operation - The unsupported operation
|
|
57
53
|
* @param {string} [options.hint] - How to fix it
|
|
@@ -61,3 +57,24 @@ export function unsupportedOperationError({ operation, hint }) {
|
|
|
61
57
|
const suffix = hint ? `. ${hint}` : ''
|
|
62
58
|
return new Error(`${operation}${suffix}`)
|
|
63
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Object} options
|
|
63
|
+
* @param {string} options.columnName - The missing column name
|
|
64
|
+
* @param {string[]} options.availableColumns - List of available column names
|
|
65
|
+
* @param {number} options.positionStart - Start position in query
|
|
66
|
+
* @param {number} options.positionEnd - End position in query
|
|
67
|
+
* @param {number} [options.rowNumber] - 1-based row number where error occurred
|
|
68
|
+
* @returns {ExecutionError}
|
|
69
|
+
*/
|
|
70
|
+
export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd, rowNumber }) {
|
|
71
|
+
const available = availableColumns.length > 0
|
|
72
|
+
? `. Available columns: ${availableColumns.join(', ')}`
|
|
73
|
+
: ''
|
|
74
|
+
return new ExecutionError({
|
|
75
|
+
message: `Column "${columnName}" not found${available}`,
|
|
76
|
+
positionStart,
|
|
77
|
+
positionEnd,
|
|
78
|
+
rowNumber,
|
|
79
|
+
})
|
|
80
|
+
}
|
package/src/parse/parse.js
CHANGED
|
@@ -97,29 +97,32 @@ export function parseSql({ query, functions }) {
|
|
|
97
97
|
function parseSelectList(state) {
|
|
98
98
|
/** @type {SelectColumn[]} */
|
|
99
99
|
const cols = []
|
|
100
|
-
const tok = current(state)
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
while (true) {
|
|
102
|
+
const tok = current(state)
|
|
103
|
+
|
|
104
|
+
// Check for qualified asterisk (table.*)
|
|
105
|
+
if (tok.type === 'identifier') {
|
|
106
|
+
const next = peekToken(state, 1)
|
|
107
|
+
const nextNext = peekToken(state, 2)
|
|
108
|
+
if (next.type === 'dot' && nextNext.type === 'operator' && nextNext.value === '*') {
|
|
109
|
+
const tableTok = consume(state) // consume table name
|
|
110
|
+
consume(state) // consume dot
|
|
111
|
+
consume(state) // consume asterisk
|
|
112
|
+
cols.push({ kind: 'star', table: tableTok.value })
|
|
113
|
+
if (!match(state, 'comma')) break
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
112
116
|
}
|
|
113
|
-
}
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
// Check for unqualified asterisk (*)
|
|
119
|
+
if (tok.type === 'operator' && tok.value === '*') {
|
|
120
|
+
consume(state)
|
|
121
|
+
cols.push({ kind: 'star' })
|
|
122
|
+
if (!match(state, 'comma')) break
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
121
125
|
|
|
122
|
-
while (true) {
|
|
123
126
|
cols.push(parseSelectItem(state))
|
|
124
127
|
if (!match(state, 'comma')) break
|
|
125
128
|
}
|
package/src/types.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface ScanOptions {
|
|
|
38
38
|
* All hints are optional and "best effort" - sources may ignore them.
|
|
39
39
|
*/
|
|
40
40
|
export interface QueryHints {
|
|
41
|
-
columns?: string[] // columns needed
|
|
41
|
+
columns?: string[] // columns needed (undefined means all columns)
|
|
42
42
|
where?: ExprNode // where clause
|
|
43
43
|
// important: only apply limit/offset if where is fully applied by the data source
|
|
44
44
|
// otherwise, the data source must return at least enough rows to ensure the engine
|
package/src/validation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* @import {AggregateFunc, BinaryOp,
|
|
3
|
+
* @import { AggregateFunc, BinaryOp, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction } from './types.js'
|
|
4
4
|
* @param {string} name
|
|
5
5
|
* @returns {name is AggregateFunc}
|
|
6
6
|
*/
|