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/types.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ExprNode, SelectStatement, SqlPrimitive } from './ast.js'
|
|
2
|
+
|
|
3
|
+
export * from './ast.js'
|
|
1
4
|
export { ParserState, Token, TokenType } from './parse/types.js'
|
|
2
5
|
export { QueryPlan } from './plan/types.js'
|
|
3
6
|
|
|
@@ -19,6 +22,7 @@ export interface ExecuteSqlOptions {
|
|
|
19
22
|
export interface PlanSqlOptions {
|
|
20
23
|
query: string | SelectStatement
|
|
21
24
|
functions?: Record<string, UserDefinedFunction>
|
|
25
|
+
tables?: Record<string, AsyncDataSource>
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
// executePlan(plan, context)
|
|
@@ -73,170 +77,21 @@ export interface ScanOptions {
|
|
|
73
77
|
signal?: AbortSignal
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
| Date
|
|
82
|
-
| null
|
|
83
|
-
| SqlPrimitive[]
|
|
84
|
-
| Record<string, any>
|
|
80
|
+
export interface FunctionSignature {
|
|
81
|
+
min: number
|
|
82
|
+
max?: number
|
|
83
|
+
signature?: string
|
|
84
|
+
}
|
|
85
85
|
|
|
86
86
|
export interface UserDefinedFunction {
|
|
87
87
|
apply: (...args: SqlPrimitive[]) => SqlPrimitive | Promise<SqlPrimitive>
|
|
88
|
-
arguments:
|
|
89
|
-
min: number
|
|
90
|
-
max?: number
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface CTEDefinition {
|
|
95
|
-
name: string
|
|
96
|
-
query: SelectStatement
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface WithClause {
|
|
100
|
-
ctes: CTEDefinition[]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface SelectStatement {
|
|
104
|
-
with?: WithClause
|
|
105
|
-
distinct: boolean
|
|
106
|
-
columns: SelectColumn[]
|
|
107
|
-
from: FromTable | FromSubquery
|
|
108
|
-
joins: JoinClause[]
|
|
109
|
-
where?: ExprNode
|
|
110
|
-
groupBy: ExprNode[]
|
|
111
|
-
having?: ExprNode
|
|
112
|
-
orderBy: OrderByItem[]
|
|
113
|
-
limit?: number
|
|
114
|
-
offset?: number
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface FromTable {
|
|
118
|
-
kind: 'table'
|
|
119
|
-
table: string
|
|
120
|
-
alias?: string
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface FromSubquery {
|
|
124
|
-
kind: 'subquery'
|
|
125
|
-
query: SelectStatement
|
|
126
|
-
alias: string
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
|
|
130
|
-
|
|
131
|
-
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
132
|
-
|
|
133
|
-
export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
134
|
-
|
|
135
|
-
export interface ExprNodeBase {
|
|
136
|
-
positionStart: number
|
|
137
|
-
positionEnd: number
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export interface LiteralNode extends ExprNodeBase {
|
|
141
|
-
type: 'literal'
|
|
142
|
-
value: SqlPrimitive
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export interface IdentifierNode extends ExprNodeBase {
|
|
146
|
-
type: 'identifier'
|
|
147
|
-
name: string
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export interface UnaryNode extends ExprNodeBase {
|
|
151
|
-
type: 'unary'
|
|
152
|
-
op: 'NOT' | 'IS NULL' | 'IS NOT NULL' | '-'
|
|
153
|
-
argument: ExprNode
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface BinaryNode extends ExprNodeBase {
|
|
157
|
-
type: 'binary'
|
|
158
|
-
op: BinaryOp
|
|
159
|
-
left: ExprNode
|
|
160
|
-
right: ExprNode
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface FunctionNode extends ExprNodeBase {
|
|
164
|
-
type: 'function'
|
|
165
|
-
name: string
|
|
166
|
-
args: ExprNode[]
|
|
167
|
-
distinct?: boolean
|
|
168
|
-
filter?: ExprNode
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export interface CastNode extends ExprNodeBase {
|
|
172
|
-
type: 'cast'
|
|
173
|
-
expr: ExprNode
|
|
174
|
-
toType: string
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export interface InSubqueryNode extends ExprNodeBase {
|
|
178
|
-
type: 'in'
|
|
179
|
-
expr: ExprNode
|
|
180
|
-
subquery: SelectStatement
|
|
88
|
+
arguments: FunctionSignature
|
|
181
89
|
}
|
|
182
90
|
|
|
183
|
-
export interface InValuesNode extends ExprNodeBase {
|
|
184
|
-
type: 'in valuelist'
|
|
185
|
-
expr: ExprNode
|
|
186
|
-
values: ExprNode[]
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export interface ExistsNode extends ExprNodeBase {
|
|
190
|
-
type: 'exists' | 'not exists'
|
|
191
|
-
subquery: SelectStatement
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export interface WhenClause {
|
|
195
|
-
condition: ExprNode
|
|
196
|
-
result: ExprNode
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface CaseNode extends ExprNodeBase {
|
|
200
|
-
type: 'case'
|
|
201
|
-
caseExpr?: ExprNode
|
|
202
|
-
whenClauses: WhenClause[]
|
|
203
|
-
elseResult?: ExprNode
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface SubqueryNode extends ExprNodeBase {
|
|
207
|
-
type: 'subquery'
|
|
208
|
-
subquery: SelectStatement
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
|
|
212
|
-
|
|
213
|
-
export interface IntervalNode extends ExprNodeBase {
|
|
214
|
-
type: 'interval'
|
|
215
|
-
value: number
|
|
216
|
-
unit: IntervalUnit
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export interface StarNode extends ExprNodeBase {
|
|
220
|
-
type: 'star'
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export type ExprNode =
|
|
224
|
-
| LiteralNode
|
|
225
|
-
| IdentifierNode
|
|
226
|
-
| UnaryNode
|
|
227
|
-
| BinaryNode
|
|
228
|
-
| FunctionNode
|
|
229
|
-
| CastNode
|
|
230
|
-
| InSubqueryNode
|
|
231
|
-
| InValuesNode
|
|
232
|
-
| ExistsNode
|
|
233
|
-
| CaseNode
|
|
234
|
-
| SubqueryNode
|
|
235
|
-
| IntervalNode
|
|
236
|
-
| StarNode
|
|
237
|
-
|
|
238
91
|
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP'
|
|
239
92
|
|
|
93
|
+
export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_REPLACE'
|
|
94
|
+
|
|
240
95
|
export type MathFunc =
|
|
241
96
|
| 'FLOOR'
|
|
242
97
|
| 'CEIL'
|
|
@@ -292,37 +147,3 @@ export type SpatialFunc =
|
|
|
292
147
|
| 'ST_GEOMFROMTEXT'
|
|
293
148
|
| 'ST_MAKEENVELOPE'
|
|
294
149
|
| 'ST_ASTEXT'
|
|
295
|
-
|
|
296
|
-
export interface StarColumn {
|
|
297
|
-
kind: 'star'
|
|
298
|
-
table?: string
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export interface DerivedColumn {
|
|
302
|
-
kind: 'derived'
|
|
303
|
-
expr: ExprNode
|
|
304
|
-
alias?: string
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export type SelectColumn = StarColumn | DerivedColumn
|
|
308
|
-
|
|
309
|
-
export interface OrderByItem {
|
|
310
|
-
expr: ExprNode
|
|
311
|
-
direction: 'ASC' | 'DESC'
|
|
312
|
-
nulls?: 'FIRST' | 'LAST'
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS' | 'POSITIONAL'
|
|
316
|
-
|
|
317
|
-
export interface JoinClause {
|
|
318
|
-
joinType: JoinType
|
|
319
|
-
table: string
|
|
320
|
-
alias?: string
|
|
321
|
-
on?: ExprNode
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export interface ExecuteContext {
|
|
325
|
-
tables: Record<string, AsyncDataSource>
|
|
326
|
-
functions?: Record<string, UserDefinedFunction>
|
|
327
|
-
signal?: AbortSignal
|
|
328
|
-
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isAggregateFunc } from './functions.js'
|
|
2
|
+
import { ParseError } from './parseErrors.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @import { ExprNode, FunctionNode } from '../types.js'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Finds the first aggregate function call in an expression tree.
|
|
10
|
+
* Does not recurse into subqueries (they have their own aggregate scope).
|
|
11
|
+
*
|
|
12
|
+
* @param {ExprNode | undefined} expr
|
|
13
|
+
* @returns {FunctionNode | undefined}
|
|
14
|
+
*/
|
|
15
|
+
export function findAggregate(expr) {
|
|
16
|
+
if (!expr) return undefined
|
|
17
|
+
if (expr.type === 'function' && isAggregateFunc(expr.funcName.toUpperCase())) {
|
|
18
|
+
return expr
|
|
19
|
+
}
|
|
20
|
+
if (expr.type === 'binary') {
|
|
21
|
+
return findAggregate(expr.left) || findAggregate(expr.right)
|
|
22
|
+
}
|
|
23
|
+
if (expr.type === 'unary') {
|
|
24
|
+
return findAggregate(expr.argument)
|
|
25
|
+
}
|
|
26
|
+
if (expr.type === 'cast') {
|
|
27
|
+
return findAggregate(expr.expr)
|
|
28
|
+
}
|
|
29
|
+
if (expr.type === 'case') {
|
|
30
|
+
if (expr.caseExpr) {
|
|
31
|
+
const found = findAggregate(expr.caseExpr)
|
|
32
|
+
if (found) return found
|
|
33
|
+
}
|
|
34
|
+
for (const when of expr.whenClauses) {
|
|
35
|
+
const found = findAggregate(when.condition) || findAggregate(when.result)
|
|
36
|
+
if (found) return found
|
|
37
|
+
}
|
|
38
|
+
return findAggregate(expr.elseResult)
|
|
39
|
+
}
|
|
40
|
+
if (expr.type === 'in valuelist') {
|
|
41
|
+
const found = findAggregate(expr.expr)
|
|
42
|
+
if (found) return found
|
|
43
|
+
for (const val of expr.values) {
|
|
44
|
+
const found = findAggregate(val)
|
|
45
|
+
if (found) return found
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Subqueries have their own aggregate scope
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Throws a ParseError if the expression contains an aggregate function.
|
|
54
|
+
*
|
|
55
|
+
* @param {ExprNode | undefined} expr - The expression to check
|
|
56
|
+
* @param {string} clause - The clause name (e.g., 'WHERE', 'JOIN ON', 'GROUP BY')
|
|
57
|
+
*/
|
|
58
|
+
export function expectNoAggregate(expr, clause) {
|
|
59
|
+
const agg = findAggregate(expr)
|
|
60
|
+
if (agg) {
|
|
61
|
+
const hint = clause === 'WHERE' ? '. Use HAVING instead.' : ''
|
|
62
|
+
throw new ParseError({
|
|
63
|
+
...agg,
|
|
64
|
+
message: `Aggregate function ${agg.funcName} is not allowed in ${clause} clause${hint}`,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured execution error with position range and optional row number.
|
|
3
|
+
*/
|
|
4
|
+
export class ExecutionError extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* @param {Object} options
|
|
7
|
+
* @param {string} options.message - Human-readable error message
|
|
8
|
+
* @param {number} options.positionStart
|
|
9
|
+
* @param {number} options.positionEnd
|
|
10
|
+
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
11
|
+
*/
|
|
12
|
+
constructor({ message, positionStart, positionEnd, rowIndex }) {
|
|
13
|
+
const rowSuffix = rowIndex != null ? ` (row ${rowIndex})` : ''
|
|
14
|
+
super(message + rowSuffix)
|
|
15
|
+
this.name = 'ExecutionError'
|
|
16
|
+
this.positionStart = positionStart
|
|
17
|
+
this.positionEnd = positionEnd
|
|
18
|
+
this.rowIndex = rowIndex
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Error for invalid context (e.g., INTERVAL without date arithmetic).
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {string} options.item - What was used incorrectly
|
|
27
|
+
* @param {string} options.validContext - Where it can be used
|
|
28
|
+
* @param {number} options.positionStart
|
|
29
|
+
* @param {number} options.positionEnd
|
|
30
|
+
* @param {number} options.rowIndex - 1-based row number where error occurred
|
|
31
|
+
* @returns {ExecutionError}
|
|
32
|
+
*/
|
|
33
|
+
export function invalidContextError({ item, validContext, positionStart, positionEnd, rowIndex }) {
|
|
34
|
+
return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowIndex })
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ExecutionError } from './executionErrors.js'
|
|
2
|
+
import { FUNCTION_SIGNATURES } from './functions.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error for invalid argument type or value.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} options
|
|
8
|
+
* @param {string} options.funcName - The function name
|
|
9
|
+
* @param {string} options.message - Specific error message
|
|
10
|
+
* @param {number} options.positionStart
|
|
11
|
+
* @param {number} options.positionEnd
|
|
12
|
+
* @param {string} [options.hint] - Recovery hint
|
|
13
|
+
* @param {number} options.rowIndex - 1-based row number where error occurred
|
|
14
|
+
* @returns {ExecutionError}
|
|
15
|
+
*/
|
|
16
|
+
export function argValueError({ funcName, message, positionStart, positionEnd, hint, rowIndex }) {
|
|
17
|
+
const funcNameUpper = funcName.toUpperCase()
|
|
18
|
+
const signature = FUNCTION_SIGNATURES[funcNameUpper]?.signature ?? ''
|
|
19
|
+
const suffix = hint ? `. ${hint}` : ''
|
|
20
|
+
return new ExecutionError({ message: `${funcName}(${signature}): ${message}${suffix}`, positionStart, positionEnd, rowIndex })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Error for aggregate function misuse.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} options
|
|
27
|
+
* @param {string} options.funcName - The aggregate function
|
|
28
|
+
* @param {number} options.positionStart
|
|
29
|
+
* @param {number} options.positionEnd
|
|
30
|
+
* @returns {ExecutionError}
|
|
31
|
+
*/
|
|
32
|
+
export function aggregateError({ funcName, positionStart, positionEnd }) {
|
|
33
|
+
return new ExecutionError({
|
|
34
|
+
message: `Aggregate function ${funcName} is not available in this context`,
|
|
35
|
+
positionStart,
|
|
36
|
+
positionEnd,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Error for unsupported CAST type.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} options
|
|
44
|
+
* @param {string} options.toType - The unsupported target type
|
|
45
|
+
* @param {number} options.positionStart
|
|
46
|
+
* @param {number} options.positionEnd
|
|
47
|
+
* @param {string} [options.fromType] - The source type (undefined means unsupported target type)
|
|
48
|
+
* @param {number} options.rowIndex - 1-based row number where error occurred
|
|
49
|
+
* @returns {ExecutionError}
|
|
50
|
+
*/
|
|
51
|
+
export function castError({ toType, positionStart, positionEnd, fromType, rowIndex }) {
|
|
52
|
+
const message = fromType
|
|
53
|
+
? `Cannot CAST ${fromType} to ${toType}`
|
|
54
|
+
: `Unsupported CAST to type ${toType}`
|
|
55
|
+
|
|
56
|
+
return new ExecutionError({ message: `${message}. Supported types: STRING, INT, BIGINT, FLOAT, BOOL`, positionStart, positionEnd, rowIndex })
|
|
57
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { AggregateFunc, BinaryOp, CastType, FunctionSignature, IntervalUnit, MathFunc, RegExpFunction, SpatialFunc, StringFunc, UserDefinedFunction } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
import { ParseError } from '../validation/parseErrors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} name
|
|
8
|
+
* @returns {name is AggregateFunc}
|
|
9
|
+
*/
|
|
10
|
+
export function isAggregateFunc(name) {
|
|
11
|
+
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP'].includes(name)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} name
|
|
16
|
+
* @returns {name is MathFunc}
|
|
17
|
+
*/
|
|
18
|
+
export function isMathFunc(name) {
|
|
19
|
+
return [
|
|
20
|
+
'FLOOR', 'CEIL', 'CEILING', 'ROUND', 'ABS', 'SIGN', 'MOD', 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
|
|
21
|
+
'SIN', 'COS', 'TAN', 'COT', 'ASIN', 'ACOS', 'ATAN', 'ATAN2', 'DEGREES', 'RADIANS', 'PI',
|
|
22
|
+
'RAND', 'RANDOM',
|
|
23
|
+
].includes(name)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} name
|
|
28
|
+
* @returns {name is RegExpFunction}
|
|
29
|
+
*/
|
|
30
|
+
export function isRegexpFunc(name) {
|
|
31
|
+
return ['REGEXP_SUBSTR', 'REGEXP_REPLACE'].includes(name)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} name
|
|
36
|
+
* @returns {name is SpatialFunc}
|
|
37
|
+
*/
|
|
38
|
+
export function isSpatialFunc(name) {
|
|
39
|
+
return [
|
|
40
|
+
'ST_INTERSECTS', 'ST_CONTAINS', 'ST_CONTAINSPROPERLY', 'ST_WITHIN',
|
|
41
|
+
'ST_OVERLAPS', 'ST_TOUCHES', 'ST_EQUALS', 'ST_CROSSES',
|
|
42
|
+
'ST_COVERS', 'ST_COVEREDBY', 'ST_DWITHIN',
|
|
43
|
+
'ST_GEOMFROMTEXT', 'ST_MAKEENVELOPE', 'ST_ASTEXT',
|
|
44
|
+
].includes(name)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} name
|
|
49
|
+
* @returns {name is IntervalUnit}
|
|
50
|
+
*/
|
|
51
|
+
export function isIntervalUnit(name) {
|
|
52
|
+
return ['DAY', 'MONTH', 'YEAR', 'HOUR', 'MINUTE', 'SECOND'].includes(name)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} name
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function isExtractField(name) {
|
|
60
|
+
return ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'DOW', 'EPOCH'].includes(name)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} name
|
|
65
|
+
* @returns {name is CastType}
|
|
66
|
+
*/
|
|
67
|
+
export function isCastType(name) {
|
|
68
|
+
return ['TEXT', 'STRING', 'VARCHAR', 'INTEGER', 'INT', 'BIGINT', 'FLOAT', 'REAL', 'DOUBLE', 'BOOLEAN', 'BOOL'].includes(name)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} name
|
|
73
|
+
* @returns {name is StringFunc}
|
|
74
|
+
*/
|
|
75
|
+
export function isStringFunc(name) {
|
|
76
|
+
return [
|
|
77
|
+
'UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM',
|
|
78
|
+
'REPLACE', 'LEFT', 'RIGHT', 'INSTR',
|
|
79
|
+
].includes(name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} op
|
|
84
|
+
* @returns {op is BinaryOp}
|
|
85
|
+
*/
|
|
86
|
+
export function isBinaryOp(op) {
|
|
87
|
+
return ['AND', 'OR', 'LIKE', '=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Function signatures: argument counts and human-readable parameter signatures.
|
|
92
|
+
* @type {Record<string, FunctionSignature>}
|
|
93
|
+
*/
|
|
94
|
+
export const FUNCTION_SIGNATURES = {
|
|
95
|
+
// String functions
|
|
96
|
+
UPPER: { min: 1, max: 1, signature: 'string' },
|
|
97
|
+
LOWER: { min: 1, max: 1, signature: 'string' },
|
|
98
|
+
LENGTH: { min: 1, max: 1, signature: 'string' },
|
|
99
|
+
TRIM: { min: 1, max: 1, signature: 'string' },
|
|
100
|
+
REPLACE: { min: 3, max: 3, signature: 'string, search, replacement' },
|
|
101
|
+
SUBSTRING: { min: 2, max: 3, signature: 'string, start[, length]' },
|
|
102
|
+
SUBSTR: { min: 2, max: 3, signature: 'string, start[, length]' },
|
|
103
|
+
CONCAT: { min: 1, signature: 'value1, value2[, ...]' },
|
|
104
|
+
LEFT: { min: 2, max: 2, signature: 'string, length' },
|
|
105
|
+
RIGHT: { min: 2, max: 2, signature: 'string, length' },
|
|
106
|
+
INSTR: { min: 2, max: 2, signature: 'string, substring' },
|
|
107
|
+
REGEXP_SUBSTR: { min: 2, max: 4, signature: 'string, pattern[, position[, flags]]' },
|
|
108
|
+
REGEXP_REPLACE: { min: 3, max: 5, signature: 'string, pattern, replacement[, position[, flags]]' },
|
|
109
|
+
|
|
110
|
+
// Date/time functions
|
|
111
|
+
RANDOM: { min: 0, max: 0, signature: '' },
|
|
112
|
+
RAND: { min: 0, max: 0, signature: '' },
|
|
113
|
+
CURRENT_DATE: { min: 0, max: 0, signature: '' },
|
|
114
|
+
CURRENT_TIME: { min: 0, max: 0, signature: '' },
|
|
115
|
+
CURRENT_TIMESTAMP: { min: 0, max: 0, signature: '' },
|
|
116
|
+
DATE_TRUNC: { min: 2, max: 2, signature: 'unit, date' },
|
|
117
|
+
DATE_PART: { min: 2, max: 2, signature: 'field, date' },
|
|
118
|
+
EXTRACT: { min: 2, max: 2, signature: 'field FROM date' },
|
|
119
|
+
|
|
120
|
+
// Math functions
|
|
121
|
+
FLOOR: { min: 1, max: 1, signature: 'number' },
|
|
122
|
+
CEIL: { min: 1, max: 1, signature: 'number' },
|
|
123
|
+
CEILING: { min: 1, max: 1, signature: 'number' },
|
|
124
|
+
ROUND: { min: 1, max: 2, signature: 'number[, decimals]' },
|
|
125
|
+
ABS: { min: 1, max: 1, signature: 'number' },
|
|
126
|
+
SIGN: { min: 1, max: 1, signature: 'number' },
|
|
127
|
+
MOD: { min: 2, max: 2, signature: 'dividend, divisor' },
|
|
128
|
+
EXP: { min: 1, max: 1, signature: 'number' },
|
|
129
|
+
LN: { min: 1, max: 1, signature: 'number' },
|
|
130
|
+
LOG10: { min: 1, max: 1, signature: 'number' },
|
|
131
|
+
POWER: { min: 2, max: 2, signature: 'base, exponent' },
|
|
132
|
+
SQRT: { min: 1, max: 1, signature: 'number' },
|
|
133
|
+
SIN: { min: 1, max: 1, signature: 'radians' },
|
|
134
|
+
COS: { min: 1, max: 1, signature: 'radians' },
|
|
135
|
+
TAN: { min: 1, max: 1, signature: 'radians' },
|
|
136
|
+
COT: { min: 1, max: 1, signature: 'radians' },
|
|
137
|
+
ASIN: { min: 1, max: 1, signature: 'number' },
|
|
138
|
+
ACOS: { min: 1, max: 1, signature: 'number' },
|
|
139
|
+
ATAN: { min: 1, max: 2, signature: 'number' },
|
|
140
|
+
ATAN2: { min: 2, max: 2, signature: 'y, x' },
|
|
141
|
+
DEGREES: { min: 1, max: 1, signature: 'radians' },
|
|
142
|
+
RADIANS: { min: 1, max: 1, signature: 'degrees' },
|
|
143
|
+
PI: { min: 0, max: 0, signature: '' },
|
|
144
|
+
|
|
145
|
+
// JSON functions
|
|
146
|
+
JSON_VALUE: { min: 2, max: 2, signature: 'expression, path' },
|
|
147
|
+
JSON_QUERY: { min: 2, max: 2, signature: 'expression, path' },
|
|
148
|
+
JSON_OBJECT: { min: 0, signature: 'key1, value1[, ...]' },
|
|
149
|
+
JSON_ARRAYAGG: { min: 1, max: 1, signature: 'expression' },
|
|
150
|
+
|
|
151
|
+
// Array functions
|
|
152
|
+
ARRAY_LENGTH: { min: 1, max: 1, signature: 'array' },
|
|
153
|
+
ARRAY_POSITION: { min: 2, max: 2, signature: 'array, element' },
|
|
154
|
+
ARRAY_SORT: { min: 1, max: 1, signature: 'array' },
|
|
155
|
+
CARDINALITY: { min: 1, max: 1, signature: 'array' },
|
|
156
|
+
|
|
157
|
+
// Conditional functions
|
|
158
|
+
COALESCE: { min: 1, signature: 'value1, value2[, ...]' },
|
|
159
|
+
NULLIF: { min: 2, max: 2, signature: 'value1, value2' },
|
|
160
|
+
|
|
161
|
+
// Aggregate functions
|
|
162
|
+
COUNT: { min: 1, max: 1, signature: 'expression' },
|
|
163
|
+
SUM: { min: 1, max: 1, signature: 'expression' },
|
|
164
|
+
AVG: { min: 1, max: 1, signature: 'expression' },
|
|
165
|
+
MIN: { min: 1, max: 1, signature: 'expression' },
|
|
166
|
+
MAX: { min: 1, max: 1, signature: 'expression' },
|
|
167
|
+
STDDEV_SAMP: { min: 1, max: 1, signature: 'expression' },
|
|
168
|
+
STDDEV_POP: { min: 1, max: 1, signature: 'expression' },
|
|
169
|
+
|
|
170
|
+
// Spatial functions
|
|
171
|
+
ST_INTERSECTS: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
172
|
+
ST_CONTAINS: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
173
|
+
ST_CONTAINSPROPERLY: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
174
|
+
ST_WITHIN: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
175
|
+
ST_OVERLAPS: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
176
|
+
ST_TOUCHES: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
177
|
+
ST_EQUALS: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
178
|
+
ST_CROSSES: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
179
|
+
ST_COVERS: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
180
|
+
ST_COVEREDBY: { min: 2, max: 2, signature: 'geometry, geometry' },
|
|
181
|
+
ST_DWITHIN: { min: 3, max: 3, signature: 'geometry, geometry, distance' },
|
|
182
|
+
ST_GEOMFROMTEXT: { min: 1, max: 1, signature: 'wkt' },
|
|
183
|
+
ST_MAKEENVELOPE: { min: 4, max: 4, signature: 'xmin, ymin, xmax, ymax' },
|
|
184
|
+
ST_ASTEXT: { min: 1, max: 1, signature: 'geometry' },
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format expected argument count for error messages.
|
|
189
|
+
* @param {number} min
|
|
190
|
+
* @param {number | undefined} max
|
|
191
|
+
* @returns {string | number}
|
|
192
|
+
*/
|
|
193
|
+
function formatExpected(min, max) {
|
|
194
|
+
if (max == null) return `at least ${min}`
|
|
195
|
+
if (min === max) return min
|
|
196
|
+
return `${min} or ${max}`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validates function argument count, throwing a ParseError if invalid.
|
|
201
|
+
* @param {string} funcName - The function name (uppercase)
|
|
202
|
+
* @param {number} argCount - Number of arguments provided
|
|
203
|
+
* @param {number} positionStart - Start position in query
|
|
204
|
+
* @param {number} positionEnd - End position in query
|
|
205
|
+
* @param {Record<string, UserDefinedFunction>} [functions] - User-defined functions
|
|
206
|
+
* @throws {ParseError}
|
|
207
|
+
*/
|
|
208
|
+
export function validateFunctionArgCount(funcName, argCount, positionStart, positionEnd, functions) {
|
|
209
|
+
// Check built-in functions
|
|
210
|
+
let spec = FUNCTION_SIGNATURES[funcName]
|
|
211
|
+
|
|
212
|
+
// Check user-defined functions (case-insensitive)
|
|
213
|
+
if (!spec && functions) {
|
|
214
|
+
const udfName = Object.keys(functions).find(k => k.toUpperCase() === funcName)
|
|
215
|
+
if (udfName) {
|
|
216
|
+
spec = functions[udfName].arguments
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!spec) return
|
|
221
|
+
|
|
222
|
+
const { min, max } = spec
|
|
223
|
+
|
|
224
|
+
if (argCount < min || max != null && argCount > max) {
|
|
225
|
+
const expected = formatExpected(min, max)
|
|
226
|
+
const signature = FUNCTION_SIGNATURES[funcName]?.signature ?? ''
|
|
227
|
+
let expectedStr = `${expected} arguments`
|
|
228
|
+
if (expected === 0) expectedStr = 'no arguments'
|
|
229
|
+
if (expected === 1) expectedStr = '1 argument'
|
|
230
|
+
if (typeof expected === 'string' && expected.endsWith(' 1')) {
|
|
231
|
+
expectedStr = `${expected} argument`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
throw new ParseError({
|
|
235
|
+
message: `${funcName}(${signature}) function requires ${expectedStr}, got ${argCount}`,
|
|
236
|
+
positionStart,
|
|
237
|
+
positionEnd,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Checks if a function is known (either built-in or user-defined).
|
|
244
|
+
* @param {string} funcName - The function name (uppercase)
|
|
245
|
+
* @param {Record<string, UserDefinedFunction>} [functions] - User-defined functions
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
export function isKnownFunction(funcName, functions) {
|
|
249
|
+
// Check built-in functions
|
|
250
|
+
if (FUNCTION_SIGNATURES[funcName]) return true
|
|
251
|
+
|
|
252
|
+
// Check user-defined functions (case-insensitive)
|
|
253
|
+
if (functions) {
|
|
254
|
+
return Object.keys(functions).some(k => k.toUpperCase() === funcName)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Reserved keywords that cannot be used as identifiers in expressions.
|
|
261
|
+
// Non-reserved keywords (e.g. DAY, MONTH, FILTER, ASC) can be used as column alias references.
|
|
262
|
+
export const RESERVED_KEYWORDS = new Set([
|
|
263
|
+
'SELECT', 'FROM', 'WHERE', 'WITH',
|
|
264
|
+
'AND', 'OR', 'NOT', 'IS', 'LIKE', 'IN', 'BETWEEN',
|
|
265
|
+
'TRUE', 'FALSE', 'NULL',
|
|
266
|
+
'EXISTS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'INTERVAL',
|
|
267
|
+
'GROUP', 'BY', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
|
268
|
+
'AS', 'ALL', 'DISTINCT',
|
|
269
|
+
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'ON',
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
// Keywords that cannot be used as implicit aliases after a column
|
|
273
|
+
export const RESERVED_AFTER_COLUMN = new Set([
|
|
274
|
+
'FROM', 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
|
275
|
+
])
|
|
276
|
+
|
|
277
|
+
// Keywords that cannot be used as table aliases
|
|
278
|
+
export const RESERVED_AFTER_TABLE = new Set([
|
|
279
|
+
'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET', 'JOIN', 'INNER',
|
|
280
|
+
'LEFT', 'RIGHT', 'FULL', 'CROSS', 'ON', 'POSITIONAL',
|
|
281
|
+
])
|