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
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// PARSE ERRORS - Issues during SQL tokenization and parsing
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
|
-
import { FUNCTION_SIGNATURES } from './validationErrors.js'
|
|
6
|
-
|
|
7
1
|
/**
|
|
8
2
|
* Structured parse error with position range.
|
|
9
3
|
*/
|
|
@@ -11,8 +5,8 @@ export class ParseError extends Error {
|
|
|
11
5
|
/**
|
|
12
6
|
* @param {Object} options
|
|
13
7
|
* @param {string} options.message - Human-readable error message
|
|
14
|
-
* @param {number} options.positionStart
|
|
15
|
-
* @param {number} options.positionEnd
|
|
8
|
+
* @param {number} options.positionStart
|
|
9
|
+
* @param {number} options.positionEnd
|
|
16
10
|
*/
|
|
17
11
|
constructor({ message, positionStart, positionEnd }) {
|
|
18
12
|
super(message)
|
|
@@ -28,8 +22,8 @@ export class ParseError extends Error {
|
|
|
28
22
|
* @param {Object} options
|
|
29
23
|
* @param {string} options.expected - Description of what was expected
|
|
30
24
|
* @param {string} options.received - What was actually found
|
|
31
|
-
* @param {number} options.positionStart
|
|
32
|
-
* @param {number} options.positionEnd
|
|
25
|
+
* @param {number} options.positionStart
|
|
26
|
+
* @param {number} options.positionEnd
|
|
33
27
|
* @param {string} [options.after] - What token came before (for context)
|
|
34
28
|
* @returns {ParseError}
|
|
35
29
|
*/
|
|
@@ -43,8 +37,8 @@ export function syntaxError({ expected, received, positionStart, positionEnd, af
|
|
|
43
37
|
*
|
|
44
38
|
* @param {Object} options
|
|
45
39
|
* @param {'string' | 'identifier'} options.type - Type of unterminated literal
|
|
46
|
-
* @param {number} options.positionStart
|
|
47
|
-
* @param {number} options.positionEnd
|
|
40
|
+
* @param {number} options.positionStart
|
|
41
|
+
* @param {number} options.positionEnd
|
|
48
42
|
* @returns {ParseError}
|
|
49
43
|
*/
|
|
50
44
|
export function unterminatedError({ type, positionStart, positionEnd }) {
|
|
@@ -58,8 +52,8 @@ export function unterminatedError({ type, positionStart, positionEnd }) {
|
|
|
58
52
|
* @param {Object} options
|
|
59
53
|
* @param {string} options.type - Type of invalid literal (e.g., 'number', 'interval value', 'interval unit')
|
|
60
54
|
* @param {string} options.value - The invalid value
|
|
61
|
-
* @param {number} options.positionStart
|
|
62
|
-
* @param {number} options.positionEnd
|
|
55
|
+
* @param {number} options.positionStart
|
|
56
|
+
* @param {number} options.positionEnd
|
|
63
57
|
* @param {string} [options.validValues] - List of valid values (for enums like interval units)
|
|
64
58
|
* @returns {ParseError}
|
|
65
59
|
*/
|
|
@@ -73,11 +67,11 @@ export function invalidLiteralError({ type, value, positionStart, positionEnd, v
|
|
|
73
67
|
*
|
|
74
68
|
* @param {Object} options
|
|
75
69
|
* @param {string} options.char - The unexpected character
|
|
76
|
-
* @param {number} options.positionStart
|
|
77
|
-
* @param {boolean}
|
|
70
|
+
* @param {number} options.positionStart
|
|
71
|
+
* @param {boolean} options.expectsSelect - Whether SELECT was expected (first token)
|
|
78
72
|
* @returns {ParseError}
|
|
79
73
|
*/
|
|
80
|
-
export function unexpectedCharError({ char, positionStart, expectsSelect
|
|
74
|
+
export function unexpectedCharError({ char, positionStart, expectsSelect }) {
|
|
81
75
|
const positionEnd = positionStart + 1
|
|
82
76
|
if (expectsSelect) {
|
|
83
77
|
return new ParseError({ message: `Expected SELECT but found "${char}" at position ${positionStart}. Queries must start with SELECT or WITH.`, positionStart, positionEnd })
|
|
@@ -90,44 +84,14 @@ export function unexpectedCharError({ char, positionStart, expectsSelect = false
|
|
|
90
84
|
*
|
|
91
85
|
* @param {Object} options
|
|
92
86
|
* @param {string} options.funcName - The unknown function name
|
|
93
|
-
* @param {number} options.positionStart
|
|
94
|
-
* @param {number} options.positionEnd
|
|
95
|
-
* @param {string} [options.validFunctions] - List of valid functions
|
|
96
|
-
* @returns {ParseError}
|
|
97
|
-
*/
|
|
98
|
-
export function unknownFunctionError({ funcName, positionStart, positionEnd, validFunctions }) {
|
|
99
|
-
const supported = validFunctions ||
|
|
100
|
-
'COUNT, SUM, AVG, MIN, MAX, UPPER, LOWER, CONCAT, LENGTH, SUBSTRING, TRIM, REPLACE, FLOOR, CEIL, ABS, MOD, EXP, LN, LOG10, POWER, SQRT, JSON_OBJECT, JSON_VALUE, JSON_QUERY, JSON_ARRAYAGG'
|
|
101
|
-
|
|
102
|
-
return new ParseError({
|
|
103
|
-
message: `Unknown function "${funcName}" at position ${positionStart}. Supported: ${supported}`,
|
|
104
|
-
positionStart,
|
|
105
|
-
positionEnd,
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Error for wrong number of function arguments at parse time.
|
|
111
|
-
*
|
|
112
|
-
* @param {Object} options
|
|
113
|
-
* @param {string} options.funcName - The function name
|
|
114
|
-
* @param {number | string} options.expected - Expected count (number or range like "2 to 3")
|
|
115
|
-
* @param {number} options.received - Actual argument count
|
|
116
|
-
* @param {number} options.positionStart - Start position in query
|
|
117
|
-
* @param {number} options.positionEnd - End position in query
|
|
87
|
+
* @param {number} options.positionStart
|
|
88
|
+
* @param {number} options.positionEnd
|
|
118
89
|
* @returns {ParseError}
|
|
119
90
|
*/
|
|
120
|
-
export function
|
|
121
|
-
|
|
122
|
-
let expectedStr = `${expected} arguments`
|
|
123
|
-
if (expected === 0) expectedStr = 'no arguments'
|
|
124
|
-
if (expected === 1) expectedStr = '1 argument'
|
|
125
|
-
if (typeof expected === 'string' && expected.endsWith(' 1')) {
|
|
126
|
-
expectedStr = `${expected} argument`
|
|
127
|
-
}
|
|
128
|
-
|
|
91
|
+
export function unknownFunctionError({ funcName, positionStart, positionEnd }) {
|
|
92
|
+
// TODO: suggest similar function names based on edit distance
|
|
129
93
|
return new ParseError({
|
|
130
|
-
message:
|
|
94
|
+
message: `Unknown function "${funcName}" at position ${positionStart}.`,
|
|
131
95
|
positionStart,
|
|
132
96
|
positionEnd,
|
|
133
97
|
})
|
|
@@ -139,12 +103,12 @@ export function argCountParseError({ funcName, expected, received, positionStart
|
|
|
139
103
|
* @param {Object} options
|
|
140
104
|
* @param {string} options.missing - What is missing (e.g., 'WHEN clause', 'FROM clause', 'ON condition')
|
|
141
105
|
* @param {string} options.context - Where it's missing from (e.g., 'CASE expression', 'SELECT statement', 'JOIN')
|
|
142
|
-
* @param {number} options.positionStart
|
|
143
|
-
* @param {number} options.positionEnd
|
|
106
|
+
* @param {number} options.positionStart
|
|
107
|
+
* @param {number} options.positionEnd
|
|
144
108
|
* @returns {ParseError}
|
|
145
109
|
*/
|
|
146
110
|
export function missingClauseError({ missing, context, positionStart, positionEnd }) {
|
|
147
|
-
return new ParseError({ message: `${context} requires ${missing}`, positionStart
|
|
111
|
+
return new ParseError({ message: `${context} requires ${missing}`, positionStart, positionEnd })
|
|
148
112
|
}
|
|
149
113
|
|
|
150
114
|
/**
|
|
@@ -152,8 +116,8 @@ export function missingClauseError({ missing, context, positionStart, positionEn
|
|
|
152
116
|
*
|
|
153
117
|
* @param {Object} options
|
|
154
118
|
* @param {string} options.cteName - The duplicate CTE name
|
|
155
|
-
* @param {number} options.positionStart
|
|
156
|
-
* @param {number} options.positionEnd
|
|
119
|
+
* @param {number} options.positionStart
|
|
120
|
+
* @param {number} options.positionEnd
|
|
157
121
|
* @returns {ParseError}
|
|
158
122
|
*/
|
|
159
123
|
export function duplicateCTEError({ cteName, positionStart, positionEnd }) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ExecutionError } from './executionErrors.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object} options
|
|
5
|
+
* @param {string} options.table - The missing table name
|
|
6
|
+
* @param {Record<string, any>} options.tables - Available tables object
|
|
7
|
+
* @param {number} [options.positionStart]
|
|
8
|
+
* @param {number} [options.positionEnd]
|
|
9
|
+
* @returns {ExecutionError}
|
|
10
|
+
*/
|
|
11
|
+
export function tableNotFoundError({ table, tables, positionStart, positionEnd }) {
|
|
12
|
+
const names = tables ? Object.keys(tables) : []
|
|
13
|
+
const available = names.length
|
|
14
|
+
? `. Available tables: ${names.join(', ')}`
|
|
15
|
+
: ''
|
|
16
|
+
return new ExecutionError({
|
|
17
|
+
message: `Table "${table}" not found${available}`,
|
|
18
|
+
positionStart,
|
|
19
|
+
positionEnd,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {string} options.columnName - The missing column name
|
|
26
|
+
* @param {string[]} options.availableColumns - List of available column names
|
|
27
|
+
* @param {number} options.positionStart
|
|
28
|
+
* @param {number} options.positionEnd
|
|
29
|
+
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
30
|
+
* @returns {ExecutionError}
|
|
31
|
+
*/
|
|
32
|
+
export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd, rowIndex }) {
|
|
33
|
+
const available = availableColumns.length > 0
|
|
34
|
+
? `. Available columns: ${availableColumns.join(', ')}`
|
|
35
|
+
: ''
|
|
36
|
+
return new ExecutionError({
|
|
37
|
+
message: `Column "${columnName}" not found${available}`,
|
|
38
|
+
positionStart,
|
|
39
|
+
positionEnd,
|
|
40
|
+
rowIndex,
|
|
41
|
+
})
|
|
42
|
+
}
|
package/src/executionErrors.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// EXECUTION ERRORS - Issues during query execution
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Structured execution error with position range and optional row number.
|
|
7
|
-
*/
|
|
8
|
-
export class ExecutionError extends Error {
|
|
9
|
-
/**
|
|
10
|
-
* @param {Object} options
|
|
11
|
-
* @param {string} options.message - Human-readable error message
|
|
12
|
-
* @param {number} options.positionStart - Start position (0-based character offset)
|
|
13
|
-
* @param {number} options.positionEnd - End position (exclusive, 0-based character offset)
|
|
14
|
-
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
15
|
-
*/
|
|
16
|
-
constructor({ message, positionStart, positionEnd, rowIndex }) {
|
|
17
|
-
const rowSuffix = rowIndex != null ? ` (row ${rowIndex})` : ''
|
|
18
|
-
super(message + rowSuffix)
|
|
19
|
-
this.name = 'ExecutionError'
|
|
20
|
-
this.positionStart = positionStart
|
|
21
|
-
this.positionEnd = positionEnd
|
|
22
|
-
this.rowIndex = rowIndex
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {Object} options
|
|
28
|
-
* @param {string} options.tableName - The missing table name
|
|
29
|
-
* @returns {Error}
|
|
30
|
-
*/
|
|
31
|
-
export function tableNotFoundError({ tableName }) {
|
|
32
|
-
return new Error(`Table "${tableName}" not found. Check spelling or add it to the tables parameter.`)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Error for invalid context (e.g., INTERVAL without date arithmetic).
|
|
37
|
-
*
|
|
38
|
-
* @param {Object} options
|
|
39
|
-
* @param {string} options.item - What was used incorrectly
|
|
40
|
-
* @param {string} options.validContext - Where it can be used
|
|
41
|
-
* @param {number} options.positionStart - Start position in query
|
|
42
|
-
* @param {number} options.positionEnd - End position in query
|
|
43
|
-
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
44
|
-
* @returns {ExecutionError}
|
|
45
|
-
*/
|
|
46
|
-
export function invalidContextError({ item, validContext, positionStart, positionEnd, rowIndex }) {
|
|
47
|
-
return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowIndex })
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {Object} options
|
|
52
|
-
* @param {string} options.operation - The unsupported operation
|
|
53
|
-
* @param {string} [options.hint] - How to fix it
|
|
54
|
-
* @returns {Error}
|
|
55
|
-
*/
|
|
56
|
-
export function unsupportedOperationError({ operation, hint }) {
|
|
57
|
-
const suffix = hint ? `. ${hint}` : ''
|
|
58
|
-
return new Error(`${operation}${suffix}`)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @param {Object} options
|
|
63
|
-
* @param {string} options.columnName - The missing column name
|
|
64
|
-
* @param {string[]} options.availableColumns - List of available column names
|
|
65
|
-
* @param {number} options.positionStart - Start position in query
|
|
66
|
-
* @param {number} options.positionEnd - End position in query
|
|
67
|
-
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
68
|
-
* @returns {ExecutionError}
|
|
69
|
-
*/
|
|
70
|
-
export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd, rowIndex }) {
|
|
71
|
-
const available = availableColumns.length > 0
|
|
72
|
-
? `. Available columns: ${availableColumns.join(', ')}`
|
|
73
|
-
: ''
|
|
74
|
-
return new ExecutionError({
|
|
75
|
-
message: `Column "${columnName}" not found${available}`,
|
|
76
|
-
positionStart,
|
|
77
|
-
positionEnd,
|
|
78
|
-
rowIndex,
|
|
79
|
-
})
|
|
80
|
-
}
|
package/src/validation.js
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import { ParseError } from './parseErrors.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @import { AggregateFunc, BinaryOp, ExprNode, FunctionNode, IntervalUnit, MathFunc, SpatialFunc, StringFunc, UserDefinedFunction } from './types.js'
|
|
5
|
-
* @param {string} name
|
|
6
|
-
* @returns {name is AggregateFunc}
|
|
7
|
-
*/
|
|
8
|
-
export function isAggregateFunc(name) {
|
|
9
|
-
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP'].includes(name)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Finds the first aggregate function call in an expression tree.
|
|
14
|
-
* Does not recurse into subqueries (they have their own aggregate scope).
|
|
15
|
-
*
|
|
16
|
-
* @param {ExprNode | undefined} expr
|
|
17
|
-
* @returns {FunctionNode | undefined}
|
|
18
|
-
*/
|
|
19
|
-
export function findAggregate(expr) {
|
|
20
|
-
if (!expr) return undefined
|
|
21
|
-
if (expr.type === 'function' && isAggregateFunc(expr.name.toUpperCase())) {
|
|
22
|
-
return expr
|
|
23
|
-
}
|
|
24
|
-
if (expr.type === 'binary') {
|
|
25
|
-
return findAggregate(expr.left) || findAggregate(expr.right)
|
|
26
|
-
}
|
|
27
|
-
if (expr.type === 'unary') {
|
|
28
|
-
return findAggregate(expr.argument)
|
|
29
|
-
}
|
|
30
|
-
if (expr.type === 'cast') {
|
|
31
|
-
return findAggregate(expr.expr)
|
|
32
|
-
}
|
|
33
|
-
if (expr.type === 'case') {
|
|
34
|
-
if (expr.caseExpr) {
|
|
35
|
-
const found = findAggregate(expr.caseExpr)
|
|
36
|
-
if (found) return found
|
|
37
|
-
}
|
|
38
|
-
for (const when of expr.whenClauses) {
|
|
39
|
-
const found = findAggregate(when.condition) || findAggregate(when.result)
|
|
40
|
-
if (found) return found
|
|
41
|
-
}
|
|
42
|
-
return findAggregate(expr.elseResult)
|
|
43
|
-
}
|
|
44
|
-
if (expr.type === 'in valuelist') {
|
|
45
|
-
const found = findAggregate(expr.expr)
|
|
46
|
-
if (found) return found
|
|
47
|
-
for (const val of expr.values) {
|
|
48
|
-
const found = findAggregate(val)
|
|
49
|
-
if (found) return found
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// Subqueries have their own aggregate scope
|
|
53
|
-
return undefined
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Throws a ParseError if the expression contains an aggregate function.
|
|
58
|
-
*
|
|
59
|
-
* @param {ExprNode | undefined} expr - The expression to check
|
|
60
|
-
* @param {string} clause - The clause name (e.g., 'WHERE', 'JOIN ON', 'GROUP BY')
|
|
61
|
-
*/
|
|
62
|
-
export function expectNoAggregate(expr, clause) {
|
|
63
|
-
const agg = findAggregate(expr)
|
|
64
|
-
if (agg) {
|
|
65
|
-
const hint = clause === 'WHERE' ? '. Use HAVING instead.' : ''
|
|
66
|
-
throw new ParseError({
|
|
67
|
-
message: `Aggregate function ${agg.name.toUpperCase()} is not allowed in ${clause} clause${hint}`,
|
|
68
|
-
positionStart: agg.positionStart,
|
|
69
|
-
positionEnd: agg.positionEnd,
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @param {string} name
|
|
76
|
-
* @returns {boolean}
|
|
77
|
-
*/
|
|
78
|
-
export function isRegexpFunc(name) {
|
|
79
|
-
return ['REGEXP_SUBSTR', 'REGEXP_REPLACE'].includes(name)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @param {string} name
|
|
84
|
-
* @returns {name is SpatialFunc}
|
|
85
|
-
*/
|
|
86
|
-
export function isSpatialFunc(name) {
|
|
87
|
-
return [
|
|
88
|
-
'ST_INTERSECTS', 'ST_CONTAINS', 'ST_CONTAINSPROPERLY', 'ST_WITHIN',
|
|
89
|
-
'ST_OVERLAPS', 'ST_TOUCHES', 'ST_EQUALS', 'ST_CROSSES',
|
|
90
|
-
'ST_COVERS', 'ST_COVEREDBY', 'ST_DWITHIN',
|
|
91
|
-
'ST_GEOMFROMTEXT', 'ST_MAKEENVELOPE', 'ST_ASTEXT',
|
|
92
|
-
].includes(name)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* @param {string} name
|
|
97
|
-
* @returns {name is MathFunc}
|
|
98
|
-
*/
|
|
99
|
-
export function isMathFunc(name) {
|
|
100
|
-
return [
|
|
101
|
-
'FLOOR', 'CEIL', 'CEILING', 'ROUND', 'ABS', 'SIGN', 'MOD', 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
|
|
102
|
-
'SIN', 'COS', 'TAN', 'COT', 'ASIN', 'ACOS', 'ATAN', 'ATAN2', 'DEGREES', 'RADIANS', 'PI',
|
|
103
|
-
'RAND', 'RANDOM',
|
|
104
|
-
].includes(name)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* @param {string} name
|
|
109
|
-
* @returns {name is IntervalUnit}
|
|
110
|
-
*/
|
|
111
|
-
export function isIntervalUnit(name) {
|
|
112
|
-
return ['DAY', 'MONTH', 'YEAR', 'HOUR', 'MINUTE', 'SECOND'].includes(name)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* @param {string} name
|
|
117
|
-
* @returns {boolean}
|
|
118
|
-
*/
|
|
119
|
-
export function isExtractField(name) {
|
|
120
|
-
return ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'DOW', 'EPOCH'].includes(name)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @param {string} name
|
|
125
|
-
* @returns {name is StringFunc}
|
|
126
|
-
*/
|
|
127
|
-
export function isStringFunc(name) {
|
|
128
|
-
return [
|
|
129
|
-
'UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM',
|
|
130
|
-
'REPLACE', 'LEFT', 'RIGHT', 'INSTR',
|
|
131
|
-
].includes(name)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* @param {string} op
|
|
136
|
-
* @returns {op is BinaryOp}
|
|
137
|
-
*/
|
|
138
|
-
export function isBinaryOp(op) {
|
|
139
|
-
return ['AND', 'OR', 'LIKE', '=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Function argument count specifications.
|
|
144
|
-
* min: minimum number of arguments
|
|
145
|
-
* max: maximum number of arguments
|
|
146
|
-
* @type {Record<string, {min: number, max?: number}>}
|
|
147
|
-
*/
|
|
148
|
-
export const FUNCTION_ARG_COUNTS = {
|
|
149
|
-
// String functions
|
|
150
|
-
UPPER: { min: 1, max: 1 },
|
|
151
|
-
LOWER: { min: 1, max: 1 },
|
|
152
|
-
LENGTH: { min: 1, max: 1 },
|
|
153
|
-
TRIM: { min: 1, max: 1 },
|
|
154
|
-
REPLACE: { min: 3, max: 3 },
|
|
155
|
-
SUBSTRING: { min: 2, max: 3 },
|
|
156
|
-
SUBSTR: { min: 2, max: 3 },
|
|
157
|
-
CONCAT: { min: 1 },
|
|
158
|
-
LEFT: { min: 2, max: 2 },
|
|
159
|
-
RIGHT: { min: 2, max: 2 },
|
|
160
|
-
INSTR: { min: 2, max: 2 },
|
|
161
|
-
REGEXP_SUBSTR: { min: 2, max: 4 },
|
|
162
|
-
REGEXP_REPLACE: { min: 3, max: 5 },
|
|
163
|
-
|
|
164
|
-
// Date/time functions
|
|
165
|
-
RANDOM: { min: 0, max: 0 },
|
|
166
|
-
RAND: { min: 0, max: 0 },
|
|
167
|
-
CURRENT_DATE: { min: 0, max: 0 },
|
|
168
|
-
CURRENT_TIME: { min: 0, max: 0 },
|
|
169
|
-
CURRENT_TIMESTAMP: { min: 0, max: 0 },
|
|
170
|
-
DATE_TRUNC: { min: 2, max: 2 },
|
|
171
|
-
DATE_PART: { min: 2, max: 2 },
|
|
172
|
-
EXTRACT: { min: 2, max: 2 },
|
|
173
|
-
|
|
174
|
-
// Math functions
|
|
175
|
-
FLOOR: { min: 1, max: 1 },
|
|
176
|
-
CEIL: { min: 1, max: 1 },
|
|
177
|
-
CEILING: { min: 1, max: 1 },
|
|
178
|
-
ROUND: { min: 1, max: 2 },
|
|
179
|
-
ABS: { min: 1, max: 1 },
|
|
180
|
-
SIGN: { min: 1, max: 1 },
|
|
181
|
-
MOD: { min: 2, max: 2 },
|
|
182
|
-
EXP: { min: 1, max: 1 },
|
|
183
|
-
LN: { min: 1, max: 1 },
|
|
184
|
-
LOG10: { min: 1, max: 1 },
|
|
185
|
-
POWER: { min: 2, max: 2 },
|
|
186
|
-
SQRT: { min: 1, max: 1 },
|
|
187
|
-
SIN: { min: 1, max: 1 },
|
|
188
|
-
COS: { min: 1, max: 1 },
|
|
189
|
-
TAN: { min: 1, max: 1 },
|
|
190
|
-
COT: { min: 1, max: 1 },
|
|
191
|
-
ASIN: { min: 1, max: 1 },
|
|
192
|
-
ACOS: { min: 1, max: 1 },
|
|
193
|
-
ATAN: { min: 1, max: 2 },
|
|
194
|
-
ATAN2: { min: 2, max: 2 },
|
|
195
|
-
DEGREES: { min: 1, max: 1 },
|
|
196
|
-
RADIANS: { min: 1, max: 1 },
|
|
197
|
-
PI: { min: 0, max: 0 },
|
|
198
|
-
|
|
199
|
-
// JSON functions
|
|
200
|
-
JSON_VALUE: { min: 2, max: 2 },
|
|
201
|
-
JSON_QUERY: { min: 2, max: 2 },
|
|
202
|
-
JSON_OBJECT: { min: 0 },
|
|
203
|
-
JSON_ARRAYAGG: { min: 1, max: 1 },
|
|
204
|
-
|
|
205
|
-
// Array functions
|
|
206
|
-
ARRAY_LENGTH: { min: 1, max: 1 },
|
|
207
|
-
ARRAY_POSITION: { min: 2, max: 2 },
|
|
208
|
-
ARRAY_SORT: { min: 1, max: 1 },
|
|
209
|
-
CARDINALITY: { min: 1, max: 1 },
|
|
210
|
-
|
|
211
|
-
// Conditional functions
|
|
212
|
-
COALESCE: { min: 1 },
|
|
213
|
-
NULLIF: { min: 2, max: 2 },
|
|
214
|
-
|
|
215
|
-
// Aggregate functions
|
|
216
|
-
COUNT: { min: 1, max: 1 },
|
|
217
|
-
SUM: { min: 1, max: 1 },
|
|
218
|
-
AVG: { min: 1, max: 1 },
|
|
219
|
-
MIN: { min: 1, max: 1 },
|
|
220
|
-
MAX: { min: 1, max: 1 },
|
|
221
|
-
STDDEV_SAMP: { min: 1, max: 1 },
|
|
222
|
-
STDDEV_POP: { min: 1, max: 1 },
|
|
223
|
-
|
|
224
|
-
// Spatial predicate functions
|
|
225
|
-
ST_INTERSECTS: { min: 2, max: 2 },
|
|
226
|
-
ST_CONTAINS: { min: 2, max: 2 },
|
|
227
|
-
ST_CONTAINSPROPERLY: { min: 2, max: 2 },
|
|
228
|
-
ST_WITHIN: { min: 2, max: 2 },
|
|
229
|
-
ST_OVERLAPS: { min: 2, max: 2 },
|
|
230
|
-
ST_TOUCHES: { min: 2, max: 2 },
|
|
231
|
-
ST_EQUALS: { min: 2, max: 2 },
|
|
232
|
-
ST_CROSSES: { min: 2, max: 2 },
|
|
233
|
-
ST_COVERS: { min: 2, max: 2 },
|
|
234
|
-
ST_COVEREDBY: { min: 2, max: 2 },
|
|
235
|
-
ST_DWITHIN: { min: 3, max: 3 },
|
|
236
|
-
ST_GEOMFROMTEXT: { min: 1, max: 1 },
|
|
237
|
-
ST_MAKEENVELOPE: { min: 4, max: 4 },
|
|
238
|
-
ST_ASTEXT: { min: 1, max: 1 },
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Format expected argument count for error messages.
|
|
243
|
-
* @param {number} min
|
|
244
|
-
* @param {number | undefined} max
|
|
245
|
-
* @returns {string | number}
|
|
246
|
-
*/
|
|
247
|
-
function formatExpected(min, max) {
|
|
248
|
-
if (max == null) return `at least ${min}`
|
|
249
|
-
if (min === max) return min
|
|
250
|
-
return `${min} or ${max}`
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Validates function argument count.
|
|
255
|
-
* @param {string} funcName - The function name (uppercase)
|
|
256
|
-
* @param {number} argCount - Number of arguments provided
|
|
257
|
-
* @param {Record<string, UserDefinedFunction>} [functions] - User-defined functions
|
|
258
|
-
* @returns {{ valid: boolean, expected: string | number }}
|
|
259
|
-
*/
|
|
260
|
-
export function validateFunctionArgCount(funcName, argCount, functions) {
|
|
261
|
-
// Check built-in functions
|
|
262
|
-
let spec = FUNCTION_ARG_COUNTS[funcName]
|
|
263
|
-
|
|
264
|
-
// Check user-defined functions (case-insensitive)
|
|
265
|
-
if (!spec && functions) {
|
|
266
|
-
const udfName = Object.keys(functions).find(k => k.toUpperCase() === funcName)
|
|
267
|
-
if (udfName) {
|
|
268
|
-
spec = functions[udfName].arguments
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (!spec) return { valid: true, expected: 0 }
|
|
273
|
-
|
|
274
|
-
const { min, max } = spec
|
|
275
|
-
|
|
276
|
-
if (argCount < min) {
|
|
277
|
-
return { valid: false, expected: formatExpected(min, max) }
|
|
278
|
-
}
|
|
279
|
-
if (max != null && argCount > max) {
|
|
280
|
-
return { valid: false, expected: formatExpected(min, max) }
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return { valid: true, expected: formatExpected(min, max) }
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Checks if a function is known (either built-in or user-defined).
|
|
288
|
-
* @param {string} funcName - The function name (uppercase)
|
|
289
|
-
* @param {Record<string, UserDefinedFunction>} [functions] - User-defined functions
|
|
290
|
-
* @returns {boolean}
|
|
291
|
-
*/
|
|
292
|
-
export function isKnownFunction(funcName, functions) {
|
|
293
|
-
// Check built-in functions
|
|
294
|
-
if (
|
|
295
|
-
isAggregateFunc(funcName) ||
|
|
296
|
-
isMathFunc(funcName) ||
|
|
297
|
-
isStringFunc(funcName) ||
|
|
298
|
-
isRegexpFunc(funcName) ||
|
|
299
|
-
isSpatialFunc(funcName)
|
|
300
|
-
) {
|
|
301
|
-
return true
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Date/time, JSON, conditional, and CAST functions
|
|
305
|
-
if ([
|
|
306
|
-
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DATE_TRUNC', 'DATE_PART', 'EXTRACT',
|
|
307
|
-
'JSON_VALUE', 'JSON_QUERY', 'JSON_OBJECT',
|
|
308
|
-
'ARRAY_LENGTH', 'ARRAY_POSITION', 'ARRAY_SORT', 'CARDINALITY',
|
|
309
|
-
'COALESCE', 'NULLIF', 'CAST',
|
|
310
|
-
].includes(funcName)) {
|
|
311
|
-
return true
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Check user-defined functions (case-insensitive)
|
|
315
|
-
if (functions) {
|
|
316
|
-
return Object.keys(functions).some(k => k.toUpperCase() === funcName)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return false
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Reserved keywords that cannot be used as identifiers in expressions.
|
|
323
|
-
// Non-reserved keywords (e.g. DAY, MONTH, FILTER, ASC) can be used as column alias references.
|
|
324
|
-
export const RESERVED_KEYWORDS = new Set([
|
|
325
|
-
'SELECT', 'FROM', 'WHERE', 'WITH',
|
|
326
|
-
'AND', 'OR', 'NOT', 'IS', 'LIKE', 'IN', 'BETWEEN',
|
|
327
|
-
'TRUE', 'FALSE', 'NULL',
|
|
328
|
-
'EXISTS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'INTERVAL',
|
|
329
|
-
'GROUP', 'BY', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
|
330
|
-
'AS', 'ALL', 'DISTINCT',
|
|
331
|
-
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'ON',
|
|
332
|
-
])
|
|
333
|
-
|
|
334
|
-
// Keywords that cannot be used as implicit aliases after a column
|
|
335
|
-
export const RESERVED_AFTER_COLUMN = new Set([
|
|
336
|
-
'FROM', 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
|
337
|
-
])
|
|
338
|
-
|
|
339
|
-
// Keywords that cannot be used as table aliases
|
|
340
|
-
export const RESERVED_AFTER_TABLE = new Set([
|
|
341
|
-
'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET', 'JOIN', 'INNER',
|
|
342
|
-
'LEFT', 'RIGHT', 'FULL', 'CROSS', 'ON', 'POSITIONAL',
|
|
343
|
-
])
|