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/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 type SqlPrimitive =
77
- | string
78
- | number
79
- | bigint
80
- | boolean
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
+ ])