squirreling 0.4.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ // ============================================================================
2
+ // PARSE ERRORS - Issues during SQL tokenization and parsing
3
+ // ============================================================================
4
+
5
+ /**
6
+ * Structured parse error with position range.
7
+ */
8
+ export class ParseError 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
+ */
15
+ constructor({ message, positionStart, positionEnd }) {
16
+ super(message)
17
+ this.name = 'ParseError'
18
+ this.positionStart = positionStart
19
+ this.positionEnd = positionEnd
20
+ }
21
+ }
22
+
23
+ /**
24
+ * General syntax error for unexpected tokens.
25
+ *
26
+ * @param {Object} options
27
+ * @param {string} options.expected - Description of what was expected
28
+ * @param {string} options.received - What was actually found
29
+ * @param {number} options.positionStart - Start character position in query
30
+ * @param {number} options.positionEnd - End character position in query
31
+ * @param {string} [options.after] - What token came before (for context)
32
+ * @returns {ParseError}
33
+ */
34
+ export function syntaxError({ expected, received, positionStart, positionEnd, after }) {
35
+ const afterClause = after ? ` after "${after}"` : ''
36
+ return new ParseError({ message: `Expected ${expected}${afterClause} but found ${received} at position ${positionStart}`, positionStart, positionEnd })
37
+ }
38
+
39
+ /**
40
+ * Error for unterminated literals (strings, identifiers).
41
+ *
42
+ * @param {'string' | 'identifier'} type - Type of unterminated literal
43
+ * @param {number} positionStart - Starting position
44
+ * @param {number} positionEnd - End position
45
+ * @returns {ParseError}
46
+ */
47
+ export function unterminatedError(type, positionStart, positionEnd) {
48
+ const name = type === 'string' ? 'string literal' : 'identifier'
49
+ return new ParseError({ message: `Unterminated ${name} starting at position ${positionStart}`, positionStart, positionEnd })
50
+ }
51
+
52
+ /**
53
+ * Error for invalid literals (numbers, intervals, etc).
54
+ *
55
+ * @param {Object} options
56
+ * @param {string} options.type - Type of invalid literal (e.g., 'number', 'interval value', 'interval unit')
57
+ * @param {string} options.value - The invalid value
58
+ * @param {number} options.positionStart - Start position in query
59
+ * @param {number} options.positionEnd - End position in query
60
+ * @param {string} [options.validValues] - List of valid values (for enums like interval units)
61
+ * @returns {ParseError}
62
+ */
63
+ export function invalidLiteralError({ type, value, positionStart, positionEnd, validValues }) {
64
+ const suffix = validValues ? `. Valid values: ${validValues}` : ''
65
+ return new ParseError({ message: `Invalid ${type} ${value} at position ${positionStart}${suffix}`, positionStart, positionEnd })
66
+ }
67
+
68
+ /**
69
+ * Error for unexpected characters during tokenization.
70
+ *
71
+ * @param {Object} options
72
+ * @param {string} options.char - The unexpected character
73
+ * @param {number} options.positionStart - Position in query
74
+ * @param {boolean} [options.expectsSelect=false] - Whether SELECT was expected (first token)
75
+ * @returns {ParseError}
76
+ */
77
+ export function unexpectedCharError({ char, positionStart, expectsSelect = false }) {
78
+ const positionEnd = positionStart + 1
79
+ if (expectsSelect) {
80
+ return new ParseError({ message: `Expected SELECT but found "${char}" at position ${positionStart}. Queries must start with SELECT.`, positionStart, positionEnd })
81
+ }
82
+ return new ParseError({ message: `Unexpected character "${char}" at position ${positionStart}`, positionStart, positionEnd })
83
+ }
84
+
85
+ /**
86
+ * Error for unknown/unsupported functions.
87
+ *
88
+ * @param {Object} options
89
+ * @param {string} options.funcName - The unknown function name
90
+ * @param {number} options.positionStart - Start position in query
91
+ * @param {number} options.positionEnd - End position in query
92
+ * @param {string} [options.validFunctions] - List of valid functions
93
+ * @returns {ParseError}
94
+ */
95
+ export function unknownFunctionError({ funcName, positionStart, positionEnd, validFunctions }) {
96
+ const supported = validFunctions ||
97
+ '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'
98
+
99
+ return new ParseError({
100
+ message: `Unknown function "${funcName}" at position ${positionStart}. Supported: ${supported}`,
101
+ positionStart,
102
+ positionEnd,
103
+ })
104
+ }
105
+
106
+ /**
107
+ * Error for missing required clause or structure.
108
+ *
109
+ * @param {Object} options
110
+ * @param {string} options.missing - What is missing (e.g., 'WHEN clause', 'FROM clause', 'ON condition')
111
+ * @param {string} options.context - Where it's missing from (e.g., 'CASE expression', 'SELECT statement', 'JOIN')
112
+ * @param {number} [options.positionStart] - Start position in query
113
+ * @param {number} [options.positionEnd] - End position in query
114
+ * @returns {ParseError}
115
+ */
116
+ export function missingClauseError({ missing, context, positionStart, positionEnd }) {
117
+ return new ParseError({ message: `${context} requires ${missing}`, positionStart: positionStart ?? 0, positionEnd: positionEnd ?? 0 })
118
+ }
package/src/types.d.ts CHANGED
@@ -6,6 +6,11 @@
6
6
  export interface QueryHints {
7
7
  columns?: string[] // columns needed
8
8
  where?: ExprNode // where clause
9
+ // important: only apply limit/offset if where is fully applied by the data source
10
+ // otherwise, the data source must return at least enough rows to ensure the engine
11
+ // can apply limit/offset correctly after filtering
12
+ // even with offset, the datasource must return rows starting from offset 0
13
+ // but doesn't need to resolve async rows before the offset
9
14
  limit?: number
10
15
  offset?: number
11
16
  }
