squirreling 0.4.8 → 0.5.0
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/aggregates.js +10 -4
- package/src/execute/execute.js +16 -10
- package/src/execute/expression.js +202 -38
- package/src/execute/having.js +7 -2
- package/src/execute/join.js +5 -4
- package/src/execute/math.js +165 -0
- package/src/executionErrors.js +62 -0
- package/src/index.js +1 -0
- package/src/parse/comparison.js +41 -8
- package/src/parse/expression.js +53 -13
- package/src/parse/state.js +13 -3
- package/src/parse/tokenize.js +34 -21
- package/src/parseErrors.js +117 -0
- package/src/types.d.ts +37 -13
- package/src/validation.js +12 -1
- package/src/validationErrors.js +127 -0
- package/src/errors.js +0 -230
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": "24.10.
|
|
40
|
+
"@types/node": "24.10.2",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.15",
|
|
42
42
|
"eslint": "9.39.1",
|
|
43
|
-
"eslint-plugin-jsdoc": "61.
|
|
43
|
+
"eslint-plugin-jsdoc": "61.5.0",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
45
|
"vitest": "4.0.15"
|
|
46
46
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
|
+
import { aggregateError } from '../validationErrors.js'
|
|
2
3
|
import { evaluateExpr } from './expression.js'
|
|
3
4
|
import { defaultDerivedAlias, stringify } from './utils.js'
|
|
4
5
|
|
|
@@ -39,7 +40,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
39
40
|
|
|
40
41
|
if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
|
|
41
42
|
if (arg.kind === 'star') {
|
|
42
|
-
throw aggregateError(func, '(*) is not supported, use a column name')
|
|
43
|
+
throw aggregateError({ funcName: func, issue: '(*) is not supported, use a column name' })
|
|
43
44
|
}
|
|
44
45
|
let sum = 0
|
|
45
46
|
let count = 0
|
|
@@ -73,7 +74,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
73
74
|
|
|
74
75
|
if (func === 'JSON_ARRAYAGG') {
|
|
75
76
|
if (arg.kind === 'star') {
|
|
76
|
-
throw aggregateError('JSON_ARRAYAGG', '(*) is not supported, use a column name or expression')
|
|
77
|
+
throw aggregateError({ funcName: 'JSON_ARRAYAGG', issue: '(*) is not supported, use a column name or expression' })
|
|
77
78
|
}
|
|
78
79
|
/** @type {SqlPrimitive[]} */
|
|
79
80
|
const values = []
|
|
@@ -96,7 +97,12 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
96
97
|
return values
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
throw unknownFunctionError(
|
|
100
|
+
throw unknownFunctionError({
|
|
101
|
+
funcName: func,
|
|
102
|
+
positionStart: 0,
|
|
103
|
+
positionEnd: 0,
|
|
104
|
+
validFunctions: 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG',
|
|
105
|
+
})
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/**
|
package/src/execute/execute.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { missingClauseError
|
|
1
|
+
import { missingClauseError } from '../parseErrors.js'
|
|
2
|
+
import { tableNotFoundError, unsupportedOperationError } from '../executionErrors.js'
|
|
2
3
|
import { generatorSource, memorySource } from '../backend/dataSource.js'
|
|
3
4
|
import { parseSql } from '../parse/parse.js'
|
|
4
5
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
@@ -61,7 +62,7 @@ export async function* executeSelect(select, tables) {
|
|
|
61
62
|
fromTableName = select.from.alias ?? select.from.table
|
|
62
63
|
dataSource = tables[select.from.table]
|
|
63
64
|
if (dataSource === undefined) {
|
|
64
|
-
throw tableNotFoundError(select.from.table)
|
|
65
|
+
throw tableNotFoundError({ tableName: select.from.table })
|
|
65
66
|
}
|
|
66
67
|
} else {
|
|
67
68
|
// Nested subquery - recursively resolve
|
|
@@ -236,6 +237,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
|
|
|
236
237
|
async function* evaluateStreaming(select, dataSource, tables) {
|
|
237
238
|
let rowsYielded = 0
|
|
238
239
|
let rowsSkipped = 0
|
|
240
|
+
let rowIndex = 0
|
|
239
241
|
const offset = select.offset ?? 0
|
|
240
242
|
const limit = select.limit ?? Infinity
|
|
241
243
|
if (limit <= 0) return
|
|
@@ -254,9 +256,10 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
254
256
|
}
|
|
255
257
|
|
|
256
258
|
for await (const row of dataSource.getRows(hints)) {
|
|
259
|
+
rowIndex++
|
|
257
260
|
// WHERE filter
|
|
258
261
|
if (select.where) {
|
|
259
|
-
const pass = await evaluateExpr({ node: select.where, row, tables })
|
|
262
|
+
const pass = await evaluateExpr({ node: select.where, row, tables, rowIndex })
|
|
260
263
|
if (!pass) continue
|
|
261
264
|
}
|
|
262
265
|
|
|
@@ -269,6 +272,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
269
272
|
// SELECT projection
|
|
270
273
|
/** @type {AsyncRow} */
|
|
271
274
|
const outRow = {}
|
|
275
|
+
const currentRowIndex = rowIndex
|
|
272
276
|
for (const col of select.columns) {
|
|
273
277
|
if (col.kind === 'star') {
|
|
274
278
|
for (const [key, cell] of Object.entries(row)) {
|
|
@@ -276,7 +280,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
276
280
|
}
|
|
277
281
|
} else if (col.kind === 'derived') {
|
|
278
282
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
279
|
-
outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
|
|
283
|
+
outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
|
|
280
284
|
} else if (col.kind === 'aggregate') {
|
|
281
285
|
throw new Error(
|
|
282
286
|
'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
|
|
@@ -334,9 +338,11 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
334
338
|
/** @type {AsyncRow[]} */
|
|
335
339
|
const filtered = []
|
|
336
340
|
|
|
337
|
-
for (
|
|
341
|
+
for (let i = 0; i < working.length; i++) {
|
|
342
|
+
const row = working[i]
|
|
343
|
+
const rowIndex = i + 1 // 1-based
|
|
338
344
|
if (select.where) {
|
|
339
|
-
const passes = await evaluateExpr({ node: select.where, row, tables })
|
|
345
|
+
const passes = await evaluateExpr({ node: select.where, row, tables, rowIndex })
|
|
340
346
|
|
|
341
347
|
if (!passes) {
|
|
342
348
|
continue
|
|
@@ -379,10 +385,10 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
379
385
|
|
|
380
386
|
const hasStar = select.columns.some(col => col.kind === 'star')
|
|
381
387
|
if (hasStar && hasAggregate) {
|
|
382
|
-
throw unsupportedOperationError(
|
|
383
|
-
'SELECT * with aggregate functions is not supported',
|
|
384
|
-
'Replace * with specific column names when using aggregate functions.'
|
|
385
|
-
)
|
|
388
|
+
throw unsupportedOperationError({
|
|
389
|
+
operation: 'SELECT * with aggregate functions is not supported',
|
|
390
|
+
hint: 'Replace * with specific column names when using aggregate functions.',
|
|
391
|
+
})
|
|
386
392
|
}
|
|
387
393
|
|
|
388
394
|
for (const group of groups) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
|
+
import { invalidContextError } from '../executionErrors.js'
|
|
1
3
|
import {
|
|
2
4
|
argCountError,
|
|
3
5
|
argValueError,
|
|
4
6
|
castError,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '../errors.js'
|
|
7
|
+
} from '../validationErrors.js'
|
|
8
|
+
import { isMathFunc } from '../validation.js'
|
|
8
9
|
import { applyIntervalToDate } from './date.js'
|
|
9
10
|
import { executeSelect } from './execute.js'
|
|
11
|
+
import { evaluateMathFunc } from './math.js'
|
|
10
12
|
import { applyBinaryOp, stringify } from './utils.js'
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -20,9 +22,10 @@ import { applyBinaryOp, stringify } from './utils.js'
|
|
|
20
22
|
* @param {ExprNode} params.node - The expression node to evaluate
|
|
21
23
|
* @param {AsyncRow} params.row - The data row to evaluate against
|
|
22
24
|
* @param {Record<string, AsyncDataSource>} params.tables
|
|
25
|
+
* @param {number} [params.rowIndex] - 1-based row index for error reporting
|
|
23
26
|
* @returns {Promise<SqlPrimitive>} The result of the evaluation
|
|
24
27
|
*/
|
|
25
|
-
export async function evaluateExpr({ node, row, tables }) {
|
|
28
|
+
export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
26
29
|
if (node.type === 'literal') {
|
|
27
30
|
return node.value
|
|
28
31
|
}
|
|
@@ -57,16 +60,16 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
57
60
|
// Unary operators
|
|
58
61
|
if (node.type === 'unary') {
|
|
59
62
|
if (node.op === 'NOT') {
|
|
60
|
-
return !await evaluateExpr({ node: node.argument, row, tables })
|
|
63
|
+
return !await evaluateExpr({ node: node.argument, row, tables, rowIndex })
|
|
61
64
|
}
|
|
62
65
|
if (node.op === 'IS NULL') {
|
|
63
|
-
return await evaluateExpr({ node: node.argument, row, tables }) == null
|
|
66
|
+
return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) == null
|
|
64
67
|
}
|
|
65
68
|
if (node.op === 'IS NOT NULL') {
|
|
66
|
-
return await evaluateExpr({ node: node.argument, row, tables }) != null
|
|
69
|
+
return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) != null
|
|
67
70
|
}
|
|
68
71
|
if (node.op === '-') {
|
|
69
|
-
const val = await evaluateExpr({ node: node.argument, row, tables })
|
|
72
|
+
const val = await evaluateExpr({ node: node.argument, row, tables, rowIndex })
|
|
70
73
|
if (val == null) return null
|
|
71
74
|
return -val
|
|
72
75
|
}
|
|
@@ -76,15 +79,15 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
76
79
|
if (node.type === 'binary') {
|
|
77
80
|
// Handle date +/- interval at AST level
|
|
78
81
|
if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
|
|
79
|
-
const dateVal = await evaluateExpr({ node: node.left, row, tables })
|
|
82
|
+
const dateVal = await evaluateExpr({ node: node.left, row, tables, rowIndex })
|
|
80
83
|
return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
|
|
81
84
|
}
|
|
82
85
|
if (node.op === '+' && node.left.type === 'interval') {
|
|
83
|
-
const dateVal = await evaluateExpr({ node: node.right, row, tables })
|
|
86
|
+
const dateVal = await evaluateExpr({ node: node.right, row, tables, rowIndex })
|
|
84
87
|
return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
const left = await evaluateExpr({ node: node.left, row, tables })
|
|
90
|
+
const left = await evaluateExpr({ node: node.left, row, tables, rowIndex })
|
|
88
91
|
|
|
89
92
|
// Short-circuit evaluation for AND and OR
|
|
90
93
|
if (node.op === 'AND') {
|
|
@@ -94,7 +97,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
94
97
|
if (left) return true
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
const right = await evaluateExpr({ node: node.right, row, tables })
|
|
100
|
+
const right = await evaluateExpr({ node: node.right, row, tables, rowIndex })
|
|
98
101
|
return applyBinaryOp(node.op, left, right)
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -102,38 +105,77 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
102
105
|
if (node.type === 'function') {
|
|
103
106
|
const funcName = node.name.toUpperCase()
|
|
104
107
|
/** @type {SqlPrimitive[]} */
|
|
105
|
-
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
|
|
108
|
+
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, rowIndex })))
|
|
106
109
|
|
|
107
110
|
if (funcName === 'UPPER') {
|
|
108
|
-
if (args.length !== 1)
|
|
111
|
+
if (args.length !== 1) {
|
|
112
|
+
throw argCountError({
|
|
113
|
+
funcName: 'UPPER',
|
|
114
|
+
expected: 1,
|
|
115
|
+
received: args.length,
|
|
116
|
+
positionStart: node.positionStart,
|
|
117
|
+
positionEnd: node.positionEnd,
|
|
118
|
+
rowNumber: rowIndex,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
109
121
|
const val = args[0]
|
|
110
122
|
if (val == null) return null
|
|
111
123
|
return String(val).toUpperCase()
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
if (funcName === 'LOWER') {
|
|
115
|
-
if (args.length !== 1)
|
|
127
|
+
if (args.length !== 1) {
|
|
128
|
+
throw argCountError({
|
|
129
|
+
funcName: 'LOWER',
|
|
130
|
+
expected: 1,
|
|
131
|
+
received: args.length,
|
|
132
|
+
positionStart: node.positionStart,
|
|
133
|
+
positionEnd: node.positionEnd,
|
|
134
|
+
rowNumber: rowIndex,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
116
137
|
const val = args[0]
|
|
117
138
|
if (val == null) return null
|
|
118
139
|
return String(val).toLowerCase()
|
|
119
140
|
}
|
|
120
141
|
|
|
121
142
|
if (funcName === 'CONCAT') {
|
|
122
|
-
if (args.length < 1)
|
|
143
|
+
if (args.length < 1) {
|
|
144
|
+
throw argCountError({
|
|
145
|
+
funcName: 'CONCAT',
|
|
146
|
+
expected: 'at least 1',
|
|
147
|
+
received: args.length,
|
|
148
|
+
positionStart: node.positionStart,
|
|
149
|
+
positionEnd: node.positionEnd,
|
|
150
|
+
rowNumber: rowIndex,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
123
153
|
// SQL CONCAT returns NULL if any argument is NULL
|
|
124
154
|
if (args.some(a => a == null)) return null
|
|
125
155
|
if (args.some(a => typeof a === 'object')) {
|
|
126
156
|
throw argValueError({
|
|
127
157
|
funcName: 'CONCAT',
|
|
128
158
|
message: 'does not support object arguments',
|
|
159
|
+
positionStart: node.positionStart,
|
|
160
|
+
positionEnd: node.positionEnd,
|
|
129
161
|
hint: 'Use CAST to convert objects to strings first.',
|
|
162
|
+
rowNumber: rowIndex,
|
|
130
163
|
})
|
|
131
164
|
}
|
|
132
165
|
return args.map(a => String(a)).join('')
|
|
133
166
|
}
|
|
134
167
|
|
|
135
168
|
if (funcName === 'LENGTH') {
|
|
136
|
-
if (args.length !== 1)
|
|
169
|
+
if (args.length !== 1) {
|
|
170
|
+
throw argCountError({
|
|
171
|
+
funcName: 'LENGTH',
|
|
172
|
+
expected: 1,
|
|
173
|
+
received: args.length,
|
|
174
|
+
positionStart: node.positionStart,
|
|
175
|
+
positionEnd: node.positionEnd,
|
|
176
|
+
rowNumber: rowIndex,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
137
179
|
const val = args[0]
|
|
138
180
|
if (val == null) return null
|
|
139
181
|
return String(val).length
|
|
@@ -141,7 +183,14 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
141
183
|
|
|
142
184
|
if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
|
|
143
185
|
if (args.length < 2 || args.length > 3) {
|
|
144
|
-
throw argCountError(
|
|
186
|
+
throw argCountError({
|
|
187
|
+
funcName,
|
|
188
|
+
expected: '2 or 3',
|
|
189
|
+
received: args.length,
|
|
190
|
+
positionStart: node.positionStart,
|
|
191
|
+
positionEnd: node.positionEnd,
|
|
192
|
+
rowNumber: rowIndex,
|
|
193
|
+
})
|
|
145
194
|
}
|
|
146
195
|
const str = args[0]
|
|
147
196
|
if (str == null) return null
|
|
@@ -151,7 +200,10 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
151
200
|
throw argValueError({
|
|
152
201
|
funcName,
|
|
153
202
|
message: `start position must be a positive integer, got ${args[1]}`,
|
|
203
|
+
positionStart: node.positionStart,
|
|
204
|
+
positionEnd: node.positionEnd,
|
|
154
205
|
hint: 'SQL uses 1-based indexing.',
|
|
206
|
+
rowNumber: rowIndex,
|
|
155
207
|
})
|
|
156
208
|
}
|
|
157
209
|
// SQL uses 1-based indexing
|
|
@@ -162,6 +214,9 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
162
214
|
throw argValueError({
|
|
163
215
|
funcName,
|
|
164
216
|
message: `length must be a non-negative integer, got ${args[2]}`,
|
|
217
|
+
positionStart: node.positionStart,
|
|
218
|
+
positionEnd: node.positionEnd,
|
|
219
|
+
rowNumber: rowIndex,
|
|
165
220
|
})
|
|
166
221
|
}
|
|
167
222
|
return strVal.substring(startIdx, startIdx + len)
|
|
@@ -170,14 +225,32 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
170
225
|
}
|
|
171
226
|
|
|
172
227
|
if (funcName === 'TRIM') {
|
|
173
|
-
if (args.length !== 1)
|
|
228
|
+
if (args.length !== 1) {
|
|
229
|
+
throw argCountError({
|
|
230
|
+
funcName: 'TRIM',
|
|
231
|
+
expected: 1,
|
|
232
|
+
received: args.length,
|
|
233
|
+
positionStart: node.positionStart,
|
|
234
|
+
positionEnd: node.positionEnd,
|
|
235
|
+
rowNumber: rowIndex,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
174
238
|
const val = args[0]
|
|
175
239
|
if (val == null) return null
|
|
176
240
|
return String(val).trim()
|
|
177
241
|
}
|
|
178
242
|
|
|
179
243
|
if (funcName === 'REPLACE') {
|
|
180
|
-
if (args.length !== 3)
|
|
244
|
+
if (args.length !== 3) {
|
|
245
|
+
throw argCountError({
|
|
246
|
+
funcName: 'REPLACE',
|
|
247
|
+
expected: 3,
|
|
248
|
+
received: args.length,
|
|
249
|
+
positionStart: node.positionStart,
|
|
250
|
+
positionEnd: node.positionEnd,
|
|
251
|
+
rowNumber: rowIndex,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
181
254
|
const str = args[0]
|
|
182
255
|
const searchStr = args[1]
|
|
183
256
|
const replaceStr = args[2]
|
|
@@ -187,28 +260,71 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
187
260
|
}
|
|
188
261
|
|
|
189
262
|
if (funcName === 'RANDOM' || funcName === 'RAND') {
|
|
190
|
-
if (args.length !== 0)
|
|
263
|
+
if (args.length !== 0) {
|
|
264
|
+
throw argCountError({
|
|
265
|
+
funcName,
|
|
266
|
+
expected: 0,
|
|
267
|
+
received: args.length,
|
|
268
|
+
positionStart: node.positionStart,
|
|
269
|
+
positionEnd: node.positionEnd,
|
|
270
|
+
rowNumber: rowIndex,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
191
273
|
return Math.random()
|
|
192
274
|
}
|
|
193
275
|
|
|
194
276
|
if (funcName === 'CURRENT_DATE') {
|
|
195
|
-
if (args.length !== 0)
|
|
277
|
+
if (args.length !== 0) {
|
|
278
|
+
throw argCountError({
|
|
279
|
+
funcName: 'CURRENT_DATE',
|
|
280
|
+
expected: 0,
|
|
281
|
+
received: args.length,
|
|
282
|
+
positionStart: node.positionStart,
|
|
283
|
+
positionEnd: node.positionEnd,
|
|
284
|
+
rowNumber: rowIndex,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
196
287
|
return new Date().toISOString().split('T')[0]
|
|
197
288
|
}
|
|
198
289
|
|
|
199
290
|
if (funcName === 'CURRENT_TIME') {
|
|
200
|
-
if (args.length !== 0)
|
|
291
|
+
if (args.length !== 0) {
|
|
292
|
+
throw argCountError({
|
|
293
|
+
funcName: 'CURRENT_TIME',
|
|
294
|
+
expected: 0,
|
|
295
|
+
received: args.length,
|
|
296
|
+
positionStart: node.positionStart,
|
|
297
|
+
positionEnd: node.positionEnd,
|
|
298
|
+
rowNumber: rowIndex,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
201
301
|
return new Date().toISOString().split('T')[1].replace('Z', '')
|
|
202
302
|
}
|
|
203
303
|
|
|
204
304
|
if (funcName === 'CURRENT_TIMESTAMP') {
|
|
205
|
-
if (args.length !== 0)
|
|
305
|
+
if (args.length !== 0) {
|
|
306
|
+
throw argCountError({
|
|
307
|
+
funcName: 'CURRENT_TIMESTAMP',
|
|
308
|
+
expected: 0,
|
|
309
|
+
received: args.length,
|
|
310
|
+
positionStart: node.positionStart,
|
|
311
|
+
positionEnd: node.positionEnd,
|
|
312
|
+
rowNumber: rowIndex,
|
|
313
|
+
})
|
|
314
|
+
}
|
|
206
315
|
return new Date().toISOString()
|
|
207
316
|
}
|
|
208
317
|
|
|
209
318
|
if (funcName === 'JSON_OBJECT') {
|
|
210
319
|
if (args.length % 2 !== 0) {
|
|
211
|
-
throw argCountError(
|
|
320
|
+
throw argCountError({
|
|
321
|
+
funcName: 'JSON_OBJECT',
|
|
322
|
+
expected: 'even number',
|
|
323
|
+
received: args.length,
|
|
324
|
+
positionStart: node.positionStart,
|
|
325
|
+
positionEnd: node.positionEnd,
|
|
326
|
+
rowNumber: rowIndex,
|
|
327
|
+
})
|
|
212
328
|
}
|
|
213
329
|
/** @type {Record<string, SqlPrimitive>} */
|
|
214
330
|
const result = {}
|
|
@@ -219,7 +335,10 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
219
335
|
throw argValueError({
|
|
220
336
|
funcName: 'JSON_OBJECT',
|
|
221
337
|
message: 'key cannot be null',
|
|
338
|
+
positionStart: node.positionStart,
|
|
339
|
+
positionEnd: node.positionEnd,
|
|
222
340
|
hint: 'All keys must be non-null values.',
|
|
341
|
+
rowNumber: rowIndex,
|
|
223
342
|
})
|
|
224
343
|
}
|
|
225
344
|
result[String(key)] = value
|
|
@@ -228,7 +347,16 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
228
347
|
}
|
|
229
348
|
|
|
230
349
|
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
|
|
231
|
-
if (args.length !== 2)
|
|
350
|
+
if (args.length !== 2) {
|
|
351
|
+
throw argCountError({
|
|
352
|
+
funcName,
|
|
353
|
+
expected: 2,
|
|
354
|
+
received: args.length,
|
|
355
|
+
positionStart: node.positionStart,
|
|
356
|
+
positionEnd: node.positionEnd,
|
|
357
|
+
rowNumber: rowIndex,
|
|
358
|
+
})
|
|
359
|
+
}
|
|
232
360
|
let jsonArg = args[0]
|
|
233
361
|
const pathArg = args[1]
|
|
234
362
|
if (jsonArg == null || pathArg == null) return null
|
|
@@ -241,7 +369,10 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
241
369
|
throw argValueError({
|
|
242
370
|
funcName,
|
|
243
371
|
message: 'invalid JSON string',
|
|
372
|
+
positionStart: node.positionStart,
|
|
373
|
+
positionEnd: node.positionEnd,
|
|
244
374
|
hint: 'First argument must be valid JSON.',
|
|
375
|
+
rowNumber: rowIndex,
|
|
245
376
|
})
|
|
246
377
|
}
|
|
247
378
|
}
|
|
@@ -249,6 +380,9 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
249
380
|
throw argValueError({
|
|
250
381
|
funcName,
|
|
251
382
|
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
383
|
+
positionStart: node.positionStart,
|
|
384
|
+
positionEnd: node.positionEnd,
|
|
385
|
+
rowNumber: rowIndex,
|
|
252
386
|
})
|
|
253
387
|
}
|
|
254
388
|
|
|
@@ -278,11 +412,25 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
278
412
|
return current
|
|
279
413
|
}
|
|
280
414
|
|
|
281
|
-
|
|
415
|
+
if (isMathFunc(funcName)) {
|
|
416
|
+
return evaluateMathFunc({
|
|
417
|
+
funcName,
|
|
418
|
+
args,
|
|
419
|
+
positionStart: node.positionStart,
|
|
420
|
+
positionEnd: node.positionEnd,
|
|
421
|
+
rowNumber: rowIndex,
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
throw unknownFunctionError({
|
|
426
|
+
funcName,
|
|
427
|
+
positionStart: node.positionStart,
|
|
428
|
+
positionEnd: node.positionEnd,
|
|
429
|
+
})
|
|
282
430
|
}
|
|
283
431
|
|
|
284
432
|
if (node.type === 'cast') {
|
|
285
|
-
const val = await evaluateExpr({ node: node.expr, row, tables })
|
|
433
|
+
const val = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
286
434
|
if (val == null) return null
|
|
287
435
|
const toType = node.toType.toUpperCase()
|
|
288
436
|
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
@@ -290,7 +438,15 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
290
438
|
return String(val)
|
|
291
439
|
}
|
|
292
440
|
// Can only cast primitives to other primitive types
|
|
293
|
-
if (typeof val === 'object')
|
|
441
|
+
if (typeof val === 'object') {
|
|
442
|
+
throw castError({
|
|
443
|
+
toType: node.toType,
|
|
444
|
+
positionStart: node.positionStart,
|
|
445
|
+
positionEnd: node.positionEnd,
|
|
446
|
+
fromType: 'object',
|
|
447
|
+
rowNumber: rowIndex,
|
|
448
|
+
})
|
|
449
|
+
}
|
|
294
450
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
295
451
|
const num = Number(val)
|
|
296
452
|
if (isNaN(num)) return null
|
|
@@ -307,21 +463,26 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
307
463
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
308
464
|
return Boolean(val)
|
|
309
465
|
}
|
|
310
|
-
throw castError(
|
|
466
|
+
throw castError({
|
|
467
|
+
toType: node.toType,
|
|
468
|
+
positionStart: node.positionStart,
|
|
469
|
+
positionEnd: node.positionEnd,
|
|
470
|
+
rowNumber: rowIndex,
|
|
471
|
+
})
|
|
311
472
|
}
|
|
312
473
|
|
|
313
474
|
// IN and NOT IN with value lists
|
|
314
475
|
if (node.type === 'in valuelist') {
|
|
315
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
476
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
316
477
|
for (const valueNode of node.values) {
|
|
317
|
-
const val = await evaluateExpr({ node: valueNode, row, tables })
|
|
478
|
+
const val = await evaluateExpr({ node: valueNode, row, tables, rowIndex })
|
|
318
479
|
if (exprVal === val) return true
|
|
319
480
|
}
|
|
320
481
|
return false
|
|
321
482
|
}
|
|
322
483
|
// IN with subqueries
|
|
323
484
|
if (node.type === 'in') {
|
|
324
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
485
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
325
486
|
const results = executeSelect(node.subquery, tables)
|
|
326
487
|
/** @type {SqlPrimitive[]} */
|
|
327
488
|
const values = []
|
|
@@ -346,28 +507,28 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
346
507
|
// CASE expressions
|
|
347
508
|
if (node.type === 'case') {
|
|
348
509
|
// For simple CASE: evaluate the case expression once
|
|
349
|
-
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
|
|
510
|
+
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, rowIndex })
|
|
350
511
|
|
|
351
512
|
// Iterate through WHEN clauses
|
|
352
513
|
for (const whenClause of node.whenClauses) {
|
|
353
514
|
let conditionResult
|
|
354
515
|
if (caseValue !== undefined) {
|
|
355
516
|
// Simple CASE: compare caseValue with condition
|
|
356
|
-
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables })
|
|
517
|
+
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
|
|
357
518
|
conditionResult = caseValue === whenValue
|
|
358
519
|
} else {
|
|
359
520
|
// Searched CASE: evaluate condition as boolean
|
|
360
|
-
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables })
|
|
521
|
+
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
|
|
361
522
|
}
|
|
362
523
|
|
|
363
524
|
if (conditionResult) {
|
|
364
|
-
return evaluateExpr({ node: whenClause.result, row, tables })
|
|
525
|
+
return evaluateExpr({ node: whenClause.result, row, tables, rowIndex })
|
|
365
526
|
}
|
|
366
527
|
}
|
|
367
528
|
|
|
368
529
|
// No WHEN clause matched, return ELSE result or NULL
|
|
369
530
|
if (node.elseResult) {
|
|
370
|
-
return evaluateExpr({ node: node.elseResult, row, tables })
|
|
531
|
+
return evaluateExpr({ node: node.elseResult, row, tables, rowIndex })
|
|
371
532
|
}
|
|
372
533
|
return null
|
|
373
534
|
}
|
|
@@ -378,6 +539,9 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
378
539
|
throw invalidContextError({
|
|
379
540
|
item: 'INTERVAL',
|
|
380
541
|
validContext: 'date arithmetic (+ or -)',
|
|
542
|
+
positionStart: node.positionStart,
|
|
543
|
+
positionEnd: node.positionEnd,
|
|
544
|
+
rowNumber: rowIndex,
|
|
381
545
|
})
|
|
382
546
|
}
|
|
383
547
|
|
package/src/execute/having.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { unknownFunctionError } from '../
|
|
1
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
2
|
import { isAggregateFunc } from '../validation.js'
|
|
3
3
|
import { evaluateExpr } from './expression.js'
|
|
4
4
|
import { applyBinaryOp } from './utils.js'
|
|
@@ -153,5 +153,10 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
|
|
|
153
153
|
return max
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
throw unknownFunctionError(
|
|
156
|
+
throw unknownFunctionError({
|
|
157
|
+
funcName,
|
|
158
|
+
positionStart: 0,
|
|
159
|
+
positionEnd: 0,
|
|
160
|
+
validFunctions: 'COUNT, SUM, AVG, MIN, MAX',
|
|
161
|
+
})
|
|
157
162
|
}
|
package/src/execute/join.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { missingClauseError
|
|
1
|
+
import { missingClauseError } from '../parseErrors.js'
|
|
2
|
+
import { tableNotFoundError } from '../executionErrors.js'
|
|
2
3
|
import { evaluateExpr } from './expression.js'
|
|
3
4
|
import { stringify } from './utils.js'
|
|
4
5
|
|
|
@@ -23,7 +24,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
23
24
|
const join = joins[0]
|
|
24
25
|
const rightSource = tables[join.table]
|
|
25
26
|
if (rightSource === undefined) {
|
|
26
|
-
throw tableNotFoundError(join.table)
|
|
27
|
+
throw tableNotFoundError({ tableName: join.table })
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Buffer right rows for hash index (required for hash join)
|
|
@@ -63,7 +64,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
63
64
|
const join = joins[i]
|
|
64
65
|
const rightSource = tables[join.table]
|
|
65
66
|
if (rightSource === undefined) {
|
|
66
|
-
throw tableNotFoundError(join.table)
|
|
67
|
+
throw tableNotFoundError({ tableName: join.table })
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/** @type {AsyncRow[]} */
|
|
@@ -99,7 +100,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
99
100
|
const lastJoin = joins[joins.length - 1]
|
|
100
101
|
const rightSource = tables[lastJoin.table]
|
|
101
102
|
if (rightSource === undefined) {
|
|
102
|
-
throw tableNotFoundError(lastJoin.table)
|
|
103
|
+
throw tableNotFoundError({ tableName: lastJoin.table })
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/** @type {AsyncRow[]} */
|