squirreling 0.10.0 → 0.10.2
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/package.json +8 -6
- package/src/ast.d.ts +184 -0
- package/src/execute/execute.js +60 -12
- package/src/execute/utils.js +14 -14
- package/src/expression/alias.js +2 -2
- package/src/expression/evaluate.js +35 -68
- package/src/expression/regexp.js +13 -25
- package/src/expression/strings.js +14 -22
- package/src/index.d.ts +1 -0
- package/src/parse/comparison.js +2 -2
- package/src/parse/expression.js +21 -10
- package/src/parse/functions.js +14 -23
- package/src/parse/joins.js +5 -2
- package/src/parse/parse.js +5 -4
- package/src/parse/state.js +2 -2
- package/src/parse/tokenize.js +2 -9
- package/src/parse/types.d.ts +1 -1
- package/src/plan/plan.js +45 -12
- package/src/spatial/bbox.js +1 -1
- package/src/types.d.ts +12 -191
- package/src/validation/aggregates.js +67 -0
- package/src/validation/executionErrors.js +35 -0
- package/src/validation/expressionErrors.js +57 -0
- package/src/validation/functions.js +281 -0
- package/src/{parseErrors.js → validation/parseErrors.js} +21 -57
- package/src/validation/planErrors.js +42 -0
- package/src/executionErrors.js +0 -80
- package/src/validation.js +0 -343
- package/src/validationErrors.js +0 -141
package/src/expression/regexp.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { argValueError } from '../
|
|
1
|
+
import { argValueError } from '../validation/expressionErrors.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @import { SqlPrimitive } from '../types.js'
|
|
4
|
+
* @import { FunctionNode, RegExpFunction, SqlPrimitive } from '../types.js'
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Evaluate a regexp function
|
|
9
9
|
*
|
|
10
10
|
* @param {Object} options
|
|
11
|
-
* @param {
|
|
11
|
+
* @param {RegExpFunction} options.funcName
|
|
12
|
+
* @param {FunctionNode} options.node
|
|
12
13
|
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
13
|
-
* @param {number} options.
|
|
14
|
-
* @param {number} options.positionEnd - End position in SQL string for error reporting
|
|
15
|
-
* @param {number} [options.rowIndex] - Row number for error reporting
|
|
14
|
+
* @param {number} options.rowIndex - Row index for error reporting
|
|
16
15
|
* @returns {SqlPrimitive}
|
|
17
16
|
*/
|
|
18
|
-
export function evaluateRegexpFunc({ funcName,
|
|
17
|
+
export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
19
18
|
if (funcName === 'REGEXP_SUBSTR') {
|
|
20
19
|
const str = args[0]
|
|
21
20
|
const pattern = args[1]
|
|
@@ -29,10 +28,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
29
28
|
position = Number(args[2])
|
|
30
29
|
if (!Number.isInteger(position) || position < 1) {
|
|
31
30
|
throw argValueError({
|
|
32
|
-
|
|
31
|
+
...node,
|
|
33
32
|
message: `position must be a positive integer, got ${args[2]}`,
|
|
34
|
-
positionStart,
|
|
35
|
-
positionEnd,
|
|
36
33
|
hint: 'SQL uses 1-based indexing.',
|
|
37
34
|
rowIndex,
|
|
38
35
|
})
|
|
@@ -45,10 +42,9 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
45
42
|
occurrence = Number(args[3])
|
|
46
43
|
if (!Number.isInteger(occurrence) || occurrence < 1) {
|
|
47
44
|
throw argValueError({
|
|
48
|
-
|
|
45
|
+
...node,
|
|
49
46
|
message: `occurrence must be a positive integer, got ${args[3]}`,
|
|
50
|
-
|
|
51
|
-
positionEnd,
|
|
47
|
+
hint: 'SQL uses 1-based indexing.',
|
|
52
48
|
rowIndex,
|
|
53
49
|
})
|
|
54
50
|
}
|
|
@@ -60,10 +56,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
60
56
|
regex = new RegExp(patternStr, 'g')
|
|
61
57
|
} catch (/** @type {any} */ error) {
|
|
62
58
|
throw argValueError({
|
|
63
|
-
|
|
59
|
+
...node,
|
|
64
60
|
message: `invalid regex pattern: ${error.message}`,
|
|
65
|
-
positionStart,
|
|
66
|
-
positionEnd,
|
|
67
61
|
rowIndex,
|
|
68
62
|
})
|
|
69
63
|
}
|
|
@@ -99,10 +93,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
99
93
|
position = Number(args[3])
|
|
100
94
|
if (!Number.isInteger(position) || position < 1) {
|
|
101
95
|
throw argValueError({
|
|
102
|
-
|
|
96
|
+
...node,
|
|
103
97
|
message: `position must be a positive integer, got ${args[3]}`,
|
|
104
|
-
positionStart,
|
|
105
|
-
positionEnd,
|
|
106
98
|
hint: 'SQL uses 1-based indexing.',
|
|
107
99
|
rowIndex,
|
|
108
100
|
})
|
|
@@ -115,10 +107,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
115
107
|
occurrence = Number(args[4])
|
|
116
108
|
if (!Number.isInteger(occurrence) || occurrence < 0) {
|
|
117
109
|
throw argValueError({
|
|
118
|
-
|
|
110
|
+
...node,
|
|
119
111
|
message: `occurrence must be a non-negative integer, got ${args[4]}`,
|
|
120
|
-
positionStart,
|
|
121
|
-
positionEnd,
|
|
122
112
|
hint: 'Use 0 to replace all occurrences.',
|
|
123
113
|
rowIndex,
|
|
124
114
|
})
|
|
@@ -131,10 +121,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
131
121
|
regex = new RegExp(patternStr, 'g')
|
|
132
122
|
} catch (/** @type {any} */ error) {
|
|
133
123
|
throw argValueError({
|
|
134
|
-
|
|
124
|
+
...node,
|
|
135
125
|
message: `invalid regex pattern: ${error.message}`,
|
|
136
|
-
positionStart,
|
|
137
|
-
positionEnd,
|
|
138
126
|
rowIndex,
|
|
139
127
|
})
|
|
140
128
|
}
|
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { SqlPrimitive, StringFunc } from '../types.js'
|
|
2
|
+
* @import { FunctionNode, SqlPrimitive, StringFunc } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { argValueError } from '../
|
|
5
|
+
import { argValueError } from '../validation/expressionErrors.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Evaluate a string function
|
|
9
9
|
*
|
|
10
10
|
* @param {Object} options
|
|
11
|
-
* @param {StringFunc} options.funcName
|
|
11
|
+
* @param {StringFunc} options.funcName
|
|
12
|
+
* @param {FunctionNode} options.node
|
|
12
13
|
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
13
|
-
* @param {number} options.
|
|
14
|
-
* @param {number} options.positionEnd - End position for error reporting
|
|
15
|
-
* @param {number} [options.rowIndex] - Row index for error reporting
|
|
14
|
+
* @param {number} options.rowIndex - Row index for error reporting
|
|
16
15
|
* @returns {SqlPrimitive}
|
|
17
16
|
*/
|
|
18
|
-
export function evaluateStringFunc({ funcName,
|
|
17
|
+
export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
19
18
|
if (funcName === 'CONCAT') {
|
|
20
19
|
// Returns NULL if any argument is NULL
|
|
21
20
|
if (args.some(a => a == null)) return null
|
|
22
21
|
if (args.some(a => typeof a === 'object')) {
|
|
23
22
|
throw argValueError({
|
|
24
|
-
|
|
23
|
+
...node,
|
|
25
24
|
message: 'does not support object arguments',
|
|
26
|
-
positionStart,
|
|
27
|
-
positionEnd,
|
|
28
25
|
hint: 'Use CAST to convert objects to strings first.',
|
|
29
26
|
rowIndex,
|
|
30
27
|
})
|
|
@@ -53,10 +50,8 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
53
50
|
const start = Number(args[1])
|
|
54
51
|
if (!Number.isInteger(start) || start < 1) {
|
|
55
52
|
throw argValueError({
|
|
56
|
-
|
|
53
|
+
...node,
|
|
57
54
|
message: `start position must be a positive integer, got ${args[1]}`,
|
|
58
|
-
positionStart,
|
|
59
|
-
positionEnd,
|
|
60
55
|
hint: 'SQL uses 1-based indexing.',
|
|
61
56
|
rowIndex,
|
|
62
57
|
})
|
|
@@ -67,10 +62,9 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
67
62
|
const len = Number(args[2])
|
|
68
63
|
if (!Number.isInteger(len) || len < 0) {
|
|
69
64
|
throw argValueError({
|
|
70
|
-
|
|
65
|
+
...node,
|
|
71
66
|
message: `length must be a non-negative integer, got ${args[2]}`,
|
|
72
|
-
|
|
73
|
-
positionEnd,
|
|
67
|
+
hint: 'SQL uses 1-based indexing.',
|
|
74
68
|
rowIndex,
|
|
75
69
|
})
|
|
76
70
|
}
|
|
@@ -97,10 +91,9 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
97
91
|
const len = Number(n)
|
|
98
92
|
if (!Number.isInteger(len) || len < 0) {
|
|
99
93
|
throw argValueError({
|
|
100
|
-
|
|
94
|
+
...node,
|
|
101
95
|
message: `length must be a non-negative integer, got ${n}`,
|
|
102
|
-
|
|
103
|
-
positionEnd,
|
|
96
|
+
hint: 'SQL uses 1-based indexing.',
|
|
104
97
|
rowIndex,
|
|
105
98
|
})
|
|
106
99
|
}
|
|
@@ -113,10 +106,9 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
113
106
|
const len = Number(n)
|
|
114
107
|
if (!Number.isInteger(len) || len < 0) {
|
|
115
108
|
throw argValueError({
|
|
116
|
-
|
|
109
|
+
...node,
|
|
117
110
|
message: `length must be a non-negative integer, got ${n}`,
|
|
118
|
-
|
|
119
|
-
positionEnd,
|
|
111
|
+
hint: 'SQL uses 1-based indexing.',
|
|
120
112
|
rowIndex,
|
|
121
113
|
})
|
|
122
114
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function parseSql(options: ParseSqlOptions): SelectStatement
|
|
|
55
55
|
* @param options
|
|
56
56
|
* @param options.query - SQL query string or parsed SelectStatement
|
|
57
57
|
* @param options.functions - user-defined functions available in the SQL context
|
|
58
|
+
* @param options.tables - optional table metadata for planning
|
|
58
59
|
* @returns the root of the query plan tree
|
|
59
60
|
*/
|
|
60
61
|
export function planSql(options: PlanSqlOptions): QueryPlan
|
package/src/parse/comparison.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isBinaryOp } from '../validation/functions.js'
|
|
2
|
+
import { syntaxError } from '../validation/parseErrors.js'
|
|
3
3
|
import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
|
|
4
4
|
import { consume, current, expect, match, peekToken } from './state.js'
|
|
5
5
|
|
package/src/parse/expression.js
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
missingClauseError,
|
|
4
4
|
syntaxError,
|
|
5
5
|
unknownFunctionError,
|
|
6
|
-
} from '../parseErrors.js'
|
|
7
|
-
import { RESERVED_KEYWORDS, isExtractField, isIntervalUnit, isKnownFunction } from '../validation.js'
|
|
6
|
+
} from '../validation/parseErrors.js'
|
|
7
|
+
import { RESERVED_KEYWORDS, isCastType, isExtractField, isIntervalUnit, isKnownFunction } from '../validation/functions.js'
|
|
8
8
|
import { parseComparison } from './comparison.js'
|
|
9
9
|
import { parseFunctionCall } from './functions.js'
|
|
10
10
|
import { parseSelectInternal } from './parse.js'
|
|
@@ -60,11 +60,19 @@ export function parsePrimary(state) {
|
|
|
60
60
|
const expr = parseExpression(state)
|
|
61
61
|
expect(state, 'keyword', 'AS')
|
|
62
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
|
+
}
|
|
63
71
|
expect(state, 'paren', ')')
|
|
64
72
|
return {
|
|
65
73
|
type: 'cast',
|
|
66
74
|
expr,
|
|
67
|
-
toType
|
|
75
|
+
toType,
|
|
68
76
|
positionStart,
|
|
69
77
|
positionEnd: state.lastPos,
|
|
70
78
|
}
|
|
@@ -75,13 +83,11 @@ export function parsePrimary(state) {
|
|
|
75
83
|
consume(state) // EXTRACT
|
|
76
84
|
consume(state) // '('
|
|
77
85
|
const fieldTok = current(state)
|
|
78
|
-
|
|
79
|
-
if (!isValidType || !isExtractField(fieldTok.value)) {
|
|
86
|
+
if (!isExtractField(fieldTok.value)) {
|
|
80
87
|
throw syntaxError({
|
|
88
|
+
...fieldTok,
|
|
81
89
|
expected: 'extract field (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, EPOCH)',
|
|
82
90
|
received: `"${fieldTok.value}"`,
|
|
83
|
-
positionStart: fieldTok.positionStart,
|
|
84
|
-
positionEnd: fieldTok.positionEnd,
|
|
85
91
|
})
|
|
86
92
|
}
|
|
87
93
|
consume(state) // field
|
|
@@ -90,7 +96,7 @@ export function parsePrimary(state) {
|
|
|
90
96
|
expect(state, 'paren', ')')
|
|
91
97
|
return {
|
|
92
98
|
type: 'function',
|
|
93
|
-
|
|
99
|
+
funcName: 'EXTRACT',
|
|
94
100
|
args: [
|
|
95
101
|
{ type: 'literal', value: fieldTok.value, positionStart: fieldTok.positionStart, positionEnd: fieldTok.positionEnd },
|
|
96
102
|
expr,
|
|
@@ -123,7 +129,7 @@ export function parsePrimary(state) {
|
|
|
123
129
|
consume(state)
|
|
124
130
|
return {
|
|
125
131
|
type: 'function',
|
|
126
|
-
|
|
132
|
+
funcName: tok.value,
|
|
127
133
|
args: [],
|
|
128
134
|
positionStart,
|
|
129
135
|
positionEnd: state.lastPos,
|
|
@@ -217,7 +223,12 @@ export function parsePrimary(state) {
|
|
|
217
223
|
const condition = parseExpression(state)
|
|
218
224
|
expect(state, 'keyword', 'THEN')
|
|
219
225
|
const result = parseExpression(state)
|
|
220
|
-
whenClauses.push({
|
|
226
|
+
whenClauses.push({
|
|
227
|
+
condition,
|
|
228
|
+
result,
|
|
229
|
+
positionStart: condition.positionStart,
|
|
230
|
+
positionEnd: result.positionEnd,
|
|
231
|
+
})
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
if (whenClauses.length === 0) {
|
package/src/parse/functions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isAggregateFunc, validateFunctionArgCount } from '../validation/functions.js'
|
|
2
|
+
import { ParseError, syntaxError } from '../validation/parseErrors.js'
|
|
3
3
|
import { parseExpression } from './expression.js'
|
|
4
4
|
import { consume, current, expect, match } from './state.js'
|
|
5
5
|
|
|
@@ -17,11 +17,13 @@ import { consume, current, expect, match } from './state.js'
|
|
|
17
17
|
* @returns {ExprNode}
|
|
18
18
|
*/
|
|
19
19
|
export function parseFunctionCall(state, funcName, positionStart) {
|
|
20
|
-
|
|
20
|
+
const funcNameUpper = funcName.toUpperCase()
|
|
21
|
+
consume(state) // '(' checked by caller
|
|
21
22
|
|
|
22
23
|
/** @type {ExprNode[]} */
|
|
23
24
|
const args = []
|
|
24
|
-
|
|
25
|
+
/** @type {true | undefined} */
|
|
26
|
+
let distinct
|
|
25
27
|
|
|
26
28
|
// Check for DISTINCT or ALL keyword (for aggregate functions like COUNT(DISTINCT x))
|
|
27
29
|
if (current(state).type === 'keyword' && current(state).value === 'DISTINCT') {
|
|
@@ -48,14 +50,12 @@ export function parseFunctionCall(state, funcName, positionStart) {
|
|
|
48
50
|
if (!match(state, 'comma')) break
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
|
|
52
53
|
expect(state, 'paren', ')')
|
|
53
54
|
|
|
54
55
|
// Check for FILTER clause (only valid for aggregate functions)
|
|
55
56
|
/** @type {ExprNode | undefined} */
|
|
56
57
|
let filter
|
|
57
58
|
if (current(state).type === 'keyword' && current(state).value === 'FILTER') {
|
|
58
|
-
const funcNameUpper = funcName.toUpperCase()
|
|
59
59
|
if (!isAggregateFunc(funcNameUpper)) {
|
|
60
60
|
throw syntaxError({
|
|
61
61
|
expected: 'aggregate function for FILTER clause',
|
|
@@ -70,44 +70,35 @@ export function parseFunctionCall(state, funcName, positionStart) {
|
|
|
70
70
|
filter = parseExpression(state)
|
|
71
71
|
expect(state, 'paren', ')')
|
|
72
72
|
}
|
|
73
|
+
const positionEnd = state.lastPos
|
|
73
74
|
|
|
74
75
|
// Validate star argument at parse time (only COUNT supports *)
|
|
75
|
-
const funcNameUpper = funcName.toUpperCase()
|
|
76
76
|
const hasStar = args.length === 1 && args[0].type === 'star'
|
|
77
|
-
if (hasStar &&
|
|
77
|
+
if (hasStar && funcNameUpper !== 'COUNT') {
|
|
78
78
|
throw new ParseError({
|
|
79
79
|
message: `${funcName} cannot be applied to "*"`,
|
|
80
80
|
positionStart,
|
|
81
|
-
positionEnd
|
|
81
|
+
positionEnd,
|
|
82
82
|
})
|
|
83
83
|
}
|
|
84
84
|
if (hasStar && distinct) {
|
|
85
85
|
throw new ParseError({
|
|
86
86
|
message: 'COUNT(DISTINCT *) is not allowed',
|
|
87
87
|
positionStart,
|
|
88
|
-
positionEnd
|
|
88
|
+
positionEnd,
|
|
89
89
|
})
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// Validate argument count at parse time
|
|
93
|
-
|
|
94
|
-
if (!validation.valid) {
|
|
95
|
-
throw argCountParseError({
|
|
96
|
-
funcName,
|
|
97
|
-
expected: validation.expected,
|
|
98
|
-
received: args.length,
|
|
99
|
-
positionStart,
|
|
100
|
-
positionEnd: state.lastPos,
|
|
101
|
-
})
|
|
102
|
-
}
|
|
93
|
+
validateFunctionArgCount(funcNameUpper, args.length, positionStart, positionEnd, state.functions)
|
|
103
94
|
|
|
104
95
|
return {
|
|
105
96
|
type: 'function',
|
|
106
|
-
|
|
97
|
+
funcName,
|
|
107
98
|
args,
|
|
108
|
-
distinct
|
|
99
|
+
distinct,
|
|
109
100
|
filter,
|
|
110
101
|
positionStart,
|
|
111
|
-
positionEnd
|
|
102
|
+
positionEnd,
|
|
112
103
|
}
|
|
113
104
|
}
|
package/src/parse/joins.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { expectNoAggregate } from '../validation/aggregates.js'
|
|
1
2
|
import { parseExpression } from './expression.js'
|
|
2
3
|
import { parseTableAlias } from './parse.js'
|
|
3
4
|
import { consume, current, expect, expectIdentifier, match } from './state.js'
|
|
4
|
-
import { expectNoAggregate } from '../validation.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @import { ExprNode, JoinClause, JoinType, ParserState } from '../types.js'
|
|
@@ -59,7 +59,8 @@ export function parseJoins(state) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Parse table name and optional alias
|
|
62
|
-
const
|
|
62
|
+
const tableTok = expectIdentifier(state)
|
|
63
|
+
const tableName = tableTok.value
|
|
63
64
|
const tableAlias = parseTableAlias(state)
|
|
64
65
|
|
|
65
66
|
// Parse ON condition (not for POSITIONAL joins)
|
|
@@ -76,6 +77,8 @@ export function parseJoins(state) {
|
|
|
76
77
|
table: tableName,
|
|
77
78
|
alias: tableAlias,
|
|
78
79
|
on: condition,
|
|
80
|
+
positionStart: tableTok.positionStart,
|
|
81
|
+
positionEnd: tableTok.positionEnd,
|
|
79
82
|
})
|
|
80
83
|
}
|
|
81
84
|
|
package/src/parse/parse.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { expectNoAggregate, findAggregate } from '../validation/aggregates.js'
|
|
2
|
+
import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE } from '../validation/functions.js'
|
|
3
|
+
import { duplicateCTEError } from '../validation/parseErrors.js'
|
|
1
4
|
import { parseExpression } from './expression.js'
|
|
2
5
|
import { parseJoins } from './joins.js'
|
|
3
6
|
import { consume, current, expect, expectIdentifier, match, parseError, peekToken } from './state.js'
|
|
4
7
|
import { tokenizeSql } from './tokenize.js'
|
|
5
|
-
import { duplicateCTEError } from '../parseErrors.js'
|
|
6
|
-
import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE, expectNoAggregate, findAggregate } from '../validation.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @import { CTEDefinition, ExprNode, FromSubquery, FromTable, OrderByItem, ParseSqlOptions, ParserState, SelectStatement, SelectColumn, WithClause } from '../types.js'
|
|
@@ -223,9 +224,9 @@ export function parseSelectInternal(state) {
|
|
|
223
224
|
from = parseFromSubquery(state)
|
|
224
225
|
} else {
|
|
225
226
|
// Simple table name: SELECT * FROM users
|
|
226
|
-
const
|
|
227
|
+
const tableTok = expectIdentifier(state)
|
|
227
228
|
const alias = parseTableAlias(state)
|
|
228
|
-
from = { kind: 'table', table, alias }
|
|
229
|
+
from = { kind: 'table', table: tableTok.value, alias, positionStart: tableTok.positionStart, positionEnd: tableTok.positionEnd }
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
// Parse JOIN clauses
|
package/src/parse/state.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { syntaxError } from '../parseErrors.js'
|
|
1
|
+
import { syntaxError } from '../validation/parseErrors.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @import { ParserState, Token, TokenType } from '../types.js'
|
|
5
|
-
* @import { ParseError } from '../parseErrors.js'
|
|
5
|
+
* @import { ParseError } from '../validation/parseErrors.js'
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
package/src/parse/tokenize.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
invalidLiteralError,
|
|
3
|
-
unexpectedCharError,
|
|
4
|
-
unterminatedError,
|
|
5
|
-
} from '../parseErrors.js'
|
|
1
|
+
import { invalidLiteralError, unexpectedCharError, unterminatedError } from '../validation/parseErrors.js'
|
|
6
2
|
|
|
7
3
|
/**
|
|
8
4
|
* @import { Token } from '../types.d.ts'
|
|
@@ -354,10 +350,7 @@ export function tokenizeSql(sql) {
|
|
|
354
350
|
continue
|
|
355
351
|
}
|
|
356
352
|
|
|
357
|
-
|
|
358
|
-
throw unexpectedCharError({ char: ch, positionStart: pos, expectsSelect: true })
|
|
359
|
-
}
|
|
360
|
-
throw unexpectedCharError({ char: ch, positionStart: pos })
|
|
353
|
+
throw unexpectedCharError({ char: ch, positionStart: pos, expectsSelect: !tokens.length })
|
|
361
354
|
}
|
|
362
355
|
|
|
363
356
|
tokens.push({
|
package/src/parse/types.d.ts
CHANGED
package/src/plan/plan.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { parseSql } from '../parse/parse.js'
|
|
2
|
-
import { findAggregate } from '../validation.js'
|
|
2
|
+
import { findAggregate } from '../validation/aggregates.js'
|
|
3
|
+
import { columnNotFoundError, tableNotFoundError } from '../validation/planErrors.js'
|
|
3
4
|
import { extractColumns } from './columns.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* @import { ExprNode, DerivedColumn, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement } from '../types.js'
|
|
7
|
+
* @import { AsyncDataSource, ExprNode, DerivedColumn, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement } from '../types.js'
|
|
7
8
|
* @import { QueryPlan } from './types.d.ts'
|
|
8
9
|
*/
|
|
9
10
|
|
|
@@ -14,7 +15,7 @@ import { extractColumns } from './columns.js'
|
|
|
14
15
|
* @param {PlanSqlOptions} options
|
|
15
16
|
* @returns {QueryPlan} the root of the query plan tree
|
|
16
17
|
*/
|
|
17
|
-
export function planSql({ query, functions }) {
|
|
18
|
+
export function planSql({ query, functions, tables }) {
|
|
18
19
|
const select = typeof query === 'string' ? parseSql({ query, functions }) : query
|
|
19
20
|
|
|
20
21
|
// Build CTE plans in order (each CTE can reference preceding CTEs)
|
|
@@ -22,12 +23,12 @@ export function planSql({ query, functions }) {
|
|
|
22
23
|
const ctePlans = new Map()
|
|
23
24
|
if (select.with) {
|
|
24
25
|
for (const cte of select.with.ctes) {
|
|
25
|
-
const ctePlan = planSelect({ select: cte.query, ctePlans })
|
|
26
|
+
const ctePlan = planSelect({ select: cte.query, ctePlans, tables })
|
|
26
27
|
ctePlans.set(cte.name.toLowerCase(), ctePlan)
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
return planSelect({ select, ctePlans })
|
|
31
|
+
return planSelect({ select, ctePlans, tables })
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -36,9 +37,10 @@ export function planSql({ query, functions }) {
|
|
|
36
37
|
* @param {object} options
|
|
37
38
|
* @param {SelectStatement} options.select
|
|
38
39
|
* @param {Map<string, QueryPlan>} options.ctePlans
|
|
40
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
39
41
|
* @returns {QueryPlan}
|
|
40
42
|
*/
|
|
41
|
-
function planSelect({ select, ctePlans }) {
|
|
43
|
+
function planSelect({ select, ctePlans, tables }) {
|
|
42
44
|
// Check for aggregation
|
|
43
45
|
const hasAggregate = select.columns.some(col =>
|
|
44
46
|
col.kind === 'derived' && findAggregate(col.expr)
|
|
@@ -67,11 +69,11 @@ function planSelect({ select, ctePlans }) {
|
|
|
67
69
|
|
|
68
70
|
// Start with the data source (FROM clause)
|
|
69
71
|
/** @type {QueryPlan} */
|
|
70
|
-
let plan = planFrom({ select, ctePlans, hints })
|
|
72
|
+
let plan = planFrom({ select, ctePlans, hints, tables })
|
|
71
73
|
|
|
72
74
|
// Add JOINs
|
|
73
75
|
if (select.joins.length) {
|
|
74
|
-
plan = planJoin({ left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, perTableColumns })
|
|
76
|
+
plan = planJoin({ left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, perTableColumns, tables })
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
// Whether FROM resolved to our own direct table scan
|
|
@@ -153,20 +155,22 @@ function planSelect({ select, ctePlans }) {
|
|
|
153
155
|
* @param {SelectStatement} options.select
|
|
154
156
|
* @param {Map<string, QueryPlan>} options.ctePlans
|
|
155
157
|
* @param {ScanOptions} options.hints
|
|
158
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
156
159
|
* @returns {QueryPlan}
|
|
157
160
|
*/
|
|
158
|
-
function planFrom({ select, ctePlans, hints }) {
|
|
161
|
+
function planFrom({ select, ctePlans, hints, tables }) {
|
|
159
162
|
if (select.from.kind === 'table') {
|
|
160
163
|
const ctePlan = ctePlans.get(select.from.table.toLowerCase())
|
|
161
164
|
if (ctePlan) {
|
|
162
165
|
return ctePlan
|
|
163
166
|
}
|
|
167
|
+
validateScan({ ...select.from, hints, tables })
|
|
164
168
|
return { type: 'Scan', table: select.from.table, hints }
|
|
165
169
|
} else {
|
|
166
170
|
if (select.from.query.with) {
|
|
167
171
|
throw new Error('WITH clause is not supported inside subqueries')
|
|
168
172
|
}
|
|
169
|
-
return planSelect({ select: select.from.query, ctePlans })
|
|
173
|
+
return planSelect({ select: select.from.query, ctePlans, tables })
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -177,9 +181,10 @@ function planFrom({ select, ctePlans, hints }) {
|
|
|
177
181
|
* @param {string} options.leftTable - name/alias of the left table
|
|
178
182
|
* @param {Map<string, QueryPlan>} options.ctePlans
|
|
179
183
|
* @param {Map<string, string[] | undefined>} options.perTableColumns
|
|
184
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
180
185
|
* @returns {QueryPlan}
|
|
181
186
|
*/
|
|
182
|
-
function planJoin({ left, joins, leftTable, ctePlans, perTableColumns }) {
|
|
187
|
+
function planJoin({ left, joins, leftTable, ctePlans, perTableColumns, tables }) {
|
|
183
188
|
let plan = left
|
|
184
189
|
let currentLeftTable = leftTable
|
|
185
190
|
|
|
@@ -191,6 +196,7 @@ function planJoin({ left, joins, leftTable, ctePlans, perTableColumns }) {
|
|
|
191
196
|
const rightHints = {}
|
|
192
197
|
if (!ctePlan) {
|
|
193
198
|
rightHints.columns = perTableColumns.get(rightTable)
|
|
199
|
+
validateScan({ ...join, hints: rightHints, tables })
|
|
194
200
|
}
|
|
195
201
|
/** @type {QueryPlan} */
|
|
196
202
|
const rightScan = ctePlan ?? { type: 'Scan', table: join.table, hints: rightHints }
|
|
@@ -313,6 +319,33 @@ function extractSimpleJoinKeys({ condition, leftTable, rightTable }) {
|
|
|
313
319
|
return { leftKey: left, rightKey: right }
|
|
314
320
|
}
|
|
315
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Validates that a table exists and requested columns are available.
|
|
324
|
+
*
|
|
325
|
+
* @param {object} options
|
|
326
|
+
* @param {string} options.table
|
|
327
|
+
* @param {ScanOptions} options.hints
|
|
328
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
329
|
+
* @param {number} options.positionStart
|
|
330
|
+
* @param {number} options.positionEnd
|
|
331
|
+
*/
|
|
332
|
+
function validateScan({ table, hints, tables, positionStart, positionEnd }) {
|
|
333
|
+
if (!tables) return
|
|
334
|
+
const resolved = tables[table]
|
|
335
|
+
if (!resolved) {
|
|
336
|
+
throw tableNotFoundError({ table, tables, positionStart, positionEnd })
|
|
337
|
+
}
|
|
338
|
+
const missingColumn = hints.columns?.find(col => !resolved.columns.includes(col))
|
|
339
|
+
if (missingColumn) {
|
|
340
|
+
throw columnNotFoundError({
|
|
341
|
+
columnName: missingColumn,
|
|
342
|
+
availableColumns: resolved.columns,
|
|
343
|
+
positionStart,
|
|
344
|
+
positionEnd,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
316
349
|
/**
|
|
317
350
|
* Checks if every SELECT column is a plain COUNT(*).
|
|
318
351
|
*
|
|
@@ -324,7 +357,7 @@ function isAllCountStar(columns) {
|
|
|
324
357
|
return columns.every(col =>
|
|
325
358
|
col.kind === 'derived' &&
|
|
326
359
|
col.expr.type === 'function' &&
|
|
327
|
-
col.expr.
|
|
360
|
+
col.expr.funcName.toUpperCase() === 'COUNT' &&
|
|
328
361
|
col.expr.args.length === 1 &&
|
|
329
362
|
col.expr.args[0].type === 'star' &&
|
|
330
363
|
!col.expr.distinct &&
|
package/src/spatial/bbox.js
CHANGED