squirreling 0.4.7 → 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/README.md +1 -0
- package/package.json +3 -3
- package/src/execute/aggregates.js +12 -5
- package/src/execute/date.js +57 -0
- package/src/execute/execute.js +20 -7
- package/src/execute/expression.js +268 -40
- package/src/execute/having.js +7 -1
- package/src/execute/join.js +9 -4
- package/src/execute/math.js +165 -0
- package/src/execute/utils.js +3 -0
- package/src/executionErrors.js +62 -0
- package/src/index.js +1 -0
- package/src/parse/comparison.js +41 -7
- package/src/parse/expression.js +121 -10
- package/src/parse/parse.js +6 -1
- package/src/parse/state.js +16 -4
- package/src/parse/tokenize.js +113 -48
- package/src/parseErrors.js +117 -0
- package/src/types.d.ts +58 -14
- package/src/validation.js +23 -1
- package/src/validationErrors.js +127 -0
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
|
+
import { invalidContextError } from '../executionErrors.js'
|
|
3
|
+
import {
|
|
4
|
+
argCountError,
|
|
5
|
+
argValueError,
|
|
6
|
+
castError,
|
|
7
|
+
} from '../validationErrors.js'
|
|
8
|
+
import { isMathFunc } from '../validation.js'
|
|
9
|
+
import { applyIntervalToDate } from './date.js'
|
|
1
10
|
import { executeSelect } from './execute.js'
|
|
11
|
+
import { evaluateMathFunc } from './math.js'
|
|
2
12
|
import { applyBinaryOp, stringify } from './utils.js'
|
|
3
13
|
|
|
4
14
|
/**
|
|
5
|
-
* @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
|
|
15
|
+
* @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, IntervalUnit } from '../types.js'
|
|
6
16
|
*/
|
|
7
17
|
|
|
8
18
|
/**
|
|
@@ -12,9 +22,10 @@ import { applyBinaryOp, stringify } from './utils.js'
|
|
|
12
22
|
* @param {ExprNode} params.node - The expression node to evaluate
|
|
13
23
|
* @param {AsyncRow} params.row - The data row to evaluate against
|
|
14
24
|
* @param {Record<string, AsyncDataSource>} params.tables
|
|
25
|
+
* @param {number} [params.rowIndex] - 1-based row index for error reporting
|
|
15
26
|
* @returns {Promise<SqlPrimitive>} The result of the evaluation
|
|
16
27
|
*/
|
|
17
|
-
export async function evaluateExpr({ node, row, tables }) {
|
|
28
|
+
export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
18
29
|
if (node.type === 'literal') {
|
|
19
30
|
return node.value
|
|
20
31
|
}
|
|
@@ -31,7 +42,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
31
42
|
return row[colName]()
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
|
-
return
|
|
45
|
+
return null
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
// Scalar subquery - returns a single value
|
|
@@ -49,16 +60,16 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
49
60
|
// Unary operators
|
|
50
61
|
if (node.type === 'unary') {
|
|
51
62
|
if (node.op === 'NOT') {
|
|
52
|
-
return !await evaluateExpr({ node: node.argument, row, tables })
|
|
63
|
+
return !await evaluateExpr({ node: node.argument, row, tables, rowIndex })
|
|
53
64
|
}
|
|
54
65
|
if (node.op === 'IS NULL') {
|
|
55
|
-
return await evaluateExpr({ node: node.argument, row, tables }) == null
|
|
66
|
+
return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) == null
|
|
56
67
|
}
|
|
57
68
|
if (node.op === 'IS NOT NULL') {
|
|
58
|
-
return await evaluateExpr({ node: node.argument, row, tables }) != null
|
|
69
|
+
return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) != null
|
|
59
70
|
}
|
|
60
71
|
if (node.op === '-') {
|
|
61
|
-
const val = await evaluateExpr({ node: node.argument, row, tables })
|
|
72
|
+
const val = await evaluateExpr({ node: node.argument, row, tables, rowIndex })
|
|
62
73
|
if (val == null) return null
|
|
63
74
|
return -val
|
|
64
75
|
}
|
|
@@ -66,7 +77,17 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
66
77
|
|
|
67
78
|
// Binary operators
|
|
68
79
|
if (node.type === 'binary') {
|
|
69
|
-
|
|
80
|
+
// Handle date +/- interval at AST level
|
|
81
|
+
if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
|
|
82
|
+
const dateVal = await evaluateExpr({ node: node.left, row, tables, rowIndex })
|
|
83
|
+
return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
|
|
84
|
+
}
|
|
85
|
+
if (node.op === '+' && node.left.type === 'interval') {
|
|
86
|
+
const dateVal = await evaluateExpr({ node: node.right, row, tables, rowIndex })
|
|
87
|
+
return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const left = await evaluateExpr({ node: node.left, row, tables, rowIndex })
|
|
70
91
|
|
|
71
92
|
// Short-circuit evaluation for AND and OR
|
|
72
93
|
if (node.op === 'AND') {
|
|
@@ -76,7 +97,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
76
97
|
if (left) return true
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
const right = await evaluateExpr({ node: node.right, row, tables })
|
|
100
|
+
const right = await evaluateExpr({ node: node.right, row, tables, rowIndex })
|
|
80
101
|
return applyBinaryOp(node.op, left, right)
|
|
81
102
|
}
|
|
82
103
|
|
|
@@ -84,34 +105,77 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
84
105
|
if (node.type === 'function') {
|
|
85
106
|
const funcName = node.name.toUpperCase()
|
|
86
107
|
/** @type {SqlPrimitive[]} */
|
|
87
|
-
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 })))
|
|
88
109
|
|
|
89
110
|
if (funcName === 'UPPER') {
|
|
90
|
-
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
|
+
}
|
|
91
121
|
const val = args[0]
|
|
92
122
|
if (val == null) return null
|
|
93
123
|
return String(val).toUpperCase()
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
if (funcName === 'LOWER') {
|
|
97
|
-
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
|
+
}
|
|
98
137
|
const val = args[0]
|
|
99
138
|
if (val == null) return null
|
|
100
139
|
return String(val).toLowerCase()
|
|
101
140
|
}
|
|
102
141
|
|
|
103
142
|
if (funcName === 'CONCAT') {
|
|
104
|
-
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
|
+
}
|
|
105
153
|
// SQL CONCAT returns NULL if any argument is NULL
|
|
106
154
|
if (args.some(a => a == null)) return null
|
|
107
155
|
if (args.some(a => typeof a === 'object')) {
|
|
108
|
-
throw
|
|
156
|
+
throw argValueError({
|
|
157
|
+
funcName: 'CONCAT',
|
|
158
|
+
message: 'does not support object arguments',
|
|
159
|
+
positionStart: node.positionStart,
|
|
160
|
+
positionEnd: node.positionEnd,
|
|
161
|
+
hint: 'Use CAST to convert objects to strings first.',
|
|
162
|
+
rowNumber: rowIndex,
|
|
163
|
+
})
|
|
109
164
|
}
|
|
110
165
|
return args.map(a => String(a)).join('')
|
|
111
166
|
}
|
|
112
167
|
|
|
113
168
|
if (funcName === 'LENGTH') {
|
|
114
|
-
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
|
+
}
|
|
115
179
|
const val = args[0]
|
|
116
180
|
if (val == null) return null
|
|
117
181
|
return String(val).length
|
|
@@ -119,21 +183,41 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
119
183
|
|
|
120
184
|
if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
|
|
121
185
|
if (args.length < 2 || args.length > 3) {
|
|
122
|
-
throw
|
|
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
|
+
})
|
|
123
194
|
}
|
|
124
195
|
const str = args[0]
|
|
125
196
|
if (str == null) return null
|
|
126
197
|
const strVal = String(str)
|
|
127
198
|
const start = Number(args[1])
|
|
128
199
|
if (!Number.isInteger(start) || start < 1) {
|
|
129
|
-
throw
|
|
200
|
+
throw argValueError({
|
|
201
|
+
funcName,
|
|
202
|
+
message: `start position must be a positive integer, got ${args[1]}`,
|
|
203
|
+
positionStart: node.positionStart,
|
|
204
|
+
positionEnd: node.positionEnd,
|
|
205
|
+
hint: 'SQL uses 1-based indexing.',
|
|
206
|
+
rowNumber: rowIndex,
|
|
207
|
+
})
|
|
130
208
|
}
|
|
131
209
|
// SQL uses 1-based indexing
|
|
132
210
|
const startIdx = start - 1
|
|
133
211
|
if (args.length === 3) {
|
|
134
212
|
const len = Number(args[2])
|
|
135
213
|
if (!Number.isInteger(len) || len < 0) {
|
|
136
|
-
throw
|
|
214
|
+
throw argValueError({
|
|
215
|
+
funcName,
|
|
216
|
+
message: `length must be a non-negative integer, got ${args[2]}`,
|
|
217
|
+
positionStart: node.positionStart,
|
|
218
|
+
positionEnd: node.positionEnd,
|
|
219
|
+
rowNumber: rowIndex,
|
|
220
|
+
})
|
|
137
221
|
}
|
|
138
222
|
return strVal.substring(startIdx, startIdx + len)
|
|
139
223
|
}
|
|
@@ -141,14 +225,32 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
141
225
|
}
|
|
142
226
|
|
|
143
227
|
if (funcName === 'TRIM') {
|
|
144
|
-
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
|
+
}
|
|
145
238
|
const val = args[0]
|
|
146
239
|
if (val == null) return null
|
|
147
240
|
return String(val).trim()
|
|
148
241
|
}
|
|
149
242
|
|
|
150
243
|
if (funcName === 'REPLACE') {
|
|
151
|
-
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
|
+
}
|
|
152
254
|
const str = args[0]
|
|
153
255
|
const searchStr = args[1]
|
|
154
256
|
const replaceStr = args[2]
|
|
@@ -158,13 +260,71 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
158
260
|
}
|
|
159
261
|
|
|
160
262
|
if (funcName === 'RANDOM' || funcName === 'RAND') {
|
|
161
|
-
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
|
+
}
|
|
162
273
|
return Math.random()
|
|
163
274
|
}
|
|
164
275
|
|
|
276
|
+
if (funcName === 'CURRENT_DATE') {
|
|
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
|
+
}
|
|
287
|
+
return new Date().toISOString().split('T')[0]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (funcName === 'CURRENT_TIME') {
|
|
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
|
+
}
|
|
301
|
+
return new Date().toISOString().split('T')[1].replace('Z', '')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (funcName === 'CURRENT_TIMESTAMP') {
|
|
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
|
+
}
|
|
315
|
+
return new Date().toISOString()
|
|
316
|
+
}
|
|
317
|
+
|
|
165
318
|
if (funcName === 'JSON_OBJECT') {
|
|
166
319
|
if (args.length % 2 !== 0) {
|
|
167
|
-
throw
|
|
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
|
+
})
|
|
168
328
|
}
|
|
169
329
|
/** @type {Record<string, SqlPrimitive>} */
|
|
170
330
|
const result = {}
|
|
@@ -172,7 +332,14 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
172
332
|
const key = args[i]
|
|
173
333
|
const value = args[i + 1]
|
|
174
334
|
if (key == null) {
|
|
175
|
-
throw
|
|
335
|
+
throw argValueError({
|
|
336
|
+
funcName: 'JSON_OBJECT',
|
|
337
|
+
message: 'key cannot be null',
|
|
338
|
+
positionStart: node.positionStart,
|
|
339
|
+
positionEnd: node.positionEnd,
|
|
340
|
+
hint: 'All keys must be non-null values.',
|
|
341
|
+
rowNumber: rowIndex,
|
|
342
|
+
})
|
|
176
343
|
}
|
|
177
344
|
result[String(key)] = value
|
|
178
345
|
}
|
|
@@ -180,7 +347,16 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
180
347
|
}
|
|
181
348
|
|
|
182
349
|
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
|
|
183
|
-
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
|
+
}
|
|
184
360
|
let jsonArg = args[0]
|
|
185
361
|
const pathArg = args[1]
|
|
186
362
|
if (jsonArg == null || pathArg == null) return null
|
|
@@ -190,11 +366,24 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
190
366
|
try {
|
|
191
367
|
jsonArg = JSON.parse(jsonArg)
|
|
192
368
|
} catch {
|
|
193
|
-
throw
|
|
369
|
+
throw argValueError({
|
|
370
|
+
funcName,
|
|
371
|
+
message: 'invalid JSON string',
|
|
372
|
+
positionStart: node.positionStart,
|
|
373
|
+
positionEnd: node.positionEnd,
|
|
374
|
+
hint: 'First argument must be valid JSON.',
|
|
375
|
+
rowNumber: rowIndex,
|
|
376
|
+
})
|
|
194
377
|
}
|
|
195
378
|
}
|
|
196
|
-
if (typeof jsonArg !== 'object') {
|
|
197
|
-
throw
|
|
379
|
+
if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
|
|
380
|
+
throw argValueError({
|
|
381
|
+
funcName,
|
|
382
|
+
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
383
|
+
positionStart: node.positionStart,
|
|
384
|
+
positionEnd: node.positionEnd,
|
|
385
|
+
rowNumber: rowIndex,
|
|
386
|
+
})
|
|
198
387
|
}
|
|
199
388
|
|
|
200
389
|
// Parse path ("$.foo.bar[0].baz" or "foo.bar[0]")
|
|
@@ -223,11 +412,25 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
223
412
|
return current
|
|
224
413
|
}
|
|
225
414
|
|
|
226
|
-
|
|
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
|
+
})
|
|
227
430
|
}
|
|
228
431
|
|
|
229
432
|
if (node.type === 'cast') {
|
|
230
|
-
const val = await evaluateExpr({ node: node.expr, row, tables })
|
|
433
|
+
const val = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
231
434
|
if (val == null) return null
|
|
232
435
|
const toType = node.toType.toUpperCase()
|
|
233
436
|
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
@@ -235,7 +438,15 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
235
438
|
return String(val)
|
|
236
439
|
}
|
|
237
440
|
// Can only cast primitives to other primitive types
|
|
238
|
-
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
|
+
}
|
|
239
450
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
240
451
|
const num = Number(val)
|
|
241
452
|
if (isNaN(num)) return null
|
|
@@ -252,21 +463,26 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
252
463
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
253
464
|
return Boolean(val)
|
|
254
465
|
}
|
|
255
|
-
throw
|
|
466
|
+
throw castError({
|
|
467
|
+
toType: node.toType,
|
|
468
|
+
positionStart: node.positionStart,
|
|
469
|
+
positionEnd: node.positionEnd,
|
|
470
|
+
rowNumber: rowIndex,
|
|
471
|
+
})
|
|
256
472
|
}
|
|
257
473
|
|
|
258
474
|
// IN and NOT IN with value lists
|
|
259
475
|
if (node.type === 'in valuelist') {
|
|
260
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
476
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
261
477
|
for (const valueNode of node.values) {
|
|
262
|
-
const val = await evaluateExpr({ node: valueNode, row, tables })
|
|
478
|
+
const val = await evaluateExpr({ node: valueNode, row, tables, rowIndex })
|
|
263
479
|
if (exprVal === val) return true
|
|
264
480
|
}
|
|
265
481
|
return false
|
|
266
482
|
}
|
|
267
483
|
// IN with subqueries
|
|
268
484
|
if (node.type === 'in') {
|
|
269
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
485
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
270
486
|
const results = executeSelect(node.subquery, tables)
|
|
271
487
|
/** @type {SqlPrimitive[]} */
|
|
272
488
|
const values = []
|
|
@@ -291,31 +507,43 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
291
507
|
// CASE expressions
|
|
292
508
|
if (node.type === 'case') {
|
|
293
509
|
// For simple CASE: evaluate the case expression once
|
|
294
|
-
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
|
|
510
|
+
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, rowIndex })
|
|
295
511
|
|
|
296
512
|
// Iterate through WHEN clauses
|
|
297
513
|
for (const whenClause of node.whenClauses) {
|
|
298
514
|
let conditionResult
|
|
299
515
|
if (caseValue !== undefined) {
|
|
300
516
|
// Simple CASE: compare caseValue with condition
|
|
301
|
-
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables })
|
|
517
|
+
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
|
|
302
518
|
conditionResult = caseValue === whenValue
|
|
303
519
|
} else {
|
|
304
520
|
// Searched CASE: evaluate condition as boolean
|
|
305
|
-
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables })
|
|
521
|
+
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
|
|
306
522
|
}
|
|
307
523
|
|
|
308
524
|
if (conditionResult) {
|
|
309
|
-
return evaluateExpr({ node: whenClause.result, row, tables })
|
|
525
|
+
return evaluateExpr({ node: whenClause.result, row, tables, rowIndex })
|
|
310
526
|
}
|
|
311
527
|
}
|
|
312
528
|
|
|
313
529
|
// No WHEN clause matched, return ELSE result or NULL
|
|
314
530
|
if (node.elseResult) {
|
|
315
|
-
return evaluateExpr({ node: node.elseResult, row, tables })
|
|
531
|
+
return evaluateExpr({ node: node.elseResult, row, tables, rowIndex })
|
|
316
532
|
}
|
|
317
533
|
return null
|
|
318
534
|
}
|
|
319
535
|
|
|
320
|
-
|
|
536
|
+
// INTERVAL expressions should only appear as part of binary +/- operations
|
|
537
|
+
// which are handled above. A standalone interval is an error.
|
|
538
|
+
if (node.type === 'interval') {
|
|
539
|
+
throw invalidContextError({
|
|
540
|
+
item: 'INTERVAL',
|
|
541
|
+
validContext: 'date arithmetic (+ or -)',
|
|
542
|
+
positionStart: node.positionStart,
|
|
543
|
+
positionEnd: node.positionEnd,
|
|
544
|
+
rowNumber: rowIndex,
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
throw new Error(`Unknown expression node type: ${node.type}. This is an internal error - the query may contain unsupported syntax.`)
|
|
321
549
|
}
|
package/src/execute/having.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
1
2
|
import { isAggregateFunc } from '../validation.js'
|
|
2
3
|
import { evaluateExpr } from './expression.js'
|
|
3
4
|
import { applyBinaryOp } from './utils.js'
|
|
@@ -152,5 +153,10 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
|
|
|
152
153
|
return max
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
throw
|
|
156
|
+
throw unknownFunctionError({
|
|
157
|
+
funcName,
|
|
158
|
+
positionStart: 0,
|
|
159
|
+
positionEnd: 0,
|
|
160
|
+
validFunctions: 'COUNT, SUM, AVG, MIN, MAX',
|
|
161
|
+
})
|
|
156
162
|
}
|
package/src/execute/join.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { missingClauseError } from '../parseErrors.js'
|
|
2
|
+
import { tableNotFoundError } from '../executionErrors.js'
|
|
1
3
|
import { evaluateExpr } from './expression.js'
|
|
2
4
|
import { stringify } from './utils.js'
|
|
3
5
|
|
|
@@ -22,7 +24,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
22
24
|
const join = joins[0]
|
|
23
25
|
const rightSource = tables[join.table]
|
|
24
26
|
if (rightSource === undefined) {
|
|
25
|
-
throw
|
|
27
|
+
throw tableNotFoundError({ tableName: join.table })
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
// Buffer right rows for hash index (required for hash join)
|
|
@@ -62,7 +64,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
62
64
|
const join = joins[i]
|
|
63
65
|
const rightSource = tables[join.table]
|
|
64
66
|
if (rightSource === undefined) {
|
|
65
|
-
throw
|
|
67
|
+
throw tableNotFoundError({ tableName: join.table })
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** @type {AsyncRow[]} */
|
|
@@ -98,7 +100,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
98
100
|
const lastJoin = joins[joins.length - 1]
|
|
99
101
|
const rightSource = tables[lastJoin.table]
|
|
100
102
|
if (rightSource === undefined) {
|
|
101
|
-
throw
|
|
103
|
+
throw tableNotFoundError({ tableName: lastJoin.table })
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/** @type {AsyncRow[]} */
|
|
@@ -234,7 +236,10 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
234
236
|
const { joinType, on: onCondition } = join
|
|
235
237
|
|
|
236
238
|
if (!onCondition) {
|
|
237
|
-
throw
|
|
239
|
+
throw missingClauseError({
|
|
240
|
+
missing: 'ON condition',
|
|
241
|
+
context: 'JOIN',
|
|
242
|
+
})
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
const keys = extractJoinKeys(onCondition, leftTable, rightTable)
|