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.
- package/README.md +5 -1
- package/package.json +3 -3
- package/src/backend/dataSource.js +15 -14
- package/src/execute/aggregates.js +10 -4
- package/src/execute/execute.js +58 -40
- package/src/execute/expression.js +212 -54
- package/src/execute/having.js +7 -2
- package/src/execute/join.js +35 -34
- package/src/execute/math.js +340 -0
- package/src/execute/utils.js +2 -2
- package/src/executionErrors.js +63 -0
- package/src/index.js +1 -0
- package/src/parse/comparison.js +41 -8
- package/src/parse/expression.js +53 -13
- package/src/parse/state.js +13 -3
- package/src/parse/tokenize.js +34 -21
- package/src/parseErrors.js +118 -0
- package/src/types.d.ts +55 -16
- package/src/validation.js +14 -1
- package/src/validationErrors.js +138 -0
- package/src/errors.js +0 -230
|
@@ -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
|
-
|
|
23
|
+
scan(hints?: QueryHints): AsyncIterable<AsyncRow>
|
|
19
24
|
}
|
|
20
|
-
export
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|