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
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
argCountError,
|
|
3
|
+
argValueError,
|
|
4
|
+
castError,
|
|
5
|
+
invalidContextError,
|
|
6
|
+
unknownFunctionError,
|
|
7
|
+
} from '../errors.js'
|
|
8
|
+
import { applyIntervalToDate } from './date.js'
|
|
1
9
|
import { executeSelect } from './execute.js'
|
|
2
|
-
import { applyBinaryOp } from './utils.js'
|
|
10
|
+
import { applyBinaryOp, stringify } from './utils.js'
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
|
-
* @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
|
|
13
|
+
* @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, IntervalUnit } from '../types.js'
|
|
6
14
|
*/
|
|
7
15
|
|
|
8
16
|
/**
|
|
@@ -31,7 +39,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
31
39
|
return row[colName]()
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
|
-
return
|
|
42
|
+
return null
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
// Scalar subquery - returns a single value
|
|
@@ -66,6 +74,16 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
66
74
|
|
|
67
75
|
// Binary operators
|
|
68
76
|
if (node.type === 'binary') {
|
|
77
|
+
// Handle date +/- interval at AST level
|
|
78
|
+
if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
|
|
79
|
+
const dateVal = await evaluateExpr({ node: node.left, row, tables })
|
|
80
|
+
return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
|
|
81
|
+
}
|
|
82
|
+
if (node.op === '+' && node.left.type === 'interval') {
|
|
83
|
+
const dateVal = await evaluateExpr({ node: node.right, row, tables })
|
|
84
|
+
return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
const left = await evaluateExpr({ node: node.left, row, tables })
|
|
70
88
|
|
|
71
89
|
// Short-circuit evaluation for AND and OR
|
|
@@ -83,33 +101,39 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
83
101
|
// Function calls
|
|
84
102
|
if (node.type === 'function') {
|
|
85
103
|
const funcName = node.name.toUpperCase()
|
|
104
|
+
/** @type {SqlPrimitive[]} */
|
|
86
105
|
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
|
|
87
106
|
|
|
88
107
|
if (funcName === 'UPPER') {
|
|
89
|
-
if (args.length !== 1) throw
|
|
108
|
+
if (args.length !== 1) throw argCountError('UPPER', 1, args.length)
|
|
90
109
|
const val = args[0]
|
|
91
110
|
if (val == null) return null
|
|
92
111
|
return String(val).toUpperCase()
|
|
93
112
|
}
|
|
94
113
|
|
|
95
114
|
if (funcName === 'LOWER') {
|
|
96
|
-
if (args.length !== 1) throw
|
|
115
|
+
if (args.length !== 1) throw argCountError('LOWER', 1, args.length)
|
|
97
116
|
const val = args[0]
|
|
98
117
|
if (val == null) return null
|
|
99
118
|
return String(val).toLowerCase()
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
if (funcName === 'CONCAT') {
|
|
103
|
-
if (args.length < 1) throw
|
|
122
|
+
if (args.length < 1) throw argCountError('CONCAT', 'at least 1', args.length)
|
|
104
123
|
// SQL CONCAT returns NULL if any argument is NULL
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
if (args.some(a => a == null)) return null
|
|
125
|
+
if (args.some(a => typeof a === 'object')) {
|
|
126
|
+
throw argValueError({
|
|
127
|
+
funcName: 'CONCAT',
|
|
128
|
+
message: 'does not support object arguments',
|
|
129
|
+
hint: 'Use CAST to convert objects to strings first.',
|
|
130
|
+
})
|
|
107
131
|
}
|
|
108
132
|
return args.map(a => String(a)).join('')
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
if (funcName === 'LENGTH') {
|
|
112
|
-
if (args.length !== 1) throw
|
|
136
|
+
if (args.length !== 1) throw argCountError('LENGTH', 1, args.length)
|
|
113
137
|
const val = args[0]
|
|
114
138
|
if (val == null) return null
|
|
115
139
|
return String(val).length
|
|
@@ -117,21 +141,28 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
117
141
|
|
|
118
142
|
if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
|
|
119
143
|
if (args.length < 2 || args.length > 3) {
|
|
120
|
-
throw
|
|
144
|
+
throw argCountError(funcName, '2 or 3', args.length)
|
|
121
145
|
}
|
|
122
146
|
const str = args[0]
|
|
123
147
|
if (str == null) return null
|
|
124
148
|
const strVal = String(str)
|
|
125
149
|
const start = Number(args[1])
|
|
126
150
|
if (!Number.isInteger(start) || start < 1) {
|
|
127
|
-
throw
|
|
151
|
+
throw argValueError({
|
|
152
|
+
funcName,
|
|
153
|
+
message: `start position must be a positive integer, got ${args[1]}`,
|
|
154
|
+
hint: 'SQL uses 1-based indexing.',
|
|
155
|
+
})
|
|
128
156
|
}
|
|
129
157
|
// SQL uses 1-based indexing
|
|
130
158
|
const startIdx = start - 1
|
|
131
159
|
if (args.length === 3) {
|
|
132
160
|
const len = Number(args[2])
|
|
133
161
|
if (!Number.isInteger(len) || len < 0) {
|
|
134
|
-
throw
|
|
162
|
+
throw argValueError({
|
|
163
|
+
funcName,
|
|
164
|
+
message: `length must be a non-negative integer, got ${args[2]}`,
|
|
165
|
+
})
|
|
135
166
|
}
|
|
136
167
|
return strVal.substring(startIdx, startIdx + len)
|
|
137
168
|
}
|
|
@@ -139,14 +170,14 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
if (funcName === 'TRIM') {
|
|
142
|
-
if (args.length !== 1) throw
|
|
173
|
+
if (args.length !== 1) throw argCountError('TRIM', 1, args.length)
|
|
143
174
|
const val = args[0]
|
|
144
175
|
if (val == null) return null
|
|
145
176
|
return String(val).trim()
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
if (funcName === 'REPLACE') {
|
|
149
|
-
if (args.length !== 3) throw
|
|
180
|
+
if (args.length !== 3) throw argCountError('REPLACE', 3, args.length)
|
|
150
181
|
const str = args[0]
|
|
151
182
|
const searchStr = args[1]
|
|
152
183
|
const replaceStr = args[2]
|
|
@@ -156,17 +187,110 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
156
187
|
}
|
|
157
188
|
|
|
158
189
|
if (funcName === 'RANDOM' || funcName === 'RAND') {
|
|
159
|
-
if (args.length !== 0) throw
|
|
190
|
+
if (args.length !== 0) throw argCountError(funcName, 0, args.length)
|
|
160
191
|
return Math.random()
|
|
161
192
|
}
|
|
162
193
|
|
|
163
|
-
|
|
194
|
+
if (funcName === 'CURRENT_DATE') {
|
|
195
|
+
if (args.length !== 0) throw argCountError('CURRENT_DATE', 0, args.length)
|
|
196
|
+
return new Date().toISOString().split('T')[0]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (funcName === 'CURRENT_TIME') {
|
|
200
|
+
if (args.length !== 0) throw argCountError('CURRENT_TIME', 0, args.length)
|
|
201
|
+
return new Date().toISOString().split('T')[1].replace('Z', '')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (funcName === 'CURRENT_TIMESTAMP') {
|
|
205
|
+
if (args.length !== 0) throw argCountError('CURRENT_TIMESTAMP', 0, args.length)
|
|
206
|
+
return new Date().toISOString()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (funcName === 'JSON_OBJECT') {
|
|
210
|
+
if (args.length % 2 !== 0) {
|
|
211
|
+
throw argCountError('JSON_OBJECT', 'even number', args.length)
|
|
212
|
+
}
|
|
213
|
+
/** @type {Record<string, SqlPrimitive>} */
|
|
214
|
+
const result = {}
|
|
215
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
216
|
+
const key = args[i]
|
|
217
|
+
const value = args[i + 1]
|
|
218
|
+
if (key == null) {
|
|
219
|
+
throw argValueError({
|
|
220
|
+
funcName: 'JSON_OBJECT',
|
|
221
|
+
message: 'key cannot be null',
|
|
222
|
+
hint: 'All keys must be non-null values.',
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
result[String(key)] = value
|
|
226
|
+
}
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
|
|
231
|
+
if (args.length !== 2) throw argCountError(funcName, 2, args.length)
|
|
232
|
+
let jsonArg = args[0]
|
|
233
|
+
const pathArg = args[1]
|
|
234
|
+
if (jsonArg == null || pathArg == null) return null
|
|
235
|
+
|
|
236
|
+
// Parse JSON if string, otherwise use directly
|
|
237
|
+
if (typeof jsonArg === 'string') {
|
|
238
|
+
try {
|
|
239
|
+
jsonArg = JSON.parse(jsonArg)
|
|
240
|
+
} catch {
|
|
241
|
+
throw argValueError({
|
|
242
|
+
funcName,
|
|
243
|
+
message: 'invalid JSON string',
|
|
244
|
+
hint: 'First argument must be valid JSON.',
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
|
|
249
|
+
throw argValueError({
|
|
250
|
+
funcName,
|
|
251
|
+
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Parse path ("$.foo.bar[0].baz" or "foo.bar[0]")
|
|
256
|
+
const path = String(pathArg)
|
|
257
|
+
const normalizedPath = path.startsWith('$') ? path.slice(1) : path
|
|
258
|
+
|
|
259
|
+
// Navigate the path
|
|
260
|
+
let current = jsonArg
|
|
261
|
+
const segments = normalizedPath.match(/\.?([^.[]+)|\[(\d+)\]/g) || []
|
|
262
|
+
for (const segment of segments) {
|
|
263
|
+
if (current == null) return null
|
|
264
|
+
if (segment.startsWith('[')) {
|
|
265
|
+
// Array index access
|
|
266
|
+
const index = parseInt(segment.slice(1, -1), 10)
|
|
267
|
+
if (!Array.isArray(current)) return null
|
|
268
|
+
current = current[index]
|
|
269
|
+
} else {
|
|
270
|
+
// Property access
|
|
271
|
+
const key = segment.startsWith('.') ? segment.slice(1) : segment
|
|
272
|
+
if (typeof current !== 'object' || Array.isArray(current)) return null
|
|
273
|
+
current = current[key]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (current == null) return null
|
|
278
|
+
return current
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw unknownFunctionError(funcName)
|
|
164
282
|
}
|
|
165
283
|
|
|
166
284
|
if (node.type === 'cast') {
|
|
167
285
|
const val = await evaluateExpr({ node: node.expr, row, tables })
|
|
168
286
|
if (val == null) return null
|
|
169
287
|
const toType = node.toType.toUpperCase()
|
|
288
|
+
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
289
|
+
if (typeof val === 'object') return stringify(val)
|
|
290
|
+
return String(val)
|
|
291
|
+
}
|
|
292
|
+
// Can only cast primitives to other primitive types
|
|
293
|
+
if (typeof val === 'object') throw castError(node.toType, 'object')
|
|
170
294
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
171
295
|
const num = Number(val)
|
|
172
296
|
if (isNaN(num)) return null
|
|
@@ -180,13 +304,10 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
180
304
|
if (isNaN(num)) return null
|
|
181
305
|
return num
|
|
182
306
|
}
|
|
183
|
-
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
184
|
-
return String(val)
|
|
185
|
-
}
|
|
186
307
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
187
308
|
return Boolean(val)
|
|
188
309
|
}
|
|
189
|
-
throw
|
|
310
|
+
throw castError(node.toType)
|
|
190
311
|
}
|
|
191
312
|
|
|
192
313
|
// IN and NOT IN with value lists
|
|
@@ -251,5 +372,14 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
251
372
|
return null
|
|
252
373
|
}
|
|
253
374
|
|
|
254
|
-
|
|
375
|
+
// INTERVAL expressions should only appear as part of binary +/- operations
|
|
376
|
+
// which are handled above. A standalone interval is an error.
|
|
377
|
+
if (node.type === 'interval') {
|
|
378
|
+
throw invalidContextError({
|
|
379
|
+
item: 'INTERVAL',
|
|
380
|
+
validContext: 'date arithmetic (+ or -)',
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
throw new Error(`Unknown expression node type: ${node.type}. This is an internal error - the query may contain unsupported syntax.`)
|
|
255
385
|
}
|
package/src/execute/having.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unknownFunctionError } from '../errors.js'
|
|
1
2
|
import { isAggregateFunc } from '../validation.js'
|
|
2
3
|
import { evaluateExpr } from './expression.js'
|
|
3
4
|
import { applyBinaryOp } from './utils.js'
|
|
@@ -41,7 +42,7 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const right = await evaluateHavingValue(expr.right, context, group, tables)
|
|
44
|
-
return applyBinaryOp(expr.op, left, right)
|
|
45
|
+
return Boolean(applyBinaryOp(expr.op, left, right))
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
if (expr.type === 'unary') {
|
|
@@ -152,5 +153,5 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
|
|
|
152
153
|
return max
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
throw
|
|
156
|
+
throw unknownFunctionError(funcName, undefined, 'COUNT, SUM, AVG, MIN, MAX')
|
|
156
157
|
}
|
package/src/execute/join.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { missingClauseError, tableNotFoundError } from '../errors.js'
|
|
1
2
|
import { evaluateExpr } from './expression.js'
|
|
3
|
+
import { stringify } from './utils.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode } from '../types.js'
|
|
@@ -21,7 +23,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
21
23
|
const join = joins[0]
|
|
22
24
|
const rightSource = tables[join.table]
|
|
23
25
|
if (rightSource === undefined) {
|
|
24
|
-
throw
|
|
26
|
+
throw tableNotFoundError(join.table)
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// Buffer right rows for hash index (required for hash join)
|
|
@@ -61,7 +63,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
61
63
|
const join = joins[i]
|
|
62
64
|
const rightSource = tables[join.table]
|
|
63
65
|
if (rightSource === undefined) {
|
|
64
|
-
throw
|
|
66
|
+
throw tableNotFoundError(join.table)
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/** @type {AsyncRow[]} */
|
|
@@ -97,7 +99,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
97
99
|
const lastJoin = joins[joins.length - 1]
|
|
98
100
|
const rightSource = tables[lastJoin.table]
|
|
99
101
|
if (rightSource === undefined) {
|
|
100
|
-
throw
|
|
102
|
+
throw tableNotFoundError(lastJoin.table)
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/** @type {AsyncRow[]} */
|
|
@@ -233,7 +235,10 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
233
235
|
const { joinType, on: onCondition } = join
|
|
234
236
|
|
|
235
237
|
if (!onCondition) {
|
|
236
|
-
throw
|
|
238
|
+
throw missingClauseError({
|
|
239
|
+
missing: 'ON condition',
|
|
240
|
+
context: 'JOIN',
|
|
241
|
+
})
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
const keys = extractJoinKeys(onCondition, leftTable, rightTable)
|
|
@@ -258,8 +263,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
258
263
|
for (const rightRow of rightRows) {
|
|
259
264
|
const keyValue = await evaluateExpr({ node: keys.rightKey, row: rightRow, tables })
|
|
260
265
|
if (keyValue == null) continue // NULL keys never match
|
|
261
|
-
const keyStr =
|
|
262
|
-
|
|
266
|
+
const keyStr = stringify(keyValue)
|
|
263
267
|
let bucket = hashMap.get(keyStr)
|
|
264
268
|
if (!bucket) {
|
|
265
269
|
bucket = []
|
|
@@ -283,7 +287,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
283
287
|
}
|
|
284
288
|
|
|
285
289
|
const keyValue = await evaluateExpr({ node: keys.leftKey, row: leftRow, tables })
|
|
286
|
-
const keyStr =
|
|
290
|
+
const keyStr = stringify(keyValue)
|
|
287
291
|
|
|
288
292
|
const matchingRightRows = hashMap.get(keyStr)
|
|
289
293
|
|
package/src/execute/utils.js
CHANGED
|
@@ -3,14 +3,27 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Applies a binary operator to two values, handling nulls according to SQL semantics
|
|
7
7
|
*
|
|
8
8
|
* @param {BinaryOp} op
|
|
9
9
|
* @param {SqlPrimitive} a
|
|
10
10
|
* @param {SqlPrimitive} b
|
|
11
|
-
* @returns {
|
|
11
|
+
* @returns {SqlPrimitive}
|
|
12
12
|
*/
|
|
13
13
|
export function applyBinaryOp(op, a, b) {
|
|
14
|
+
// Arithmetic operators return null if either operand is null
|
|
15
|
+
if (op === '+' || op === '-' || op === '*' || op === '/' || op === '%') {
|
|
16
|
+
if (a == null || b == null) return null
|
|
17
|
+
const numA = Number(a)
|
|
18
|
+
const numB = Number(b)
|
|
19
|
+
if (op === '+') return numA + numB
|
|
20
|
+
if (op === '-') return numA - numB
|
|
21
|
+
if (op === '*') return numA * numB
|
|
22
|
+
if (op === '/') return numB === 0 ? null : numA / numB
|
|
23
|
+
if (op === '%') return numB === 0 ? null : numA % numB
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Comparison and logical operators
|
|
14
27
|
if (a == null || b == null) {
|
|
15
28
|
return false
|
|
16
29
|
}
|
|
@@ -33,6 +46,8 @@ export function applyBinaryOp(op, a, b) {
|
|
|
33
46
|
const regex = new RegExp(`^${regexPattern}$`, 'i')
|
|
34
47
|
return regex.test(str)
|
|
35
48
|
}
|
|
49
|
+
|
|
50
|
+
return null
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
/**
|
|
@@ -119,5 +134,22 @@ export function defaultDerivedAlias(expr) {
|
|
|
119
134
|
if (expr.type === 'function') {
|
|
120
135
|
return expr.name.toLowerCase() + '_' + expr.args.map(defaultDerivedAlias).join('_')
|
|
121
136
|
}
|
|
137
|
+
if (expr.type === 'interval') {
|
|
138
|
+
return `interval_${expr.value}_${expr.unit.toLowerCase()}`
|
|
139
|
+
}
|
|
122
140
|
return 'expr'
|
|
123
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {SqlPrimitive} value
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
export function stringify(value) {
|
|
148
|
+
if (value == null) return 'NULL'
|
|
149
|
+
return JSON.stringify(value, (_, val) => {
|
|
150
|
+
if (typeof val === 'bigint') {
|
|
151
|
+
return val <= Number.MAX_SAFE_INTEGER ? Number(val) : val.toString()
|
|
152
|
+
}
|
|
153
|
+
return val
|
|
154
|
+
})
|
|
155
|
+
}
|
package/src/parse/comparison.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { syntaxError } from '../errors.js'
|
|
1
2
|
import { isBinaryOp } from '../validation.js'
|
|
2
|
-
import {
|
|
3
|
+
import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
|
|
3
4
|
import { consume, current, expect, match, peekToken } from './state.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -11,7 +12,7 @@ import { consume, current, expect, match, peekToken } from './state.js'
|
|
|
11
12
|
* @returns {ExprNode}
|
|
12
13
|
*/
|
|
13
14
|
export function parseComparison(state) {
|
|
14
|
-
const left =
|
|
15
|
+
const left = parseAdditive(state)
|
|
15
16
|
const tok = current(state)
|
|
16
17
|
|
|
17
18
|
// IS [NOT] NULL
|
|
@@ -41,7 +42,7 @@ export function parseComparison(state) {
|
|
|
41
42
|
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
42
43
|
consume(state) // NOT
|
|
43
44
|
consume(state) // LIKE
|
|
44
|
-
const right =
|
|
45
|
+
const right = parseAdditive(state)
|
|
45
46
|
return {
|
|
46
47
|
type: 'unary',
|
|
47
48
|
op: 'NOT',
|
|
@@ -57,7 +58,7 @@ export function parseComparison(state) {
|
|
|
57
58
|
|
|
58
59
|
if (tok.type === 'keyword' && tok.value === 'LIKE') {
|
|
59
60
|
consume(state)
|
|
60
|
-
const right =
|
|
61
|
+
const right = parseAdditive(state)
|
|
61
62
|
return {
|
|
62
63
|
type: 'binary',
|
|
63
64
|
op: 'LIKE',
|
|
@@ -72,9 +73,9 @@ export function parseComparison(state) {
|
|
|
72
73
|
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
73
74
|
consume(state) // NOT
|
|
74
75
|
consume(state) // BETWEEN
|
|
75
|
-
const lower =
|
|
76
|
+
const lower = parseAdditive(state)
|
|
76
77
|
expect(state, 'keyword', 'AND')
|
|
77
|
-
const upper =
|
|
78
|
+
const upper = parseAdditive(state)
|
|
78
79
|
// NOT BETWEEN -> expr < lower OR expr > upper
|
|
79
80
|
return {
|
|
80
81
|
type: 'binary',
|
|
@@ -87,9 +88,9 @@ export function parseComparison(state) {
|
|
|
87
88
|
|
|
88
89
|
if (tok.type === 'keyword' && tok.value === 'BETWEEN') {
|
|
89
90
|
consume(state)
|
|
90
|
-
const lower =
|
|
91
|
+
const lower = parseAdditive(state)
|
|
91
92
|
expect(state, 'keyword', 'AND')
|
|
92
|
-
const upper =
|
|
93
|
+
const upper = parseAdditive(state)
|
|
93
94
|
// BETWEEN -> expr >= lower AND expr <= upper
|
|
94
95
|
return {
|
|
95
96
|
type: 'binary',
|
|
@@ -110,7 +111,7 @@ export function parseComparison(state) {
|
|
|
110
111
|
// parseSubquery expects to consume the opening paren itself
|
|
111
112
|
const parenTok = current(state)
|
|
112
113
|
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
113
|
-
throw
|
|
114
|
+
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, position: parenTok.position, after: 'IN' })
|
|
114
115
|
}
|
|
115
116
|
const peekTok = peekToken(state, 1)
|
|
116
117
|
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
@@ -155,7 +156,7 @@ export function parseComparison(state) {
|
|
|
155
156
|
// parseSubquery expects to consume the opening paren itself
|
|
156
157
|
const parenTok = current(state)
|
|
157
158
|
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
158
|
-
throw
|
|
159
|
+
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, position: parenTok.position, after: 'IN' })
|
|
159
160
|
}
|
|
160
161
|
const peekTok = peekToken(state, 1)
|
|
161
162
|
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
@@ -186,7 +187,7 @@ export function parseComparison(state) {
|
|
|
186
187
|
|
|
187
188
|
if (tok.type === 'operator' && isBinaryOp(tok.value)) {
|
|
188
189
|
consume(state)
|
|
189
|
-
const right =
|
|
190
|
+
const right = parseAdditive(state)
|
|
190
191
|
return {
|
|
191
192
|
type: 'binary',
|
|
192
193
|
op: tok.value,
|
package/src/parse/expression.js
CHANGED
|
@@ -1,12 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
invalidLiteralError,
|
|
3
|
+
missingClauseError,
|
|
4
|
+
syntaxError,
|
|
5
|
+
unknownFunctionError,
|
|
6
|
+
} from '../errors.js'
|
|
7
|
+
import { isAggregateFunc, isIntervalUnit, isStringFunc } from '../validation.js'
|
|
2
8
|
import { parseComparison } from './comparison.js'
|
|
3
9
|
import { parseSelectInternal } from './parse.js'
|
|
4
10
|
import { consume, current, expect, expectIdentifier, match, peekToken } from './state.js'
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
|
-
* @import { ExprNode, ParserState, SelectStatement, WhenClause } from '../types.js'
|
|
13
|
+
* @import { ExprNode, IntervalNode, ParserState, SelectStatement, WhenClause } from '../types.js'
|
|
8
14
|
*/
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @param {ParserState} state
|
|
18
|
+
* @returns {IntervalNode}
|
|
19
|
+
*/
|
|
20
|
+
function parseInterval(state) {
|
|
21
|
+
consume(state) // INTERVAL
|
|
22
|
+
|
|
23
|
+
// Handle optional negative sign
|
|
24
|
+
let sign = 1
|
|
25
|
+
const signTok = current(state)
|
|
26
|
+
if (signTok.type === 'operator' && signTok.value === '-') {
|
|
27
|
+
consume(state)
|
|
28
|
+
sign = -1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get value (number or quoted string)
|
|
32
|
+
const valueTok = current(state)
|
|
33
|
+
/** @type {number} */
|
|
34
|
+
let value
|
|
35
|
+
if (valueTok.type === 'number') {
|
|
36
|
+
consume(state)
|
|
37
|
+
value = sign * Number(valueTok.numericValue)
|
|
38
|
+
} else if (valueTok.type === 'string') {
|
|
39
|
+
consume(state)
|
|
40
|
+
const parsed = parseFloat(valueTok.value)
|
|
41
|
+
if (isNaN(parsed)) {
|
|
42
|
+
throw invalidLiteralError({ type: 'interval value', value: valueTok.value, position: valueTok.position })
|
|
43
|
+
}
|
|
44
|
+
value = sign * parsed
|
|
45
|
+
} else {
|
|
46
|
+
throw syntaxError({ expected: 'interval value (number)', received: `"${valueTok.value}"`, position: valueTok.position })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get unit keyword
|
|
50
|
+
const unitTok = current(state)
|
|
51
|
+
if (unitTok.type !== 'keyword' || !isIntervalUnit(unitTok.value)) {
|
|
52
|
+
throw invalidLiteralError({
|
|
53
|
+
type: 'interval unit',
|
|
54
|
+
value: unitTok.value,
|
|
55
|
+
position: unitTok.position,
|
|
56
|
+
validValues: 'DAY, MONTH, YEAR, HOUR, MINUTE, SECOND',
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
consume(state)
|
|
60
|
+
|
|
61
|
+
return { type: 'interval', value, unit: unitTok.value }
|
|
62
|
+
}
|
|
63
|
+
|
|
10
64
|
/**
|
|
11
65
|
* @param {ParserState} state
|
|
12
66
|
* @returns {ExprNode}
|
|
@@ -64,7 +118,7 @@ export function parsePrimary(state) {
|
|
|
64
118
|
|
|
65
119
|
// validate function names
|
|
66
120
|
if (!isStringFunc(funcName) && !isAggregateFunc(funcName)) {
|
|
67
|
-
throw
|
|
121
|
+
throw unknownFunctionError(funcName, tok.position)
|
|
68
122
|
}
|
|
69
123
|
|
|
70
124
|
consume(state) // function name
|
|
@@ -99,6 +153,17 @@ export function parsePrimary(state) {
|
|
|
99
153
|
}
|
|
100
154
|
}
|
|
101
155
|
|
|
156
|
+
// Niladic datetime functions (no parentheses required per ANSI SQL)
|
|
157
|
+
const niladicFuncs = ['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP']
|
|
158
|
+
if (niladicFuncs.includes(tok.value)) {
|
|
159
|
+
consume(state)
|
|
160
|
+
return {
|
|
161
|
+
type: 'function',
|
|
162
|
+
name: tok.value,
|
|
163
|
+
args: [],
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
102
167
|
consume(state)
|
|
103
168
|
let name = tok.value
|
|
104
169
|
|
|
@@ -175,7 +240,10 @@ export function parsePrimary(state) {
|
|
|
175
240
|
}
|
|
176
241
|
|
|
177
242
|
if (whenClauses.length === 0) {
|
|
178
|
-
throw
|
|
243
|
+
throw missingClauseError({
|
|
244
|
+
missing: 'at least one WHEN clause',
|
|
245
|
+
context: 'CASE expression',
|
|
246
|
+
})
|
|
179
247
|
}
|
|
180
248
|
|
|
181
249
|
// Parse optional ELSE clause
|
|
@@ -194,6 +262,9 @@ export function parsePrimary(state) {
|
|
|
194
262
|
elseResult,
|
|
195
263
|
}
|
|
196
264
|
}
|
|
265
|
+
if (tok.value === 'INTERVAL') {
|
|
266
|
+
return parseInterval(state)
|
|
267
|
+
}
|
|
197
268
|
}
|
|
198
269
|
|
|
199
270
|
if (tok.type === 'operator' && tok.value === '-') {
|
|
@@ -207,7 +278,7 @@ export function parsePrimary(state) {
|
|
|
207
278
|
}
|
|
208
279
|
|
|
209
280
|
const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
|
|
210
|
-
throw
|
|
281
|
+
throw syntaxError({ expected: 'expression', received: found, position: tok.position })
|
|
211
282
|
}
|
|
212
283
|
|
|
213
284
|
/**
|
|
@@ -272,6 +343,54 @@ function parseNot(state) {
|
|
|
272
343
|
return parseComparison(state)
|
|
273
344
|
}
|
|
274
345
|
|
|
346
|
+
/**
|
|
347
|
+
* @param {ParserState} state
|
|
348
|
+
* @returns {ExprNode}
|
|
349
|
+
*/
|
|
350
|
+
export function parseAdditive(state) {
|
|
351
|
+
let node = parseMultiplicative(state)
|
|
352
|
+
while (true) {
|
|
353
|
+
const tok = current(state)
|
|
354
|
+
if (tok.type === 'operator' && (tok.value === '+' || tok.value === '-')) {
|
|
355
|
+
consume(state)
|
|
356
|
+
const right = parseMultiplicative(state)
|
|
357
|
+
node = {
|
|
358
|
+
type: 'binary',
|
|
359
|
+
op: tok.value,
|
|
360
|
+
left: node,
|
|
361
|
+
right,
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
break
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return node
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {ParserState} state
|
|
372
|
+
* @returns {ExprNode}
|
|
373
|
+
*/
|
|
374
|
+
function parseMultiplicative(state) {
|
|
375
|
+
let node = parsePrimary(state)
|
|
376
|
+
while (true) {
|
|
377
|
+
const tok = current(state)
|
|
378
|
+
if (tok.type === 'operator' && (tok.value === '*' || tok.value === '/' || tok.value === '%')) {
|
|
379
|
+
consume(state)
|
|
380
|
+
const right = parsePrimary(state)
|
|
381
|
+
node = {
|
|
382
|
+
type: 'binary',
|
|
383
|
+
op: tok.value,
|
|
384
|
+
left: node,
|
|
385
|
+
right,
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return node
|
|
392
|
+
}
|
|
393
|
+
|
|
275
394
|
/**
|
|
276
395
|
* Creates an ExprCursor adapter for the ParserState.
|
|
277
396
|
*
|