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/functions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isAggregateFunc,
|
|
2
|
-
import { ParseError,
|
|
1
|
+
import { isAggregateFunc, isKnownFunction, niladicFuncs, validateFunctionArgs } from '../validation/functions.js'
|
|
2
|
+
import { ParseError, UnknownFunctionError } from '../validation/parseErrors.js'
|
|
3
3
|
import { parseExpression } from './expression.js'
|
|
4
4
|
import { consume, current, expect, match } from './state.js'
|
|
5
5
|
|
|
@@ -8,17 +8,33 @@ import { consume, current, expect, match } from './state.js'
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Parses a function call after the function name has been identified.
|
|
12
|
-
* Expects the current token to be '('.
|
|
13
|
-
*
|
|
14
11
|
* @param {ParserState} state
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {number} positionStart - Start position of the function name
|
|
12
|
+
* @param {number} positionStart
|
|
17
13
|
* @returns {ExprNode}
|
|
18
14
|
*/
|
|
19
|
-
export function parseFunctionCall(state,
|
|
15
|
+
export function parseFunctionCall(state, positionStart) {
|
|
16
|
+
const funcTok = consume(state)
|
|
17
|
+
const funcName = funcTok.originalValue ?? funcTok.value
|
|
20
18
|
const funcNameUpper = funcName.toUpperCase()
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
// Validate function existence early for better error messages
|
|
21
|
+
if (!isKnownFunction(funcNameUpper, state.functions)) {
|
|
22
|
+
throw new UnknownFunctionError({ funcName, ...funcTok })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Niladic datetime functions (no parentheses required per ANSI SQL)
|
|
26
|
+
const parens = current(state)
|
|
27
|
+
if (niladicFuncs.includes(funcNameUpper) && parens.type !== 'paren' || parens.value !== '(') {
|
|
28
|
+
return {
|
|
29
|
+
type: 'function',
|
|
30
|
+
funcName,
|
|
31
|
+
args: [],
|
|
32
|
+
positionStart,
|
|
33
|
+
positionEnd: state.lastPos,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(state, 'paren', '(')
|
|
22
38
|
|
|
23
39
|
/** @type {ExprNode[]} */
|
|
24
40
|
const args = []
|
|
@@ -26,51 +42,30 @@ export function parseFunctionCall(state, funcName, positionStart) {
|
|
|
26
42
|
let distinct
|
|
27
43
|
|
|
28
44
|
// Check for DISTINCT or ALL keyword (for aggregate functions like COUNT(DISTINCT x))
|
|
29
|
-
if (
|
|
30
|
-
consume(state)
|
|
45
|
+
if (match(state, 'keyword', 'DISTINCT')) {
|
|
31
46
|
distinct = true
|
|
32
|
-
} else
|
|
33
|
-
|
|
47
|
+
} else {
|
|
48
|
+
match(state, 'keyword', 'ALL')
|
|
34
49
|
}
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const starTok = current(state)
|
|
41
|
-
consume(state)
|
|
42
|
-
args.push({
|
|
43
|
-
type: 'star',
|
|
44
|
-
positionStart: starTok.positionStart,
|
|
45
|
-
positionEnd: state.lastPos,
|
|
46
|
-
})
|
|
47
|
-
} else {
|
|
48
|
-
args.push(parseExpression(state))
|
|
49
|
-
}
|
|
50
|
-
if (!match(state, 'comma')) break
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
expect(state, 'paren', ')')
|
|
51
|
+
// Parse function arguments
|
|
52
|
+
while (true) {
|
|
53
|
+
const next = current(state)
|
|
54
|
+
if (next.type === 'paren' && next.value === ')') break
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
expected: 'aggregate function for FILTER clause',
|
|
62
|
-
received: `FILTER on non-aggregate function "${funcName}"`,
|
|
63
|
-
positionStart: current(state).positionStart,
|
|
64
|
-
positionEnd: current(state).positionEnd,
|
|
56
|
+
// Handle COUNT(*) - treat * as a special identifier
|
|
57
|
+
if (match(state, 'operator', '*')) {
|
|
58
|
+
args.push({
|
|
59
|
+
type: 'star',
|
|
60
|
+
positionStart: next.positionStart,
|
|
61
|
+
positionEnd: state.lastPos,
|
|
65
62
|
})
|
|
63
|
+
} else {
|
|
64
|
+
args.push(parseExpression(state))
|
|
66
65
|
}
|
|
67
|
-
|
|
68
|
-
expect(state, 'paren', '(')
|
|
69
|
-
expect(state, 'keyword', 'WHERE')
|
|
70
|
-
filter = parseExpression(state)
|
|
71
|
-
expect(state, 'paren', ')')
|
|
66
|
+
if (!match(state, 'comma')) break
|
|
72
67
|
}
|
|
73
|
-
|
|
68
|
+
expect(state, 'paren', ')')
|
|
74
69
|
|
|
75
70
|
// Validate star argument at parse time (only COUNT supports *)
|
|
76
71
|
const hasStar = args.length === 1 && args[0].type === 'star'
|
|
@@ -78,19 +73,36 @@ export function parseFunctionCall(state, funcName, positionStart) {
|
|
|
78
73
|
throw new ParseError({
|
|
79
74
|
message: `${funcName} cannot be applied to "*"`,
|
|
80
75
|
positionStart,
|
|
81
|
-
positionEnd,
|
|
76
|
+
positionEnd: state.lastPos,
|
|
82
77
|
})
|
|
83
78
|
}
|
|
84
79
|
if (hasStar && distinct) {
|
|
85
80
|
throw new ParseError({
|
|
86
81
|
message: 'COUNT(DISTINCT *) is not allowed',
|
|
87
82
|
positionStart,
|
|
88
|
-
positionEnd,
|
|
83
|
+
positionEnd: state.lastPos,
|
|
89
84
|
})
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
// Validate argument count at parse time
|
|
93
|
-
|
|
88
|
+
validateFunctionArgs(funcNameUpper, args.length, positionStart, state.lastPos, state.functions)
|
|
89
|
+
|
|
90
|
+
// Check for FILTER clause (only valid for aggregate functions)
|
|
91
|
+
/** @type {ExprNode | undefined} */
|
|
92
|
+
let filter
|
|
93
|
+
const filterTok = current(state)
|
|
94
|
+
if (match(state, 'keyword', 'FILTER')) {
|
|
95
|
+
if (!isAggregateFunc(funcNameUpper)) {
|
|
96
|
+
throw new ParseError({
|
|
97
|
+
message: `FILTER cannot be applied to non-aggregate function "${funcName}"`,
|
|
98
|
+
...filterTok,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
expect(state, 'paren', '(')
|
|
102
|
+
expect(state, 'keyword', 'WHERE')
|
|
103
|
+
filter = parseExpression(state)
|
|
104
|
+
expect(state, 'paren', ')')
|
|
105
|
+
}
|
|
94
106
|
|
|
95
107
|
return {
|
|
96
108
|
type: 'function',
|
|
@@ -99,6 +111,6 @@ export function parseFunctionCall(state, funcName, positionStart) {
|
|
|
99
111
|
distinct,
|
|
100
112
|
filter,
|
|
101
113
|
positionStart,
|
|
102
|
-
positionEnd,
|
|
114
|
+
positionEnd: state.lastPos,
|
|
103
115
|
}
|
|
104
116
|
}
|
package/src/parse/joins.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expectNoAggregate } from '../validation/aggregates.js'
|
|
2
2
|
import { parseExpression } from './expression.js'
|
|
3
3
|
import { parseTableAlias } from './parse.js'
|
|
4
|
-
import {
|
|
4
|
+
import { current, expect, match } from './state.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @import { ExprNode, JoinClause, JoinType, ParserState } from '../types.js'
|
|
@@ -22,45 +22,31 @@ export function parseJoins(state) {
|
|
|
22
22
|
/** @type {JoinType} */
|
|
23
23
|
let joinType = 'INNER'
|
|
24
24
|
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
joinType = 'FULL'
|
|
41
|
-
} else if (tok.value === 'POSITIONAL') {
|
|
42
|
-
consume(state)
|
|
43
|
-
joinType = 'POSITIONAL'
|
|
44
|
-
} else if (tok.value === 'JOIN') {
|
|
45
|
-
// Just JOIN (defaults to INNER)
|
|
46
|
-
consume(state)
|
|
47
|
-
} else {
|
|
48
|
-
// Not a join keyword, stop parsing joins
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// If we consumed a join type keyword (INNER/LEFT/RIGHT/FULL), expect JOIN
|
|
53
|
-
if (tok.value !== 'JOIN') {
|
|
54
|
-
expect(state, 'keyword', 'JOIN')
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
// No more joins
|
|
25
|
+
if (match(state, 'keyword', 'INNER')) {
|
|
26
|
+
joinType = 'INNER'
|
|
27
|
+
} else if (match(state, 'keyword', 'LEFT')) {
|
|
28
|
+
match(state, 'keyword', 'OUTER') // LEFT OUTER JOIN
|
|
29
|
+
joinType = 'LEFT'
|
|
30
|
+
} else if (match(state, 'keyword', 'RIGHT')) {
|
|
31
|
+
match(state, 'keyword', 'OUTER') // RIGHT OUTER JOIN
|
|
32
|
+
joinType = 'RIGHT'
|
|
33
|
+
} else if (match(state, 'keyword', 'FULL')) {
|
|
34
|
+
match(state, 'keyword', 'OUTER') // FULL OUTER JOIN
|
|
35
|
+
joinType = 'FULL'
|
|
36
|
+
} else if (match(state, 'keyword', 'POSITIONAL')) {
|
|
37
|
+
joinType = 'POSITIONAL'
|
|
38
|
+
} else if (!match(state, 'keyword', 'JOIN')) {
|
|
39
|
+
// Not a join keyword, stop parsing joins
|
|
58
40
|
break
|
|
59
41
|
}
|
|
60
42
|
|
|
43
|
+
// If we consumed a type keyword, expect JOIN next
|
|
44
|
+
if (tok.value !== 'JOIN') {
|
|
45
|
+
expect(state, 'keyword', 'JOIN')
|
|
46
|
+
}
|
|
47
|
+
|
|
61
48
|
// Parse table name and optional alias
|
|
62
|
-
const tableTok =
|
|
63
|
-
const tableName = tableTok.value
|
|
49
|
+
const tableTok = expect(state, 'identifier')
|
|
64
50
|
const tableAlias = parseTableAlias(state)
|
|
65
51
|
|
|
66
52
|
// Parse ON condition (not for POSITIONAL joins)
|
|
@@ -74,10 +60,10 @@ export function parseJoins(state) {
|
|
|
74
60
|
|
|
75
61
|
joins.push({
|
|
76
62
|
joinType,
|
|
77
|
-
table:
|
|
63
|
+
table: tableTok.value,
|
|
78
64
|
alias: tableAlias,
|
|
79
65
|
on: condition,
|
|
80
|
-
positionStart:
|
|
66
|
+
positionStart: tok.positionStart,
|
|
81
67
|
positionEnd: tableTok.positionEnd,
|
|
82
68
|
})
|
|
83
69
|
}
|