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.
@@ -0,0 +1,281 @@
1
+ import { isCastType, isExtractField, isIntervalUnit, isKnownFunction, niladicFuncs } from '../validation/functions.js'
2
+ import { InvalidLiteralError, ParseError, SyntaxError, UnknownFunctionError } from '../validation/parseErrors.js'
3
+ import { RESERVED_KEYWORDS } from '../validation/keywords.js'
4
+ import { parseExpression } from './expression.js'
5
+ import { parseFunctionCall } from './functions.js'
6
+ import { parseStatement } from './parse.js'
7
+ import { consume, current, expect, match, peekToken } from './state.js'
8
+
9
+ /**
10
+ * @import { ExprNode, IntervalNode, ParserState, WhenClause } from '../types.js'
11
+ */
12
+
13
+ /**
14
+ * @param {ParserState} state
15
+ * @returns {ExprNode}
16
+ */
17
+ export function parsePrimary(state) {
18
+ const tok = current(state)
19
+ const { positionStart } = tok
20
+
21
+ if (match(state, 'paren', '(')) {
22
+ // Peek ahead to see if this is a scalar subquery
23
+ const next = current(state)
24
+ if (next.type === 'keyword' && next.value === 'SELECT') {
25
+ // It's a scalar subquery
26
+ const subquery = parseStatement(state)
27
+ expect(state, 'paren', ')')
28
+ return {
29
+ type: 'subquery',
30
+ subquery,
31
+ positionStart,
32
+ positionEnd: state.lastPos,
33
+ }
34
+ }
35
+ // Regular grouped expression
36
+ const expr = parseExpression(state)
37
+ expect(state, 'paren', ')')
38
+ return expr
39
+ }
40
+
41
+ if (tok.type === 'identifier') {
42
+ const next = peekToken(state, 1)
43
+ const funcNameUpper = tok.value.toUpperCase()
44
+
45
+ // CAST(expr AS type)
46
+ if (funcNameUpper === 'CAST' && next.type === 'paren' && next.value === '(') {
47
+ consume(state) // CAST
48
+ consume(state) // '('
49
+ const expr = parseExpression(state)
50
+ expect(state, 'keyword', 'AS')
51
+ const typeTok = expect(state, 'identifier')
52
+ const toType = typeTok.value.toUpperCase()
53
+ if (!isCastType(toType)) {
54
+ throw new SyntaxError({
55
+ expected: 'cast type (STRING, INT, BIGINT, FLOAT, BOOL)',
56
+ after: 'AS',
57
+ ...typeTok,
58
+ })
59
+ }
60
+ expect(state, 'paren', ')')
61
+ return {
62
+ type: 'cast',
63
+ expr,
64
+ toType,
65
+ positionStart,
66
+ positionEnd: state.lastPos,
67
+ }
68
+ }
69
+
70
+ // EXTRACT(field FROM expr)
71
+ if (funcNameUpper === 'EXTRACT' && next.type === 'paren' && next.value === '(') {
72
+ consume(state) // EXTRACT
73
+ consume(state) // '('
74
+ const fieldTok = consume(state)
75
+ const fieldUpper = fieldTok.value.toUpperCase()
76
+ if (!isExtractField(fieldUpper)) {
77
+ throw new SyntaxError({
78
+ expected: 'extract field (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, EPOCH)',
79
+ after: 'EXTRACT(',
80
+ ...fieldTok,
81
+ })
82
+ }
83
+ expect(state, 'keyword', 'FROM')
84
+ const expr = parseExpression(state)
85
+ expect(state, 'paren', ')')
86
+ return {
87
+ type: 'function',
88
+ funcName: tok.originalValue ?? tok.value,
89
+ args: [
90
+ { type: 'literal', value: fieldTok.value, positionStart: fieldTok.positionStart, positionEnd: fieldTok.positionEnd },
91
+ expr,
92
+ ],
93
+ positionStart,
94
+ positionEnd: state.lastPos,
95
+ }
96
+ }
97
+
98
+ // function call
99
+ if (niladicFuncs.includes(funcNameUpper) || next.type === 'paren' && next.value === '(') {
100
+
101
+ // Validate function existence early for better error messages
102
+ if (!isKnownFunction(funcNameUpper, state.functions)) {
103
+ throw new UnknownFunctionError({
104
+ funcName: tok.value,
105
+ positionStart,
106
+ positionEnd: tok.positionEnd,
107
+ })
108
+ }
109
+
110
+ return parseFunctionCall(state, positionStart)
111
+ }
112
+
113
+ // Table identifier
114
+ let name = consume(state).value
115
+
116
+ // table.column
117
+ if (match(state, 'dot')) {
118
+ name += '.' + expect(state, 'identifier').value
119
+ }
120
+
121
+ return {
122
+ type: 'identifier',
123
+ name,
124
+ positionStart,
125
+ positionEnd: state.lastPos,
126
+ }
127
+ }
128
+
129
+ if (tok.type === 'number' || tok.type === 'string') {
130
+ consume(state)
131
+ return {
132
+ type: 'literal',
133
+ value: tok.numericValue ?? tok.value,
134
+ positionStart,
135
+ positionEnd: state.lastPos,
136
+ }
137
+ }
138
+
139
+ // Keywords that can be used as function names (e.g., LEFT, RIGHT)
140
+ if (tok.type === 'keyword') {
141
+ const next = peekToken(state, 1)
142
+ if (next.type === 'paren' && next.value === '(' && isKnownFunction(tok.value, state.functions)) {
143
+ return parseFunctionCall(state, positionStart)
144
+ }
145
+
146
+ if (match(state, 'keyword', 'TRUE')) {
147
+ return { type: 'literal', value: true, positionStart, positionEnd: state.lastPos }
148
+ }
149
+ if (match(state, 'keyword', 'FALSE')) {
150
+ return { type: 'literal', value: false, positionStart, positionEnd: state.lastPos }
151
+ }
152
+ if (match(state, 'keyword', 'NULL')) {
153
+ return { type: 'literal', value: null, positionStart, positionEnd: state.lastPos }
154
+ }
155
+ if (match(state, 'keyword', 'EXISTS')) {
156
+ expect(state, 'paren', '(')
157
+ const subquery = parseStatement(state)
158
+ expect(state, 'paren', ')')
159
+ return {
160
+ type: 'exists',
161
+ subquery,
162
+ positionStart,
163
+ positionEnd: state.lastPos,
164
+ }
165
+ }
166
+ if (match(state, 'keyword', 'CASE')) {
167
+ // Check if it's simple CASE (CASE expr WHEN ...) or searched CASE (CASE WHEN ...)
168
+ /** @type {ExprNode | undefined} */
169
+ let caseExpr
170
+ const next = current(state)
171
+ if (next.type !== 'keyword' || next.value !== 'WHEN') {
172
+ // Simple CASE: parse the case expression
173
+ caseExpr = parseExpression(state)
174
+ }
175
+
176
+ // Parse WHEN clauses
177
+ /** @type {WhenClause[]} */
178
+ const whenClauses = []
179
+ while (match(state, 'keyword', 'WHEN')) {
180
+ const condition = parseExpression(state)
181
+ expect(state, 'keyword', 'THEN')
182
+ const result = parseExpression(state)
183
+ whenClauses.push({
184
+ condition,
185
+ result,
186
+ positionStart: condition.positionStart,
187
+ positionEnd: result.positionEnd,
188
+ })
189
+ }
190
+
191
+ if (whenClauses.length === 0) {
192
+ throw new ParseError({
193
+ message: 'CASE expression requires at least one WHEN clause',
194
+ positionStart,
195
+ positionEnd: state.lastPos,
196
+ })
197
+ }
198
+
199
+ // Parse optional ELSE clause
200
+ /** @type {ExprNode | undefined} */
201
+ let elseResult
202
+ if (match(state, 'keyword', 'ELSE')) {
203
+ elseResult = parseExpression(state)
204
+ }
205
+
206
+ expect(state, 'keyword', 'END')
207
+
208
+ return {
209
+ type: 'case',
210
+ caseExpr,
211
+ whenClauses,
212
+ elseResult,
213
+ positionStart,
214
+ positionEnd: state.lastPos,
215
+ }
216
+ }
217
+
218
+ if (tok.value === 'INTERVAL') {
219
+ return parseInterval(state)
220
+ }
221
+
222
+ // Non-reserved keywords can be used as identifiers (e.g. column aliases)
223
+ if (!RESERVED_KEYWORDS.has(tok.value)) {
224
+ consume(state)
225
+ return {
226
+ type: 'identifier',
227
+ name: tok.originalValue ?? tok.value,
228
+ positionStart,
229
+ positionEnd: state.lastPos,
230
+ }
231
+ }
232
+ }
233
+
234
+ if (match(state, 'operator', '-')) {
235
+ const argument = parsePrimary(state)
236
+ return {
237
+ type: 'unary',
238
+ op: '-',
239
+ argument,
240
+ positionStart,
241
+ positionEnd: argument.positionEnd,
242
+ }
243
+ }
244
+
245
+ throw new SyntaxError({ expected: 'expression', ...tok })
246
+ }
247
+
248
+ /**
249
+ * @param {ParserState} state
250
+ * @returns {IntervalNode}
251
+ */
252
+ function parseInterval(state) {
253
+ const { positionStart } = expect(state, 'keyword', 'INTERVAL')
254
+
255
+ // Get value (number or quoted string)
256
+ const valueTok = consume(state)
257
+ /** @type {number} */
258
+ let value
259
+ if (valueTok.type === 'number') {
260
+ value = Number(valueTok.numericValue)
261
+ } else if (valueTok.type === 'string') {
262
+ value = parseFloat(valueTok.value)
263
+ if (isNaN(value)) {
264
+ throw new InvalidLiteralError({ expected: 'interval value', ...valueTok })
265
+ }
266
+ } else {
267
+ throw new SyntaxError({ expected: 'interval value (number)', ...valueTok })
268
+ }
269
+
270
+ // Get unit keyword
271
+ const unitTok = consume(state)
272
+ if (unitTok.type !== 'keyword' || !isIntervalUnit(unitTok.value)) {
273
+ throw new InvalidLiteralError({
274
+ expected: 'interval unit',
275
+ validValues: 'DAY, MONTH, YEAR, HOUR, MINUTE, SECOND',
276
+ ...unitTok,
277
+ })
278
+ }
279
+
280
+ return { type: 'interval', value, unit: unitTok.value, positionStart, positionEnd: state.lastPos }
281
+ }
@@ -1,4 +1,4 @@
1
- import { syntaxError } from '../validation/parseErrors.js'
1
+ import { SyntaxError } from '../validation/parseErrors.js'
2
2
 
