squirreling 0.4.6 → 0.4.8
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 +12 -0
- package/package.json +2 -2
- package/src/errors.js +230 -0
- package/src/execute/aggregates.js +33 -7
- package/src/execute/date.js +57 -0
- package/src/execute/execute.js +15 -8
- package/src/execute/expression.js +151 -21
- package/src/execute/having.js +3 -2
- package/src/execute/join.js +11 -7
- package/src/execute/utils.js +34 -2
- package/src/parse/comparison.js +12 -11
- package/src/parse/expression.js +124 -5
- package/src/parse/parse.js +6 -1
- package/src/parse/state.js +5 -3
- package/src/parse/tokenize.js +89 -37
- package/src/types.d.ts +39 -6
- package/src/validation.js +28 -3
package/README.md
CHANGED
|
@@ -67,3 +67,15 @@ const allUsers: Record<string, SqlPrimitive>[] = await collect(executeSql({
|
|
|
67
67
|
}))
|
|
68
68
|
console.log(allUsers)
|
|
69
69
|
```
|
|
70
|
+
|
|
71
|
+
## Supported SQL Features
|
|
72
|
+
|
|
73
|
+
- `SELECT` statements with `WHERE`, `ORDER BY`, `LIMIT`, `OFFSET`
|
|
74
|
+
- Subqueries in `SELECT`, `FROM`, and `WHERE` clauses
|
|
75
|
+
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`
|
|
76
|
+
- `GROUP BY` and `HAVING` clauses
|
|
77
|
+
- Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
|
|
78
|
+
- String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
|
|
79
|
+
- Date functions: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
|
|
80
|
+
- Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
|
81
|
+
- Basic expressions and arithmetic operations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Squirreling SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@types/node": "24.10.1",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.15",
|
|
42
42
|
"eslint": "9.39.1",
|
|
43
|
-
"eslint-plugin-jsdoc": "61.4.
|
|
43
|
+
"eslint-plugin-jsdoc": "61.4.2",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
45
|
"vitest": "4.0.15"
|
|
46
46
|
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// PARSE ERRORS - Issues during SQL tokenization and parsing
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* General syntax error for unexpected tokens.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {string} options.expected - Description of what was expected
|
|
10
|
+
* @param {string} options.received - What was actually found
|
|
11
|
+
* @param {number} options.position - Character position in query
|
|
12
|
+
* @param {string} [options.after] - What token came before (for context)
|
|
13
|
+
* @returns {Error}
|
|
14
|
+
*/
|
|
15
|
+
export function syntaxError({ expected, received, position, after }) {
|
|
16
|
+
const afterClause = after ? ` after "${after}"` : ''
|
|
17
|
+
return new Error(`Expected ${expected}${afterClause} but found ${received} at position ${position}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error for unterminated literals (strings, identifiers).
|
|
22
|
+
*
|
|
23
|
+
* @param {'string' | 'identifier'} type - Type of unterminated literal
|
|
24
|
+
* @param {number} position - Starting position
|
|
25
|
+
* @returns {Error}
|
|
26
|
+
*/
|
|
27
|
+
export function unterminatedError(type, position) {
|
|
28
|
+
const name = type === 'string' ? 'string literal' : 'identifier'
|
|
29
|
+
return new Error(`Unterminated ${name} starting at position ${position}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Error for invalid literals (numbers, intervals, etc).
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} options
|
|
36
|
+
* @param {string} options.type - Type of invalid literal (e.g., 'number', 'interval value', 'interval unit')
|
|
37
|
+
* @param {string} options.value - The invalid value
|
|
38
|
+
* @param {number} options.position - Position in query
|
|
39
|
+
* @param {string} [options.validValues] - List of valid values (for enums like interval units)
|
|
40
|
+
* @returns {Error}
|
|
41
|
+
*/
|
|
42
|
+
export function invalidLiteralError({ type, value, position, validValues }) {
|
|
43
|
+
const suffix = validValues ? `. Valid values: ${validValues}` : ''
|
|
44
|
+
return new Error(`Invalid ${type} ${value} at position ${position}${suffix}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Error for unexpected characters during tokenization.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} char - The unexpected character
|
|
51
|
+
* @param {number} position - Position in query
|
|
52
|
+
* @param {boolean} [expectsSelect=false] - Whether SELECT was expected (first token)
|
|
53
|
+
* @returns {Error}
|
|
54
|
+
*/
|
|
55
|
+
export function unexpectedCharError(char, position, expectsSelect = false) {
|
|
56
|
+
if (expectsSelect) {
|
|
57
|
+
return new Error(`Expected SELECT but found "${char}" at position ${position}. Queries must start with SELECT.`)
|
|
58
|
+
}
|
|
59
|
+
return new Error(`Unexpected character "${char}" at position ${position}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Error for unknown/unsupported functions.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} funcName - The unknown function name
|
|
66
|
+
* @param {number} [position] - Position in query (for parse errors)
|
|
67
|
+
* @param {string} [validFunctions] - List of valid functions
|
|
68
|
+
* @returns {Error}
|
|
69
|
+
*/
|
|
70
|
+
export function unknownFunctionError(funcName, position, validFunctions) {
|
|
71
|
+
const supported = validFunctions ||
|
|
72
|
+
'COUNT, SUM, AVG, MIN, MAX, UPPER, LOWER, CONCAT, LENGTH, SUBSTRING, TRIM, REPLACE, JSON_OBJECT, JSON_VALUE, JSON_QUERY, JSON_ARRAYAGG'
|
|
73
|
+
|
|
74
|
+
if (position !== undefined) {
|
|
75
|
+
return new Error(`Unknown function "${funcName}" at position ${position}. Supported: ${supported}`)
|
|
76
|
+
}
|
|
77
|
+
return new Error(`Unsupported function: ${funcName}. Supported: ${supported}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Error for missing required clause or structure.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} options
|
|
84
|
+
* @param {string} options.missing - What is missing (e.g., 'WHEN clause', 'FROM clause', 'ON condition')
|
|
85
|
+
* @param {string} options.context - Where it's missing from (e.g., 'CASE expression', 'SELECT statement', 'JOIN')
|
|
86
|
+
* @returns {Error}
|
|
87
|
+
*/
|
|
88
|
+
export function missingClauseError({ missing, context }) {
|
|
89
|
+
return new Error(`${context} requires ${missing}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// EXECUTION ERRORS - Issues during query execution
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Error for missing table.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} tableName - The missing table name
|
|
100
|
+
* @returns {Error}
|
|
101
|
+
*/
|
|
102
|
+
export function tableNotFoundError(tableName) {
|
|
103
|
+
return new Error(`Table "${tableName}" not found. Check spelling or add it to the tables parameter.`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Error for invalid context (e.g., INTERVAL without date arithmetic).
|
|
108
|
+
*
|
|
109
|
+
* @param {Object} options
|
|
110
|
+
* @param {string} options.item - What was used incorrectly
|
|
111
|
+
* @param {string} options.validContext - Where it can be used
|
|
112
|
+
* @returns {Error}
|
|
113
|
+
*/
|
|
114
|
+
export function invalidContextError({ item, validContext }) {
|
|
115
|
+
return new Error(`${item} can only be used with ${validContext}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Error for unsupported operation combinations.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} operation - The unsupported operation
|
|
122
|
+
* @param {string} [hint] - How to fix it
|
|
123
|
+
* @returns {Error}
|
|
124
|
+
*/
|
|
125
|
+
export function unsupportedOperationError(operation, hint) {
|
|
126
|
+
const suffix = hint ? `. ${hint}` : ''
|
|
127
|
+
return new Error(`${operation}${suffix}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// VALIDATION ERRORS - Function argument and type validation
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Function signatures for helpful error messages.
|
|
136
|
+
* Maps function name to its parameter signature.
|
|
137
|
+
* @type {Record<string, string>}
|
|
138
|
+
*/
|
|
139
|
+
const FUNCTION_SIGNATURES = {
|
|
140
|
+
// String functions
|
|
141
|
+
UPPER: 'string',
|
|
142
|
+
LOWER: 'string',
|
|
143
|
+
LENGTH: 'string',
|
|
144
|
+
TRIM: 'string',
|
|
145
|
+
REPLACE: 'string, search, replacement',
|
|
146
|
+
SUBSTRING: 'string, start[, length]',
|
|
147
|
+
SUBSTR: 'string, start[, length]',
|
|
148
|
+
CONCAT: 'value1, value2[, ...]',
|
|
149
|
+
|
|
150
|
+
// Date/time functions
|
|
151
|
+
RANDOM: '',
|
|
152
|
+
RAND: '',
|
|
153
|
+
CURRENT_DATE: '',
|
|
154
|
+
CURRENT_TIME: '',
|
|
155
|
+
CURRENT_TIMESTAMP: '',
|
|
156
|
+
|
|
157
|
+
// JSON functions
|
|
158
|
+
JSON_VALUE: 'expression, path',
|
|
159
|
+
JSON_QUERY: 'expression, path',
|
|
160
|
+
JSON_OBJECT: 'key1, value1[, ...]',
|
|
161
|
+
JSON_ARRAYAGG: 'expression',
|
|
162
|
+
|
|
163
|
+
// Aggregate functions
|
|
164
|
+
COUNT: 'expression',
|
|
165
|
+
SUM: 'expression',
|
|
166
|
+
AVG: 'expression',
|
|
167
|
+
MIN: 'expression',
|
|
168
|
+
MAX: 'expression',
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Error for wrong number of function arguments.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} funcName - The function name
|
|
175
|
+
* @param {number | string} expected - Expected count (number or range like "2 or 3")
|
|
176
|
+
* @param {number} received - Actual argument count
|
|
177
|
+
* @returns {Error}
|
|
178
|
+
*/
|
|
179
|
+
export function argCountError(funcName, expected, received) {
|
|
180
|
+
const signature = FUNCTION_SIGNATURES[funcName] ?? ''
|
|
181
|
+
let expectedStr = `${expected} arguments`
|
|
182
|
+
if (expected === 0) expectedStr = 'no arguments'
|
|
183
|
+
if (expected === 1) expectedStr = '1 argument'
|
|
184
|
+
if (typeof expected === 'string' && expected.endsWith(' 1')) {
|
|
185
|
+
expectedStr = `${expected} argument`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return new Error(`${funcName}(${signature}) function requires ${expectedStr}, got ${received}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Error for invalid argument type or value.
|
|
193
|
+
*
|
|
194
|
+
* @param {Object} options
|
|
195
|
+
* @param {string} options.funcName - The function name
|
|
196
|
+
* @param {string} options.message - Specific error message
|
|
197
|
+
* @param {string} [options.hint] - Recovery hint
|
|
198
|
+
* @returns {Error}
|
|
199
|
+
*/
|
|
200
|
+
export function argValueError({ funcName, message, hint }) {
|
|
201
|
+
const signature = FUNCTION_SIGNATURES[funcName] ?? ''
|
|
202
|
+
const suffix = hint ? `. ${hint}` : ''
|
|
203
|
+
return new Error(`${funcName}(${signature}): ${message}${suffix}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Error for aggregate function misuse (e.g., SUM(*)).
|
|
208
|
+
*
|
|
209
|
+
* @param {string} funcName - The aggregate function
|
|
210
|
+
* @param {string} issue - What's wrong (e.g., "(*) is not supported")
|
|
211
|
+
* @returns {Error}
|
|
212
|
+
*/
|
|
213
|
+
export function aggregateError(funcName, issue) {
|
|
214
|
+
return new Error(`${funcName}${issue}. Only COUNT supports *. Use a column name for ${funcName}.`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Error for unsupported CAST type.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} toType - The unsupported target type
|
|
221
|
+
* @param {string} [fromType] - The source type (optional)
|
|
222
|
+
* @returns {Error}
|
|
223
|
+
*/
|
|
224
|
+
export function castError(toType, fromType) {
|
|
225
|
+
const message = fromType
|
|
226
|
+
? `Cannot CAST ${fromType} to ${toType}`
|
|
227
|
+
: `Unsupported CAST to type ${toType}`
|
|
228
|
+
|
|
229
|
+
return new Error(`${message}. Supported types: TEXT, VARCHAR, INTEGER, INT, BIGINT, FLOAT, REAL, DOUBLE, BOOLEAN`)
|
|
230
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { aggregateError, unknownFunctionError } from '../errors.js'
|
|
1
2
|
import { evaluateExpr } from './expression.js'
|
|
2
|
-
import { defaultDerivedAlias } from './utils.js'
|
|
3
|
+
import { defaultDerivedAlias, stringify } from './utils.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Evaluates an aggregate function over a set of rows
|
|
6
7
|
*
|
|
7
|
-
* @import { AggregateColumn, AsyncDataSource,
|
|
8
|
+
* @import { AggregateColumn, AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
|
|
8
9
|
* @param {Object} options
|
|
9
10
|
* @param {AggregateColumn} options.col - aggregate column definition
|
|
10
11
|
* @param {AsyncRow[]} options.rows - rows to aggregate
|
|
11
12
|
* @param {Record<string, AsyncDataSource>} options.tables
|
|
12
|
-
* @returns {Promise<
|
|
13
|
+
* @returns {Promise<SqlPrimitive>} aggregated result
|
|
13
14
|
*/
|
|
14
15
|
export async function evaluateAggregate({ col, rows, tables }) {
|
|
15
16
|
const { arg, func } = col
|
|
@@ -20,7 +21,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
20
21
|
const seen = new Set()
|
|
21
22
|
for (const row of rows) {
|
|
22
23
|
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
23
|
-
if (v
|
|
24
|
+
if (v != null) {
|
|
24
25
|
seen.add(v)
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -29,7 +30,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
29
30
|
let count = 0
|
|
30
31
|
for (const row of rows) {
|
|
31
32
|
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
32
|
-
if (v
|
|
33
|
+
if (v != null) {
|
|
33
34
|
count += 1
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -38,7 +39,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
38
39
|
|
|
39
40
|
if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
|
|
40
41
|
if (arg.kind === 'star') {
|
|
41
|
-
throw
|
|
42
|
+
throw aggregateError(func, '(*) is not supported, use a column name')
|
|
42
43
|
}
|
|
43
44
|
let sum = 0
|
|
44
45
|
let count = 0
|
|
@@ -70,7 +71,32 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
70
71
|
if (func === 'MAX') return max
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
if (func === 'JSON_ARRAYAGG') {
|
|
75
|
+
if (arg.kind === 'star') {
|
|
76
|
+
throw aggregateError('JSON_ARRAYAGG', '(*) is not supported, use a column name or expression')
|
|
77
|
+
}
|
|
78
|
+
/** @type {SqlPrimitive[]} */
|
|
79
|
+
const values = []
|
|
80
|
+
if (arg.quantifier === 'distinct') {
|
|
81
|
+
const seen = new Set()
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
84
|
+
const key = stringify(v)
|
|
85
|
+
if (!seen.has(key)) {
|
|
86
|
+
seen.add(key)
|
|
87
|
+
values.push(v)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
93
|
+
values.push(v)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return values
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw unknownFunctionError(func, undefined, 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG')
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { SqlPrimitive, IntervalUnit } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {SqlPrimitive} val
|
|
7
|
+
* @returns {Date | null}
|
|
8
|
+
*/
|
|
9
|
+
function toDate(val) {
|
|
10
|
+
if (val instanceof Date) return val
|
|
11
|
+
const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
|
|
12
|
+
if (typeof val === 'string' && dateOrTime.test(val)) {
|
|
13
|
+
const date = new Date(val)
|
|
14
|
+
if (!isNaN(date.getTime())) {
|
|
15
|
+
return date
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply an interval to a date
|
|
23
|
+
* @param {SqlPrimitive} dateVal
|
|
24
|
+
* @param {number} value
|
|
25
|
+
* @param {IntervalUnit} unit
|
|
26
|
+
* @param {'+' | '-'} op
|
|
27
|
+
* @returns {string | null}
|
|
28
|
+
*/
|
|
29
|
+
export function applyIntervalToDate(dateVal, value, unit, op) {
|
|
30
|
+
const date = toDate(dateVal)
|
|
31
|
+
if (date == null) return null
|
|
32
|
+
|
|
33
|
+
const multiplier = op === '+' ? 1 : -1
|
|
34
|
+
const adjusted = value * multiplier
|
|
35
|
+
|
|
36
|
+
if (unit === 'SECOND') {
|
|
37
|
+
date.setUTCSeconds(date.getUTCSeconds() + adjusted)
|
|
38
|
+
} else if (unit === 'MINUTE') {
|
|
39
|
+
date.setUTCMinutes(date.getUTCMinutes() + adjusted)
|
|
40
|
+
} else if (unit === 'HOUR') {
|
|
41
|
+
date.setUTCHours(date.getUTCHours() + adjusted)
|
|
42
|
+
} else if (unit === 'DAY') {
|
|
43
|
+
date.setUTCDate(date.getUTCDate() + adjusted)
|
|
44
|
+
} else if (unit === 'MONTH') {
|
|
45
|
+
date.setUTCMonth(date.getUTCMonth() + adjusted)
|
|
46
|
+
} else if (unit === 'YEAR') {
|
|
47
|
+
date.setUTCFullYear(date.getUTCFullYear() + adjusted)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Return in same format as input
|
|
51
|
+
if (dateVal instanceof Date) return date.toISOString()
|
|
52
|
+
if (String(dateVal).includes('T')) {
|
|
53
|
+
return date.toISOString()
|
|
54
|
+
} else {
|
|
55
|
+
return date.toISOString().split('T')[0]
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/execute/execute.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { missingClauseError, tableNotFoundError, unsupportedOperationError } from '../errors.js'
|
|
1
2
|
import { generatorSource, memorySource } from '../backend/dataSource.js'
|
|
2
3
|
import { parseSql } from '../parse/parse.js'
|
|
3
4
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
5
|
+
import { extractColumns } from './columns.js'
|
|
4
6
|
import { evaluateExpr } from './expression.js'
|
|
5
7
|
import { evaluateHavingExpr } from './having.js'
|
|
6
8
|
import { executeJoins } from './join.js'
|
|
7
|
-
import { compareForTerm, defaultDerivedAlias } from './utils.js'
|
|
8
|
-
import { extractColumns } from './columns.js'
|
|
9
|
+
import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions,
|
|
12
|
+
* @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -22,7 +23,10 @@ export async function* executeSql({ tables, query }) {
|
|
|
22
23
|
|
|
23
24
|
// Check for unsupported operations
|
|
24
25
|
if (!select.from) {
|
|
25
|
-
throw
|
|
26
|
+
throw missingClauseError({
|
|
27
|
+
missing: 'FROM clause',
|
|
28
|
+
context: 'SELECT statement',
|
|
29
|
+
})
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
// Normalize tables: convert arrays to AsyncDataSource
|
|
@@ -57,7 +61,7 @@ export async function* executeSelect(select, tables) {
|
|
|
57
61
|
fromTableName = select.from.alias ?? select.from.table
|
|
58
62
|
dataSource = tables[select.from.table]
|
|
59
63
|
if (dataSource === undefined) {
|
|
60
|
-
throw
|
|
64
|
+
throw tableNotFoundError(select.from.table)
|
|
61
65
|
}
|
|
62
66
|
} else {
|
|
63
67
|
// Nested subquery - recursively resolve
|
|
@@ -85,7 +89,7 @@ async function stableRowKey(row) {
|
|
|
85
89
|
const parts = []
|
|
86
90
|
for (const k of keys) {
|
|
87
91
|
const v = await row[k]()
|
|
88
|
-
parts.push(k + ':' +
|
|
92
|
+
parts.push(k + ':' + stringify(v))
|
|
89
93
|
}
|
|
90
94
|
return parts.join('|')
|
|
91
95
|
}
|
|
@@ -358,7 +362,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
358
362
|
const keyParts = []
|
|
359
363
|
for (const expr of select.groupBy) {
|
|
360
364
|
const v = await evaluateExpr({ node: expr, row, tables })
|
|
361
|
-
keyParts.push(
|
|
365
|
+
keyParts.push(stringify(v))
|
|
362
366
|
}
|
|
363
367
|
const key = keyParts.join('|')
|
|
364
368
|
let group = map.get(key)
|
|
@@ -375,7 +379,10 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
375
379
|
|
|
376
380
|
const hasStar = select.columns.some(col => col.kind === 'star')
|
|
377
381
|
if (hasStar && hasAggregate) {
|
|
378
|
-
throw
|
|
382
|
+
throw unsupportedOperationError(
|
|
383
|
+
'SELECT * with aggregate functions is not supported',
|
|
384
|
+
'Replace * with specific column names when using aggregate functions.'
|
|
385
|
+
)
|
|
379
386
|
}
|
|
380
387
|
|
|
381
388
|
for (const group of groups) {
|