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.
@@ -1,391 +1,242 @@
1
- import {
2
- invalidLiteralError,
3
- missingClauseError,
4
- syntaxError,
5
- unknownFunctionError,
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, IntervalNode, ParserState, SelectStatement, WhenClause } from '../types.js'
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
- return parseOr(state)
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
- export function parsePrimary(state) {
30
- const tok = current(state)
31
- const { positionStart } = tok
32
-
33
- if (tok.type === 'paren' && tok.value === '(') {
34
- // Peek ahead to see if this is a scalar subquery
35
- const nextTok = peekToken(state, 1)
36
- if (nextTok.type === 'keyword' && nextTok.value === 'SELECT') {
37
- // It's a scalar subquery
38
- const subquery = parseSubquery(state)
39
- return {
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
- if (tok.type === 'identifier') {
54
- const next = peekToken(state, 1)
55
-
56
- // CAST expression
57
- if (tok.value === 'CAST' && next.type === 'paren' && next.value === '(') {
58
- consume(state) // CAST
59
- consume(state) // '('
60
- const expr = parseExpression(state)
61
- expect(state, 'keyword', 'AS')
62
- const typeTok = expectIdentifier(state)
63
- const toType = typeTok.value.toUpperCase()
64
- if (!isCastType(toType)) {
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: 'function',
99
- funcName: 'EXTRACT',
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: 'literal',
161
- value: tok.numericValue ?? null,
76
+ type: 'unary',
77
+ op: 'NOT',
78
+ argument,
162
79
  positionStart,
163
- positionEnd: state.lastPos,
80
+ positionEnd: argument.positionEnd,
164
81
  }
165
82
  }
83
+ return parseComparison(state)
84
+ }
166
85
 
167
- if (tok.type === 'string') {
168
- consume(state)
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: 'literal',
171
- value: tok.value,
99
+ type: 'unary',
100
+ op,
101
+ argument: left,
172
102
  positionStart,
173
103
  positionEnd: state.lastPos,
174
104
  }
175
105
  }
176
106
 
177
- // Keywords that can be used as function names (e.g., LEFT, RIGHT)
178
- if (tok.type === 'keyword') {
179
- const next = peekToken(state, 1)
180
- if (next.type === 'paren' && next.value === '(' && isKnownFunction(tok.value, state.functions)) {
181
- consume(state) // function name
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: 'exists',
202
- subquery,
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: state.lastPos,
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: 'case',
254
- caseExpr,
255
- whenClauses,
256
- elseResult,
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: state.lastPos,
140
+ positionEnd: upper.positionEnd,
259
141
  }
260
142
  }
261
- if (tok.value === 'INTERVAL') {
262
- return parseInterval(state)
263
- }
264
143
 
265
- // Non-reserved keywords can be used as identifiers (e.g. column aliases)
266
- if (!RESERVED_KEYWORDS.has(tok.value)) {
267
- consume(state)
144
+ // NOT IN
145
+ if (match(state, 'keyword', 'IN')) {
146
+ const node = parseIn(state, left)
268
147
  return {
269
- type: 'identifier',
270
- name: tok.originalValue ?? tok.value,
148
+ type: 'unary',
149
+ op: 'NOT',
150
+ argument: node,
271
151
  positionStart,
272
- positionEnd: state.lastPos,
152
+ positionEnd: node.positionEnd,
273
153
  }
274
154
  }
275
- }
276
155
 
277
- if (tok.type === 'operator' && tok.value === '-') {
278
- consume(state)
279
- const argument = parsePrimary(state)
280
- return {
281
- type: 'unary',
282
- op: '-',
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
- const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
290
- throw syntaxError({ expected: 'expression', received: found, positionStart: tok.positionStart, positionEnd: tok.positionEnd })
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: 'OR',
304
- left: node,
169
+ op: 'LIKE',
170
+ left,
305
171
  right,
306
- positionStart: node.positionStart,
172
+ positionStart,
307
173
  positionEnd: right.positionEnd,
308
174
  }
309
175
  }
310
- return node
311
- }
312
176
 
313
- /**
314
- * @param {ParserState} state
315
- * @returns {ExprNode}
316
- */
317
- function parseAnd(state) {
318
- let node = parseNot(state)
319
- while (match(state, 'keyword', 'AND')) {
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: node,
325
- right,
326
- positionStart: node.positionStart,
327
- positionEnd: right.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
- * @param {ParserState} state
335
- * @returns {ExprNode}
336
- */
337
- function parseNot(state) {
338
- const tok = current(state)
339
- if (match(state, 'keyword', 'NOT')) {
340
- const { positionStart } = tok
341
- // Check for NOT EXISTS
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: 'unary',
356
- op: 'NOT',
357
- argument,
203
+ type: 'binary',
204
+ op: opTok.value,
205
+ left,
206
+ right,
358
207
  positionStart,
359
- positionEnd: argument.positionEnd,
208
+ positionEnd: right.positionEnd,
360
209
  }
361
210
  }
362
- return parseComparison(state)
211
+
212
+ return left
363
213
  }
364
214
 
365
215
  /**
366
216
  * @param {ParserState} state
367
217
  * @returns {ExprNode}
368
218
  */
369
- export function parseAdditive(state) {
370
- let node = parseMultiplicative(state)
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
- node = {
226
+ // Recursive left-associative binary operator
227
+ left = {
377
228
  type: 'binary',
378
229
  op: tok.value,
379
- left: node,
230
+ left,
380
231
  right,
381
- positionStart: node.positionStart,
232
+ positionStart: left.positionStart,
382
233
  positionEnd: right.positionEnd,
383
234
  }
384
235
  } else {
385
236
  break
386
237
  }
387
238
  }
388
- return node
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 node = parsePrimary(state)
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
- node = {
253
+ // Recursively build left-associative tree for multiplicative operators
254
+ left = {
403
255
  type: 'binary',
404
256
  op: tok.value,
405
- left: node,
257
+ left,
406
258
  right,
407
- positionStart: node.positionStart,
259
+ positionStart: left.positionStart,
408
260
  positionEnd: right.positionEnd,
409
261
  }
410
262
  } else {
411
263
  break
412
264
  }
413
265
  }
414
- return node
266
+ return left
415
267
  }
416
268
 
417
269
  /**
418
- * Creates an ExprCursor adapter for the ParserState.
270
+ * Parses an IN expression (subquery or value list).
419
271
  *
420
272
  * @param {ParserState} state
421
- * @returns {SelectStatement}
273
+ * @param {ExprNode} left
274
+ * @returns {ExprNode}
422
275
  */
423
- export function parseSubquery(state) {
276
+ function parseIn(state, left) {
424
277
  expect(state, 'paren', '(')
425
- const query = parseSelectInternal(state)
426
- expect(state, 'paren', ')')
427
- return query
428
- }
429
-
430
- /**
431
- * @param {ParserState} state
432
- * @returns {IntervalNode}
433
- */
434
- function parseInterval(state) {
435
- const { positionStart } = current(state)
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
- // Get unit keyword
457
- const unitTok = current(state)
458
- if (unitTok.type !== 'keyword' || !isIntervalUnit(unitTok.value)) {
459
- throw invalidLiteralError({
460
- type: 'interval unit',
461
- value: unitTok.value,
462
- positionStart: unitTok.positionStart,
463
- positionEnd: unitTok.positionEnd,
464
- validValues: 'DAY, MONTH, YEAR, HOUR, MINUTE, SECOND',
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
  }