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
|
@@ -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
|
+
}
|
package/src/parse/state.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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} [
|
|
45
|
+
* @param {string} [expected]
|
|
46
46
|
* @returns {boolean}
|
|
47
47
|
*/
|
|
48
|
-
export function match(state, type,
|
|
48
|
+
export function match(state, type, expected) {
|
|
49
49
|
const tok = current(state)
|
|
50
50
|
if (tok.type !== type) return false
|
|
51
|
-
if (
|
|
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}
|
|
59
|
+
* @param {string} [expected]
|
|
60
60
|
* @returns {Token}
|
|
61
61
|
*/
|
|
62
|
-
export function expect(state, type,
|
|
62
|
+
export function expect(state, type, expected) {
|
|
63
63
|
const tok = current(state)
|
|
64
|
-
if (tok.type !== type || tok.value !==
|
|
65
|
-
throw parseError(state,
|
|
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
|
|
94
|
-
|
|
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
|
}
|