squirreling 0.10.2 → 0.11.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 +5 -4
- package/package.json +5 -5
- package/src/ast.d.ts +32 -15
- package/src/backend/dataSource.js +4 -3
- package/src/execute/aggregates.js +160 -19
- package/src/execute/execute.js +129 -23
- package/src/execute/join.js +20 -21
- package/src/execute/utils.js +19 -7
- package/src/expression/alias.js +3 -2
- package/src/expression/evaluate.js +87 -61
- package/src/expression/math.js +2 -0
- package/src/expression/regexp.js +11 -9
- package/src/expression/strings.js +11 -9
- package/src/index.d.ts +10 -5
- package/src/index.js +1 -1
- package/src/parse/expression.js +187 -351
- package/src/parse/functions.js +63 -51
- package/src/parse/joins.js +24 -38
- package/src/parse/parse.js +244 -200
- package/src/parse/primary.js +281 -0
- package/src/parse/state.js +11 -25
- package/src/parse/tokenize.js +77 -196
- package/src/plan/columns.js +115 -17
- package/src/plan/plan.js +121 -44
- package/src/plan/types.d.ts +11 -1
- package/src/spatial/bbox.js +3 -3
- package/src/spatial/geometry.d.ts +1 -1
- package/src/spatial/index.d.ts +6 -0
- package/src/spatial/index.js +3 -0
- package/src/spatial/spatial.js +19 -53
- package/src/types.d.ts +17 -5
- package/src/validation/executionErrors.js +20 -12
- package/src/validation/functions.js +28 -53
- package/src/validation/keywords.js +35 -0
- package/src/validation/parseErrors.js +101 -82
- package/src/validation/planErrors.js +41 -33
- package/src/parse/comparison.js +0 -233
- package/src/validation/expressionErrors.js +0 -57
package/src/parse/expression.js
CHANGED
|
@@ -1,391 +1,242 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '../validation/parseErrors.js'
|
|
7
|
-
import { RESERVED_KEYWORDS, isCastType, isExtractField, isIntervalUnit, isKnownFunction } from '../validation/functions.js'
|
|
8
|
-
import { parseComparison } from './comparison.js'
|
|
9
|
-
import { parseFunctionCall } from './functions.js'
|
|
10
|
-
import { parseSelectInternal } from './parse.js'
|
|
11
|
-
import { consume, current, expect, expectIdentifier, match, peekToken } from './state.js'
|
|
1
|
+
import { isBinaryOp } from '../validation/functions.js'
|
|
2
|
+
import { SyntaxError } from '../validation/parseErrors.js'
|
|
3
|
+
import { parsePrimary } from './primary.js'
|
|
4
|
+
import { parseStatement } from './parse.js'
|
|
5
|
+
import { consume, current, expect, match } from './state.js'
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
|
-
* @import { ExprNode,
|
|
8
|
+
* @import { ExprNode, ParserState } from '../types.js'
|
|
15
9
|
*/
|
|
16
10
|
|
|
11
|
+
// Precedence (lowest to highest):
|
|
12
|
+
// OR, AND, NOT, Comparison, Additive, Multiplicative, Primary
|
|
13
|
+
|
|
17
14
|
/**
|
|
18
15
|
* @param {ParserState} state
|
|
19
16
|
* @returns {ExprNode}
|
|
20
17
|
*/
|
|
21
18
|
export function parseExpression(state) {
|
|
22
|
-
|
|
19
|
+
let left = parseAnd(state)
|
|
20
|
+
while (match(state, 'keyword', 'OR')) {
|
|
21
|
+
const right = parseAnd(state)
|
|
22
|
+
left = {
|
|
23
|
+
type: 'binary',
|
|
24
|
+
op: 'OR',
|
|
25
|
+
left,
|
|
26
|
+
right,
|
|
27
|
+
positionStart: left.positionStart,
|
|
28
|
+
positionEnd: right.positionEnd,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return left
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
35
|
* @param {ParserState} state
|
|
27
36
|
* @returns {ExprNode}
|
|
28
37
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
type: 'subquery',
|
|
41
|
-
subquery,
|
|
42
|
-
positionStart,
|
|
43
|
-
positionEnd: state.lastPos,
|
|
44
|
-
}
|
|
38
|
+
function parseAnd(state) {
|
|
39
|
+
let left = parseNot(state)
|
|
40
|
+
while (match(state, 'keyword', 'AND')) {
|
|
41
|
+
const right = parseNot(state)
|
|
42
|
+
left = {
|
|
43
|
+
type: 'binary',
|
|
44
|
+
op: 'AND',
|
|
45
|
+
left,
|
|
46
|
+
right,
|
|
47
|
+
positionStart: left.positionStart,
|
|
48
|
+
positionEnd: right.positionEnd,
|
|
45
49
|
}
|
|
46
|
-
// Regular grouped expression
|
|
47
|
-
consume(state)
|
|
48
|
-
const expr = parseExpression(state)
|
|
49
|
-
expect(state, 'paren', ')')
|
|
50
|
-
return expr
|
|
51
50
|
}
|
|
51
|
+
return left
|
|
52
|
+
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw syntaxError({
|
|
66
|
-
...typeTok,
|
|
67
|
-
expected: 'cast type (STRING, INT, BIGINT, FLOAT, BOOL)',
|
|
68
|
-
received: `"${typeTok.value}"`,
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
expect(state, 'paren', ')')
|
|
72
|
-
return {
|
|
73
|
-
type: 'cast',
|
|
74
|
-
expr,
|
|
75
|
-
toType,
|
|
76
|
-
positionStart,
|
|
77
|
-
positionEnd: state.lastPos,
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// EXTRACT(field FROM expr)
|
|
82
|
-
if (tok.value === 'EXTRACT' && next.type === 'paren' && next.value === '(') {
|
|
83
|
-
consume(state) // EXTRACT
|
|
84
|
-
consume(state) // '('
|
|
85
|
-
const fieldTok = current(state)
|
|
86
|
-
if (!isExtractField(fieldTok.value)) {
|
|
87
|
-
throw syntaxError({
|
|
88
|
-
...fieldTok,
|
|
89
|
-
expected: 'extract field (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, EPOCH)',
|
|
90
|
-
received: `"${fieldTok.value}"`,
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
consume(state) // field
|
|
94
|
-
expect(state, 'keyword', 'FROM')
|
|
95
|
-
const expr = parseExpression(state)
|
|
54
|
+
/**
|
|
55
|
+
* @param {ParserState} state
|
|
56
|
+
* @returns {ExprNode}
|
|
57
|
+
*/
|
|
58
|
+
function parseNot(state) {
|
|
59
|
+
const tok = current(state)
|
|
60
|
+
if (match(state, 'keyword', 'NOT')) {
|
|
61
|
+
const { positionStart } = tok
|
|
62
|
+
// Check for NOT EXISTS
|
|
63
|
+
if (match(state, 'keyword', 'EXISTS')) {
|
|
64
|
+
expect(state, 'paren', '(')
|
|
65
|
+
const subquery = parseStatement(state)
|
|
96
66
|
expect(state, 'paren', ')')
|
|
97
67
|
return {
|
|
98
|
-
type: '
|
|
99
|
-
|
|
100
|
-
args: [
|
|
101
|
-
{ type: 'literal', value: fieldTok.value, positionStart: fieldTok.positionStart, positionEnd: fieldTok.positionEnd },
|
|
102
|
-
expr,
|
|
103
|
-
],
|
|
104
|
-
positionStart,
|
|
105
|
-
positionEnd: state.lastPos,
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// function call
|
|
110
|
-
if (next.type === 'paren' && next.value === '(') {
|
|
111
|
-
const funcName = tok.value
|
|
112
|
-
|
|
113
|
-
// Validate function existence early for better error messages
|
|
114
|
-
if (!isKnownFunction(funcName.toUpperCase(), state.functions)) {
|
|
115
|
-
throw unknownFunctionError({
|
|
116
|
-
funcName,
|
|
117
|
-
positionStart,
|
|
118
|
-
positionEnd: tok.positionEnd,
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
consume(state) // function name
|
|
123
|
-
return parseFunctionCall(state, funcName, positionStart)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Niladic datetime functions (no parentheses required per ANSI SQL)
|
|
127
|
-
const niladicFuncs = ['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP']
|
|
128
|
-
if (niladicFuncs.includes(tok.value)) {
|
|
129
|
-
consume(state)
|
|
130
|
-
return {
|
|
131
|
-
type: 'function',
|
|
132
|
-
funcName: tok.value,
|
|
133
|
-
args: [],
|
|
68
|
+
type: 'not exists',
|
|
69
|
+
subquery,
|
|
134
70
|
positionStart,
|
|
135
71
|
positionEnd: state.lastPos,
|
|
136
72
|
}
|
|
137
73
|
}
|
|
138
|
-
|
|
139
|
-
consume(state)
|
|
140
|
-
let name = tok.value
|
|
141
|
-
|
|
142
|
-
// table.column
|
|
143
|
-
if (current(state).type === 'dot') {
|
|
144
|
-
consume(state)
|
|
145
|
-
const columnTok = expectIdentifier(state)
|
|
146
|
-
name = name + '.' + columnTok.value
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
type: 'identifier',
|
|
151
|
-
name,
|
|
152
|
-
positionStart,
|
|
153
|
-
positionEnd: state.lastPos,
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (tok.type === 'number') {
|
|
158
|
-
consume(state)
|
|
74
|
+
const argument = parseNot(state)
|
|
159
75
|
return {
|
|
160
|
-
type: '
|
|
161
|
-
|
|
76
|
+
type: 'unary',
|
|
77
|
+
op: 'NOT',
|
|
78
|
+
argument,
|
|
162
79
|
positionStart,
|
|
163
|
-
positionEnd:
|
|
80
|
+
positionEnd: argument.positionEnd,
|
|
164
81
|
}
|
|
165
82
|
}
|
|
83
|
+
return parseComparison(state)
|
|
84
|
+
}
|
|
166
85
|
|
|
167
|
-
|
|
168
|
-
|
|
86
|
+
/**
|
|
87
|
+
* @param {ParserState} state
|
|
88
|
+
* @returns {ExprNode}
|
|
89
|
+
*/
|
|
90
|
+
function parseComparison(state) {
|
|
91
|
+
const left = parseAdditive(state)
|
|
92
|
+
const { positionStart } = left
|
|
93
|
+
|
|
94
|
+
// IS [NOT] NULL
|
|
95
|
+
if (match(state, 'keyword', 'IS')) {
|
|
96
|
+
const op = match(state, 'keyword', 'NOT') ? 'IS NOT NULL' : 'IS NULL'
|
|
97
|
+
expect(state, 'keyword', 'NULL')
|
|
169
98
|
return {
|
|
170
|
-
type: '
|
|
171
|
-
|
|
99
|
+
type: 'unary',
|
|
100
|
+
op,
|
|
101
|
+
argument: left,
|
|
172
102
|
positionStart,
|
|
173
103
|
positionEnd: state.lastPos,
|
|
174
104
|
}
|
|
175
105
|
}
|
|
176
106
|
|
|
177
|
-
//
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
return parseFunctionCall(state, tok.value, positionStart)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (tok.value === 'TRUE') {
|
|
186
|
-
consume(state)
|
|
187
|
-
return { type: 'literal', value: true, positionStart, positionEnd: state.lastPos }
|
|
188
|
-
}
|
|
189
|
-
if (tok.value === 'FALSE') {
|
|
190
|
-
consume(state)
|
|
191
|
-
return { type: 'literal', value: false, positionStart, positionEnd: state.lastPos }
|
|
192
|
-
}
|
|
193
|
-
if (tok.value === 'NULL') {
|
|
194
|
-
consume(state)
|
|
195
|
-
return { type: 'literal', value: null, positionStart, positionEnd: state.lastPos }
|
|
196
|
-
}
|
|
197
|
-
if (tok.value === 'EXISTS') {
|
|
198
|
-
consume(state) // EXISTS
|
|
199
|
-
const subquery = parseSubquery(state)
|
|
107
|
+
// Binary operators
|
|
108
|
+
if (match(state, 'keyword', 'NOT')) {
|
|
109
|
+
// NOT LIKE
|
|
110
|
+
if (match(state, 'keyword', 'LIKE')) {
|
|
111
|
+
const right = parseAdditive(state)
|
|
200
112
|
return {
|
|
201
|
-
type: '
|
|
202
|
-
|
|
113
|
+
type: 'unary',
|
|
114
|
+
op: 'NOT',
|
|
115
|
+
argument: {
|
|
116
|
+
type: 'binary',
|
|
117
|
+
op: 'LIKE',
|
|
118
|
+
left,
|
|
119
|
+
right,
|
|
120
|
+
positionStart,
|
|
121
|
+
positionEnd: right.positionEnd,
|
|
122
|
+
},
|
|
203
123
|
positionStart,
|
|
204
|
-
positionEnd:
|
|
124
|
+
positionEnd: right.positionEnd,
|
|
205
125
|
}
|
|
206
126
|
}
|
|
207
|
-
if (tok.value === 'CASE') {
|
|
208
|
-
consume(state) // CASE
|
|
209
|
-
|
|
210
|
-
// Check if it's simple CASE (CASE expr WHEN ...) or searched CASE (CASE WHEN ...)
|
|
211
|
-
/** @type {ExprNode | undefined} */
|
|
212
|
-
let caseExpr
|
|
213
|
-
const nextTok = current(state)
|
|
214
|
-
if (nextTok.type !== 'keyword' || nextTok.value !== 'WHEN') {
|
|
215
|
-
// Simple CASE: parse the case expression
|
|
216
|
-
caseExpr = parseExpression(state)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Parse WHEN clauses
|
|
220
|
-
/** @type {WhenClause[]} */
|
|
221
|
-
const whenClauses = []
|
|
222
|
-
while (match(state, 'keyword', 'WHEN')) {
|
|
223
|
-
const condition = parseExpression(state)
|
|
224
|
-
expect(state, 'keyword', 'THEN')
|
|
225
|
-
const result = parseExpression(state)
|
|
226
|
-
whenClauses.push({
|
|
227
|
-
condition,
|
|
228
|
-
result,
|
|
229
|
-
positionStart: condition.positionStart,
|
|
230
|
-
positionEnd: result.positionEnd,
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (whenClauses.length === 0) {
|
|
235
|
-
throw missingClauseError({
|
|
236
|
-
missing: 'at least one WHEN clause',
|
|
237
|
-
context: 'CASE expression',
|
|
238
|
-
positionStart,
|
|
239
|
-
positionEnd: state.lastPos,
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Parse optional ELSE clause
|
|
244
|
-
/** @type {ExprNode | undefined} */
|
|
245
|
-
let elseResult
|
|
246
|
-
if (match(state, 'keyword', 'ELSE')) {
|
|
247
|
-
elseResult = parseExpression(state)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
expect(state, 'keyword', 'END')
|
|
251
127
|
|
|
128
|
+
// NOT BETWEEN - convert to range comparison
|
|
129
|
+
if (match(state, 'keyword', 'BETWEEN')) {
|
|
130
|
+
const lower = parseAdditive(state)
|
|
131
|
+
expect(state, 'keyword', 'AND')
|
|
132
|
+
const upper = parseAdditive(state)
|
|
133
|
+
// NOT BETWEEN -> expr < lower OR expr > upper
|
|
252
134
|
return {
|
|
253
|
-
type: '
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
135
|
+
type: 'binary',
|
|
136
|
+
op: 'OR',
|
|
137
|
+
left: { type: 'binary', op: '<', left, right: lower, positionStart, positionEnd: lower.positionEnd },
|
|
138
|
+
right: { type: 'binary', op: '>', left, right: upper, positionStart, positionEnd: upper.positionEnd },
|
|
257
139
|
positionStart,
|
|
258
|
-
positionEnd:
|
|
140
|
+
positionEnd: upper.positionEnd,
|
|
259
141
|
}
|
|
260
142
|
}
|
|
261
|
-
if (tok.value === 'INTERVAL') {
|
|
262
|
-
return parseInterval(state)
|
|
263
|
-
}
|
|
264
143
|
|
|
265
|
-
//
|
|
266
|
-
if (
|
|
267
|
-
|
|
144
|
+
// NOT IN
|
|
145
|
+
if (match(state, 'keyword', 'IN')) {
|
|
146
|
+
const node = parseIn(state, left)
|
|
268
147
|
return {
|
|
269
|
-
type: '
|
|
270
|
-
|
|
148
|
+
type: 'unary',
|
|
149
|
+
op: 'NOT',
|
|
150
|
+
argument: node,
|
|
271
151
|
positionStart,
|
|
272
|
-
positionEnd:
|
|
152
|
+
positionEnd: node.positionEnd,
|
|
273
153
|
}
|
|
274
154
|
}
|
|
275
|
-
}
|
|
276
155
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
argument,
|
|
284
|
-
positionStart,
|
|
285
|
-
positionEnd: argument.positionEnd,
|
|
286
|
-
}
|
|
156
|
+
const found = current(state)
|
|
157
|
+
throw new SyntaxError({
|
|
158
|
+
expected: 'LIKE, BETWEEN, or IN',
|
|
159
|
+
after: 'NOT',
|
|
160
|
+
...found,
|
|
161
|
+
})
|
|
287
162
|
}
|
|
288
163
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* @param {ParserState} state
|
|
295
|
-
* @returns {ExprNode}
|
|
296
|
-
*/
|
|
297
|
-
function parseOr(state) {
|
|
298
|
-
let node = parseAnd(state)
|
|
299
|
-
while (match(state, 'keyword', 'OR')) {
|
|
300
|
-
const right = parseAnd(state)
|
|
301
|
-
node = {
|
|
164
|
+
// LIKE
|
|
165
|
+
if (match(state, 'keyword', 'LIKE')) {
|
|
166
|
+
const right = parseAdditive(state)
|
|
167
|
+
return {
|
|
302
168
|
type: 'binary',
|
|
303
|
-
op: '
|
|
304
|
-
left
|
|
169
|
+
op: 'LIKE',
|
|
170
|
+
left,
|
|
305
171
|
right,
|
|
306
|
-
positionStart
|
|
172
|
+
positionStart,
|
|
307
173
|
positionEnd: right.positionEnd,
|
|
308
174
|
}
|
|
309
175
|
}
|
|
310
|
-
return node
|
|
311
|
-
}
|
|
312
176
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const right = parseNot(state)
|
|
321
|
-
node = {
|
|
177
|
+
// BETWEEN - convert to range comparison
|
|
178
|
+
if (match(state, 'keyword', 'BETWEEN')) {
|
|
179
|
+
const lower = parseAdditive(state)
|
|
180
|
+
expect(state, 'keyword', 'AND')
|
|
181
|
+
const upper = parseAdditive(state)
|
|
182
|
+
// BETWEEN -> expr >= lower AND expr <= upper
|
|
183
|
+
return {
|
|
322
184
|
type: 'binary',
|
|
323
185
|
op: 'AND',
|
|
324
|
-
left:
|
|
325
|
-
right,
|
|
326
|
-
positionStart
|
|
327
|
-
positionEnd:
|
|
186
|
+
left: { type: 'binary', op: '>=', left, right: lower, positionStart, positionEnd: lower.positionEnd },
|
|
187
|
+
right: { type: 'binary', op: '<=', left, right: upper, positionStart, positionEnd: upper.positionEnd },
|
|
188
|
+
positionStart,
|
|
189
|
+
positionEnd: upper.positionEnd,
|
|
328
190
|
}
|
|
329
191
|
}
|
|
330
|
-
return node
|
|
331
|
-
}
|
|
332
192
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const nextTok = current(state)
|
|
343
|
-
if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
|
|
344
|
-
consume(state) // EXISTS
|
|
345
|
-
const subquery = parseSubquery(state)
|
|
346
|
-
return {
|
|
347
|
-
type: 'not exists',
|
|
348
|
-
subquery,
|
|
349
|
-
positionStart,
|
|
350
|
-
positionEnd: state.lastPos,
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const argument = parseNot(state)
|
|
193
|
+
// IN
|
|
194
|
+
if (match(state, 'keyword', 'IN')) {
|
|
195
|
+
return parseIn(state, left)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const opTok = current(state)
|
|
199
|
+
if (opTok.type === 'operator' && isBinaryOp(opTok.value)) {
|
|
200
|
+
consume(state)
|
|
201
|
+
const right = parseAdditive(state)
|
|
354
202
|
return {
|
|
355
|
-
type: '
|
|
356
|
-
op:
|
|
357
|
-
|
|
203
|
+
type: 'binary',
|
|
204
|
+
op: opTok.value,
|
|
205
|
+
left,
|
|
206
|
+
right,
|
|
358
207
|
positionStart,
|
|
359
|
-
positionEnd:
|
|
208
|
+
positionEnd: right.positionEnd,
|
|
360
209
|
}
|
|
361
210
|
}
|
|
362
|
-
|
|
211
|
+
|
|
212
|
+
return left
|
|
363
213
|
}
|
|
364
214
|
|
|
365
215
|
/**
|
|
366
216
|
* @param {ParserState} state
|
|
367
217
|
* @returns {ExprNode}
|
|
368
218
|
*/
|
|
369
|
-
|
|
370
|
-
let
|
|
219
|
+
function parseAdditive(state) {
|
|
220
|
+
let left = parseMultiplicative(state)
|
|
371
221
|
while (true) {
|
|
372
222
|
const tok = current(state)
|
|
373
223
|
if (tok.type === 'operator' && (tok.value === '+' || tok.value === '-')) {
|
|
374
224
|
consume(state)
|
|
375
225
|
const right = parseMultiplicative(state)
|
|
376
|
-
|
|
226
|
+
// Recursive left-associative binary operator
|
|
227
|
+
left = {
|
|
377
228
|
type: 'binary',
|
|
378
229
|
op: tok.value,
|
|
379
|
-
left
|
|
230
|
+
left,
|
|
380
231
|
right,
|
|
381
|
-
positionStart:
|
|
232
|
+
positionStart: left.positionStart,
|
|
382
233
|
positionEnd: right.positionEnd,
|
|
383
234
|
}
|
|
384
235
|
} else {
|
|
385
236
|
break
|
|
386
237
|
}
|
|
387
238
|
}
|
|
388
|
-
return
|
|
239
|
+
return left
|
|
389
240
|
}
|
|
390
241
|
|
|
391
242
|
/**
|
|
@@ -393,78 +244,63 @@ export function parseAdditive(state) {
|
|
|
393
244
|
* @returns {ExprNode}
|
|
394
245
|
*/
|
|
395
246
|
function parseMultiplicative(state) {
|
|
396
|
-
let
|
|
247
|
+
let left = parsePrimary(state)
|
|
397
248
|
while (true) {
|
|
398
249
|
const tok = current(state)
|
|
399
250
|
if (tok.type === 'operator' && (tok.value === '*' || tok.value === '/' || tok.value === '%')) {
|
|
400
251
|
consume(state)
|
|
401
252
|
const right = parsePrimary(state)
|
|
402
|
-
|
|
253
|
+
// Recursively build left-associative tree for multiplicative operators
|
|
254
|
+
left = {
|
|
403
255
|
type: 'binary',
|
|
404
256
|
op: tok.value,
|
|
405
|
-
left
|
|
257
|
+
left,
|
|
406
258
|
right,
|
|
407
|
-
positionStart:
|
|
259
|
+
positionStart: left.positionStart,
|
|
408
260
|
positionEnd: right.positionEnd,
|
|
409
261
|
}
|
|
410
262
|
} else {
|
|
411
263
|
break
|
|
412
264
|
}
|
|
413
265
|
}
|
|
414
|
-
return
|
|
266
|
+
return left
|
|
415
267
|
}
|
|
416
268
|
|
|
417
269
|
/**
|
|
418
|
-
*
|
|
270
|
+
* Parses an IN expression (subquery or value list).
|
|
419
271
|
*
|
|
420
272
|
* @param {ParserState} state
|
|
421
|
-
* @
|
|
273
|
+
* @param {ExprNode} left
|
|
274
|
+
* @returns {ExprNode}
|
|
422
275
|
*/
|
|
423
|
-
|
|
276
|
+
function parseIn(state, left) {
|
|
424
277
|
expect(state, 'paren', '(')
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
consume(state) // INTERVAL
|
|
437
|
-
|
|
438
|
-
// Get value (number or quoted string)
|
|
439
|
-
const valueTok = current(state)
|
|
440
|
-
/** @type {number} */
|
|
441
|
-
let value
|
|
442
|
-
if (valueTok.type === 'number') {
|
|
443
|
-
consume(state)
|
|
444
|
-
value = Number(valueTok.numericValue)
|
|
445
|
-
} else if (valueTok.type === 'string') {
|
|
446
|
-
consume(state)
|
|
447
|
-
const parsed = parseFloat(valueTok.value)
|
|
448
|
-
if (isNaN(parsed)) {
|
|
449
|
-
throw invalidLiteralError({ type: 'interval value', value: valueTok.value, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
278
|
+
// Subquery
|
|
279
|
+
const next = current(state)
|
|
280
|
+
if (next.type === 'keyword' && next.value === 'SELECT') {
|
|
281
|
+
const subquery = parseStatement(state)
|
|
282
|
+
expect(state, 'paren', ')')
|
|
283
|
+
return {
|
|
284
|
+
type: 'in',
|
|
285
|
+
expr: left,
|
|
286
|
+
subquery,
|
|
287
|
+
positionStart: left.positionStart,
|
|
288
|
+
positionEnd: state.lastPos,
|
|
450
289
|
}
|
|
451
|
-
value = parsed
|
|
452
|
-
} else {
|
|
453
|
-
throw syntaxError({ expected: 'interval value (number)', received: `"${valueTok.value}"`, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
454
290
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
291
|
+
// Value list
|
|
292
|
+
/** @type {ExprNode[]} */
|
|
293
|
+
const values = []
|
|
294
|
+
while (true) {
|
|
295
|
+
values.push(parseExpression(state))
|
|
296
|
+
if (!match(state, 'comma')) break
|
|
297
|
+
}
|
|
298
|
+
expect(state, 'paren', ')')
|
|
299
|
+
return {
|
|
300
|
+
type: 'in valuelist',
|
|
301
|
+
expr: left,
|
|
302
|
+
values,
|
|
303
|
+
positionStart: left.positionStart,
|
|
304
|
+
positionEnd: state.lastPos,
|
|
466
305
|
}
|
|
467
|
-
consume(state)
|
|
468
|
-
|
|
469
|
-
return { type: 'interval', value, unit: unitTok.value, positionStart, positionEnd: state.lastPos }
|
|
470
306
|
}
|