3
3
  /**
4
4
  * @import { ParserState, Token, TokenType } from '../types.js'
@@ -34,7 +34,7 @@ export function consume(state) {
34
34
  const tok = current(state)
35
35
  state.lastPos = tok.positionEnd
36
36
  if (state.pos < state.tokens.length - 1) {
37
- state.pos += 1
37
+ state.pos++
38
38
  }
39
39
  return tok
40
40
  }
@@ -42,13 +42,13 @@ export function consume(state) {
42
42
  /**
43
43
  * @param {ParserState} state
44
44
  * @param {TokenType} type
45
- * @param {string} [value]
45
+ * @param {string} [expected]
46
46
  * @returns {boolean}
47
47
  */
48
- export function match(state, type, value) {
48
+ export function match(state, type, expected) {
49
49
  const tok = current(state)
50
50
  if (tok.type !== type) return false
51
- if (typeof value === 'string' && tok.value !== value) return false
51
+ if (expected && tok.value !== expected) return false
52
52
  consume(state)
53
53
  return true
54
54
  }
@@ -56,26 +56,13 @@ export function match(state, type, value) {
56
56
  /**
57
57
  * @param {ParserState} state
58
58
  * @param {TokenType} type
59
- * @param {string} value
59
+ * @param {string} [expected]
60
60
  * @returns {Token}
61
61
  */
62
- export function expect(state, type, value) {
62
+ export function expect(state, type, expected) {
63
63
  const tok = current(state)
64
- if (tok.type !== type || tok.value !== value) {
65
- throw parseError(state, value)
66
- }
67
- consume(state)
68
- return tok
69
- }
70
-
71
- /**
72
- * @param {ParserState} state
73
- * @returns {Token}
74
- */
75
- export function expectIdentifier(state) {
76
- const tok = current(state)
77
- if (tok.type !== 'identifier') {
78
- throw parseError(state, 'identifier')
64
+ if (tok.type !== type || expected && tok.value !== expected) {
65
+ throw parseError(state, expected ?? type)
79
66
  }
80
67
  consume(state)
81
68
  return tok
@@ -90,7 +77,6 @@ export function expectIdentifier(state) {
90
77
  export function parseError(state, expected) {
91
78
  const tok = current(state)
92
79
  const prevToken = state.tokens[state.pos - 1]
93
- const after = prevToken ? prevToken.originalValue ?? prevToken.value : undefined
94
- const received = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
95
- return syntaxError({ expected, received, positionStart: tok.positionStart, positionEnd: tok.positionEnd, after })
80
+ const after = prevToken?.originalValue ?? prevToken?.value
81
+ return new SyntaxError({ expected, after, ...tok })
96
82
  }