@@ -15,16 +20,20 @@ export interface QueryHints {
15
20
  * Provides an async iterator over rows.
16
21
  */
17
22
  export interface AsyncDataSource {
18
- getRows(hints?: QueryHints): AsyncIterable<AsyncRow>
23
+ scan(hints?: QueryHints): AsyncIterable<AsyncRow>
19
24
  }
20
- export type AsyncRow = Record<string, AsyncCell>
25
+ export interface AsyncRow {
26
+ columns: string[]
27
+ cells: AsyncCells
28
+ }
29
+ export type AsyncCells = Record<string, AsyncCell>
21
30
  export type AsyncCell = () => Promise<SqlPrimitive>
22
31
 
23
32
  export type Row = Record<string, SqlPrimitive>[]
24
33
 
25
34
  export interface ExecuteSqlOptions {
26
35
  tables: Record<string, Row | AsyncDataSource>
27
- query: string
36
+ query: string | SelectStatement
28
37
  }
29
38
 
30
39
  export type SqlPrimitive =
@@ -68,54 +77,59 @@ export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
68
77
 
69
78
  export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
70
79
 
71
- export interface LiteralNode {
80
+ export interface ExprNodeBase {
81
+ positionStart: number
82
+ positionEnd: number
83
+ }
84
+
85
+ export interface LiteralNode extends ExprNodeBase {
72
86
  type: 'literal'
73
87
  value: SqlPrimitive
74
88
  }
75
89
 
76
- export interface IdentifierNode {
90
+ export interface IdentifierNode extends ExprNodeBase {
77
91
  type: 'identifier'
78
92
  name: string
79
93
  }
80
94
 
81
- export interface UnaryNode {
95
+ export interface UnaryNode extends ExprNodeBase {
82
96
  type: 'unary'
83
97
  op: 'NOT' | 'IS NULL' | 'IS NOT NULL' | '-'
84
98
  argument: ExprNode
85
99
  }
86
100
 
87
- export interface BinaryNode {
101
+ export interface BinaryNode extends ExprNodeBase {
88
102
  type: 'binary'
89
103
  op: BinaryOp
90
104
  left: ExprNode
91
105
  right: ExprNode
92
106
  }
93
107
 
94
- export interface FunctionNode {
108
+ export interface FunctionNode extends ExprNodeBase {
95
109
  type: 'function'
96
110
  name: string
97
111
  args: ExprNode[]
98
112
  }
99
113
 
100
- export interface CastNode {
114
+ export interface CastNode extends ExprNodeBase {
101
115
  type: 'cast'
102
116
  expr: ExprNode
103
117
  toType: string
104
118
  }
105
119
 
106
- export interface InSubqueryNode {
120
+ export interface InSubqueryNode extends ExprNodeBase {
107
121
  type: 'in'
108
122
  expr: ExprNode
109
123
  subquery: SelectStatement
110
124
  }
111
125
 
112
- export interface InValuesNode {
126
+ export interface InValuesNode extends ExprNodeBase {
113
127
  type: 'in valuelist'
114
128
  expr: ExprNode
115
129
  values: ExprNode[]
116
130
  }
117
131
 
118
- export interface ExistsNode {
132
+ export interface ExistsNode extends ExprNodeBase {
119
133
  type: 'exists' | 'not exists'
120
134
  subquery: SelectStatement
121
135
  }
@@ -125,21 +139,21 @@ export interface WhenClause {
125
139
  result: ExprNode
126
140
  }
127
141
 
128
- export interface CaseNode {
142
+ export interface CaseNode extends ExprNodeBase {
129
143
  type: 'case'
130
144
  caseExpr?: ExprNode
131
145
  whenClauses: WhenClause[]
132
146
  elseResult?: ExprNode
133
147
  }
134
148
 
135
- export interface SubqueryNode {
149
+ export interface SubqueryNode extends ExprNodeBase {
136
150
  type: 'subquery'
137
151
  subquery: SelectStatement
138
152
  }
139
153
 
140
154
  export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
141
155
 
142
- export interface IntervalNode {
156
+ export interface IntervalNode extends ExprNodeBase {
143
157
  type: 'interval'
144
158
  value: number
145
159
  unit: IntervalUnit
@@ -167,6 +181,29 @@ export interface StarColumn {
167
181
 
168
182
  export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG'
169
183
 
184
+ export type MathFunc =
185
+ | 'FLOOR'
186
+ | 'CEIL'
187
+ | 'CEILING'
188
+ | 'ABS'
189
+ | 'MOD'
190
+ | 'EXP'
191
+ | 'LN'
192
+ | 'LOG10'
193
+ | 'POWER'
194
+ | 'SQRT'
195
+ | 'SIN'
196
+ | 'COS'
197
+ | 'TAN'
198
+ | 'COT'
199
+ | 'ASIN'
200
+ | 'ACOS'
201
+ | 'ATAN'
202
+ | 'ATAN2'
203
+ | 'DEGREES'
204
+ | 'RADIANS'
205
+ | 'PI'
206
+
170
207
  export type StringFunc =
171
208
  | 'UPPER'
172
209
  | 'LOWER'
@@ -228,6 +265,7 @@ export interface JoinClause {
228
265
  export interface ParserState {
229
266
  tokens: Token[]
230
267
  pos: number
268
+ lastPos?: number
231
269
  }
232
270
 
233
271
  // Tokenizer types
@@ -246,7 +284,8 @@ export type TokenType =
246
284
  export interface Token {
247
285
  type: TokenType
248
286
  value: string
249
- position: number
287
+ positionStart: number
288
+ positionEnd: number
250
289
  numericValue?: number | bigint
251
290
  originalValue?: string
252
291
  }
package/src/validation.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, StringFunc} from './types.js'
3
+ * @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, MathFunc, StringFunc} from './types.js'
4
4
  * @param {string} name
5
5
  * @returns {name is AggregateFunc}
6
6
  */
@@ -8,6 +8,19 @@ export function isAggregateFunc(name) {
8
8
  return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG'].includes(name)
9
9
  }
10
10
 
11
+ /**
12
+ * @param {string} name
13
+ * @returns {name is MathFunc}
14
+ */
15
+ export function isMathFunc(name) {
16
+ return [
17
+ 'FLOOR', 'CEIL', 'CEILING', 'ABS', 'MOD',
18
+ 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
19
+ 'SIN', 'COS', 'TAN', 'COT', 'ASIN', 'ACOS', 'ATAN', 'ATAN2',
20
+ 'DEGREES', 'RADIANS', 'PI',
21
+ ].includes(name)
22
+ }
23
+
11
24
  /**
12
25
  * @param {string} name
13
26
  * @returns {name is IntervalUnit}
@@ -0,0 +1,138 @@
1
+ import { ExecutionError } from './executionErrors.js'
2
+
3
+ // ============================================================================
4
+ // VALIDATION ERRORS - Function argument and type validation
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Function signatures for helpful error messages.
9
+ * Maps function name to its parameter signature.
10
+ * @type {Record<string, string>}
11
+ */
12
+ const FUNCTION_SIGNATURES = {
13
+ // String functions
14
+ UPPER: 'string',
15
+ LOWER: 'string',
16
+ LENGTH: 'string',
17
+ TRIM: 'string',
18
+ REPLACE: 'string, search, replacement',
19
+ SUBSTRING: 'string, start[, length]',
20
+ SUBSTR: 'string, start[, length]',
21
+ CONCAT: 'value1, value2[, ...]',
22
+
23
+ // Date/time functions
24
+ RANDOM: '',
25
+ RAND: '',
26
+ CURRENT_DATE: '',
27
+ CURRENT_TIME: '',
28
+ CURRENT_TIMESTAMP: '',
29
+
30
+ // Math functions
31
+ FLOOR: 'number',
32
+ CEIL: 'number',
33
+ CEILING: 'number',
34
+ ABS: 'number',
35
+ MOD: 'dividend, divisor',
36
+ EXP: 'number',
37
+ LN: 'number',
38
+ LOG10: 'number',
39
+ POWER: 'base, exponent',
40
+ SQRT: 'number',
41
+ SIN: 'radians',
42
+ COS: 'radians',
43
+ TAN: 'radians',
44
+ COT: 'radians',
45
+ ASIN: 'number',
46
+ ACOS: 'number',
47
+ ATAN: 'number',
48
+ ATAN2: 'y, x',
49
+ DEGREES: 'radians',
50
+ RADIANS: 'degrees',
51
+ PI: '',
52
+
53
+ // JSON functions
54
+ JSON_VALUE: 'expression, path',
55
+ JSON_QUERY: 'expression, path',
56
+ JSON_OBJECT: 'key1, value1[, ...]',
57
+ JSON_ARRAYAGG: 'expression',
58
+
59
+ // Aggregate functions
60
+ COUNT: 'expression',
61
+ SUM: 'expression',
62
+ AVG: 'expression',
63
+ MIN: 'expression',
64
+ MAX: 'expression',
65
+ }
66
+
67
+ /**
68
+ * Error for wrong number of function arguments.
69
+ *
70
+ * @param {Object} options
71
+ * @param {string} options.funcName - The function name
72
+ * @param {number | string} options.expected - Expected count (number or range like "2 or 3")
73
+ * @param {number} options.received - Actual argument count
74
+ * @param {number} options.positionStart - Start position in query
75
+ * @param {number} options.positionEnd - End position in query
76
+ * @param {number} [options.rowNumber] - 1-based row number where error occurred
77
+ * @returns {ExecutionError}
78
+ */
79
+ export function argCountError({ funcName, expected, received, positionStart, positionEnd, rowNumber }) {
80
+ const signature = FUNCTION_SIGNATURES[funcName] ?? ''
81
+ let expectedStr = `${expected} arguments`
82
+ if (expected === 0) expectedStr = 'no arguments'
83
+ if (expected === 1) expectedStr = '1 argument'
84
+ if (typeof expected === 'string' && expected.endsWith(' 1')) {
85
+ expectedStr = `${expected} argument`
86
+ }
87
+
88
+ return new ExecutionError({ message: `${funcName}(${signature}) function requires ${expectedStr}, got ${received}`, positionStart, positionEnd, rowNumber })
89
+ }
90
+
91
+ /**
92
+ * Error for invalid argument type or value.
93
+ *
94
+ * @param {Object} options
95
+ * @param {string} options.funcName - The function name
96
+ * @param {string} options.message - Specific error message
97
+ * @param {number} options.positionStart - Start position in query
98
+ * @param {number} options.positionEnd - End position in query
99
+ * @param {string} [options.hint] - Recovery hint
100
+ * @param {number} [options.rowNumber] - 1-based row number where error occurred
101
+ * @returns {ExecutionError}
102
+ */
103
+ export function argValueError({ funcName, message, positionStart, positionEnd, hint, rowNumber }) {
104
+ const signature = FUNCTION_SIGNATURES[funcName] ?? ''
105
+ const suffix = hint ? `. ${hint}` : ''
106
+ return new ExecutionError({ message: `${funcName}(${signature}): ${message}${suffix}`, positionStart, positionEnd, rowNumber })
107
+ }
108
+
109
+ /**
110
+ * Error for aggregate function misuse (e.g., SUM(*)).
111
+ *
112
+ * @param {Object} options
113
+ * @param {string} options.funcName - The aggregate function
114
+ * @param {string} options.issue - What's wrong (e.g., "(*) is not supported")
115
+ * @returns {Error}
116
+ */
117
+ export function aggregateError({ funcName, issue }) {
118
+ return new Error(`${funcName}${issue}. Only COUNT supports *. Use a column name for ${funcName}.`)
119
+ }
120
+
121
+ /**
122
+ * Error for unsupported CAST type.
123
+ *
124
+ * @param {Object} options
125
+ * @param {string} options.toType - The unsupported target type
126
+ * @param {number} options.positionStart - Start position in query
127
+ * @param {number} options.positionEnd - End position in query
128
+ * @param {string} [options.fromType] - The source type (optional)
129
+ * @param {number} [options.rowNumber] - 1-based row number where error occurred
130
+ * @returns {ExecutionError}
131
+ */
132
+ export function castError({ toType, positionStart, positionEnd, fromType, rowNumber }) {
133
+ const message = fromType
134
+ ? `Cannot CAST ${fromType} to ${toType}`
135
+ : `Unsupported CAST to type ${toType}`
136
+
137
+ return new ExecutionError({ message: `${message}. Supported types: TEXT, VARCHAR, INTEGER, INT, BIGINT, FLOAT, REAL, DOUBLE, BOOLEAN`, positionStart, positionEnd, rowNumber })
138
+ }
package/src/errors.js DELETED
@@ -1,230 +0,0 @@
1
- // ============================================================================
2
- // PARSE ERRORS - Issues during SQL tokenization and parsing
3
- // ============================================================================
4
-
5
- /**
6
- * General syntax error for unexpected tokens.
7
- *
8
- * @param {Object} options
9
- * @param {string} options.expected - Description of what was expected
10
- * @param {string} options.received - What was actually found
11
- * @param {number} options.position - Character position in query
12
- * @param {string} [options.after] - What token came before (for context)
13
- * @returns {Error}
14
- */
15
- export function syntaxError({ expected, received, position, after }) {
16
- const afterClause = after ? ` after "${after}"` : ''
17
- return new Error(`Expected ${expected}${afterClause} but found ${received} at position ${position}`)
18
- }
19
-
20
- /**
21
- * Error for unterminated literals (strings, identifiers).
22
- *
23
- * @param {'string' | 'identifier'} type - Type of unterminated literal
24
- * @param {number} position - Starting position
25
- * @returns {Error}
26
- */
27
- export function unterminatedError(type, position) {
28
- const name = type === 'string' ? 'string literal' : 'identifier'
29
- return new Error(`Unterminated ${name} starting at position ${position}`)
30
- }
31
-
32
- /**
33
- * Error for invalid literals (numbers, intervals, etc).
34
- *
35
- * @param {Object} options
36
- * @param {string} options.type - Type of invalid literal (e.g., 'number', 'interval value', 'interval unit')
37
- * @param {string} options.value - The invalid value
38
- * @param {number} options.position - Position in query
39
- * @param {string} [options.validValues] - List of valid values (for enums like interval units)
40
- * @returns {Error}
41
- */
42
- export function invalidLiteralError({ type, value, position, validValues }) {
43
- const suffix = validValues ? `. Valid values: ${validValues}` : ''
44
- return new Error(`Invalid ${type} ${value} at position ${position}${suffix}`)
45
- }
46
-
47
- /**
48
- * Error for unexpected characters during tokenization.
49
- *
50
- * @param {string} char - The unexpected character
51
- * @param {number} position - Position in query
52
- * @param {boolean} [expectsSelect=false] - Whether SELECT was expected (first token)
53
- * @returns {Error}
54
- */
55
- export function unexpectedCharError(char, position, expectsSelect = false) {
56
- if (expectsSelect) {
57
- return new Error(`Expected SELECT but found "${char}" at position ${position}. Queries must start with SELECT.`)
58
- }
59
- return new Error(`Unexpected character "${char}" at position ${position}`)
60
- }
61
-
62
- /**
63
- * Error for unknown/unsupported functions.
64
- *
65
- * @param {string} funcName - The unknown function name
66
- * @param {number} [position] - Position in query (for parse errors)
67
- * @param {string} [validFunctions] - List of valid functions
68
- * @returns {Error}
69
- */
70
- export function unknownFunctionError(funcName, position, validFunctions) {
71
- const supported = validFunctions ||
72
- 'COUNT, SUM, AVG, MIN, MAX, UPPER, LOWER, CONCAT, LENGTH, SUBSTRING, TRIM, REPLACE, JSON_OBJECT, JSON_VALUE, JSON_QUERY, JSON_ARRAYAGG'
73
-
74
- if (position !== undefined) {
75
- return new Error(`Unknown function "${funcName}" at position ${position}. Supported: ${supported}`)
76
- }
77
- return new Error(`Unsupported function: ${funcName}. Supported: ${supported}`)
78
- }
79
-
80
- /**
81
- * Error for missing required clause or structure.
82
- *
83
- * @param {Object} options
84
- * @param {string} options.missing - What is missing (e.g., 'WHEN clause', 'FROM clause', 'ON condition')
85
- * @param {string} options.context - Where it's missing from (e.g., 'CASE expression', 'SELECT statement', 'JOIN')
86
- * @returns {Error}
87
- */
88
- export function missingClauseError({ missing, context }) {
89
- return new Error(`${context} requires ${missing}`)
90
- }
91
-
92
- // ============================================================================
93
- // EXECUTION ERRORS - Issues during query execution
94
- // ============================================================================
95
-
96
- /**
97
- * Error for missing table.
98
- *
99
- * @param {string} tableName - The missing table name
100
- * @returns {Error}
101
- */
102
- export function tableNotFoundError(tableName) {
103
- return new Error(`Table "${tableName}" not found. Check spelling or add it to the tables parameter.`)
104
- }
105
-
106
- /**
107
- * Error for invalid context (e.g., INTERVAL without date arithmetic).
108
- *
109
- * @param {Object} options
110
- * @param {string} options.item - What was used incorrectly
111
- * @param {string} options.validContext - Where it can be used
112
- * @returns {Error}
113
- */
114
- export function invalidContextError({ item, validContext }) {
115
- return new Error(`${item} can only be used with ${validContext}`)
116
- }
117
-
118
- /**
119
- * Error for unsupported operation combinations.
120
- *
121
- * @param {string} operation - The unsupported operation
122
- * @param {string} [hint] - How to fix it
123
- * @returns {Error}
124
- */
125
- export function unsupportedOperationError(operation, hint) {
126
- const suffix = hint ? `. ${hint}` : ''
127
- return new Error(`${operation}${suffix}`)
128
- }
129
-
130
- // ============================================================================
131
- // VALIDATION ERRORS - Function argument and type validation
132
- // ============================================================================
133
-
134
- /**
135
- * Function signatures for helpful error messages.
136
- * Maps function name to its parameter signature.
137
- * @type {Record<string, string>}
138
- */
139
- const FUNCTION_SIGNATURES = {
140
- // String functions
141
- UPPER: 'string',
142
- LOWER: 'string',
143
- LENGTH: 'string',
144
- TRIM: 'string',
145
- REPLACE: 'string, search, replacement',
146
- SUBSTRING: 'string, start[, length]',
147
- SUBSTR: 'string, start[, length]',
148
- CONCAT: 'value1, value2[, ...]',
149
-
150
- // Date/time functions
151
- RANDOM: '',
152
- RAND: '',
153
- CURRENT_DATE: '',
154
- CURRENT_TIME: '',
155
- CURRENT_TIMESTAMP: '',
156
-
157
- // JSON functions
158
- JSON_VALUE: 'expression, path',
159
- JSON_QUERY: 'expression, path',
160
- JSON_OBJECT: 'key1, value1[, ...]',
161
- JSON_ARRAYAGG: 'expression',
162
-
163
- // Aggregate functions
164
- COUNT: 'expression',
165
- SUM: 'expression',
166
- AVG: 'expression',
167
- MIN: 'expression',
168
- MAX: 'expression',
169
- }
170
-
171
- /**
172
- * Error for wrong number of function arguments.
173
- *
174
- * @param {string} funcName - The function name
175
- * @param {number | string} expected - Expected count (number or range like "2 or 3")
176
- * @param {number} received - Actual argument count
177
- * @returns {Error}
178
- */
179
- export function argCountError(funcName, expected, received) {
180
- const signature = FUNCTION_SIGNATURES[funcName] ?? ''
181
- let expectedStr = `${expected} arguments`
182
- if (expected === 0) expectedStr = 'no arguments'
183
- if (expected === 1) expectedStr = '1 argument'
184
- if (typeof expected === 'string' && expected.endsWith(' 1')) {
185
- expectedStr = `${expected} argument`
186
- }
187
-
188
- return new Error(`${funcName}(${signature}) function requires ${expectedStr}, got ${received}`)
189
- }
190
-
191
- /**
192
- * Error for invalid argument type or value.
193
- *
194
- * @param {Object} options
195
- * @param {string} options.funcName - The function name
196
- * @param {string} options.message - Specific error message
197
- * @param {string} [options.hint] - Recovery hint
198
- * @returns {Error}
199
- */
200
- export function argValueError({ funcName, message, hint }) {
201
- const signature = FUNCTION_SIGNATURES[funcName] ?? ''
202
- const suffix = hint ? `. ${hint}` : ''
203
- return new Error(`${funcName}(${signature}): ${message}${suffix}`)
204
- }
205
-
206
- /**
207
- * Error for aggregate function misuse (e.g., SUM(*)).
208
- *
209
- * @param {string} funcName - The aggregate function
210
- * @param {string} issue - What's wrong (e.g., "(*) is not supported")
211
- * @returns {Error}
212
- */
213
- export function aggregateError(funcName, issue) {
214
- return new Error(`${funcName}${issue}. Only COUNT supports *. Use a column name for ${funcName}.`)
215
- }
216
-
217
- /**
218
- * Error for unsupported CAST type.
219
- *
220
- * @param {string} toType - The unsupported target type
221
- * @param {string} [fromType] - The source type (optional)
222
- * @returns {Error}
223
- */
224
- export function castError(toType, fromType) {
225
- const message = fromType
226
- ? `Cannot CAST ${fromType} to ${toType}`
227
- : `Unsupported CAST to type ${toType}`
228
-
229
- return new Error(`${message}. Supported types: TEXT, VARCHAR, INTEGER, INT, BIGINT, FLOAT, REAL, DOUBLE, BOOLEAN`)
230
- }