squirreling 0.4.8 → 0.5.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/package.json +3 -3
- package/src/execute/aggregates.js +10 -4
- package/src/execute/execute.js +16 -10
- package/src/execute/expression.js +202 -38
- package/src/execute/having.js +7 -2
- package/src/execute/join.js +5 -4
- package/src/execute/math.js +165 -0
- package/src/executionErrors.js +62 -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 +117 -0
- package/src/types.d.ts +37 -13
- package/src/validation.js +12 -1
- package/src/validationErrors.js +127 -0
- package/src/errors.js +0 -230
package/src/parse/state.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { syntaxError } from '../
|
|
1
|
+
import { syntaxError } from '../parseErrors.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @import { ParserState, Token, TokenType } from '../types.js'
|
|
@@ -31,12 +31,22 @@ export function peekToken(state, offset) {
|
|
|
31
31
|
*/
|
|
32
32
|
export function consume(state) {
|
|
33
33
|
const tok = current(state)
|
|
34
|
+
state.lastPos = tok.positionEnd
|
|
34
35
|
if (state.pos < state.tokens.length - 1) {
|
|
35
36
|
state.pos += 1
|
|
36
37
|
}
|
|
37
38
|
return tok
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Gets the position after the last consumed token.
|
|
43
|
+
* @param {ParserState} state
|
|
44
|
+
* @returns {number}
|
|
45
|
+
*/
|
|
46
|
+
export function lastPosition(state) {
|
|
47
|
+
return state.lastPos ?? 0
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
/**
|
|
41
51
|
* @param {ParserState} state
|
|
42
52
|
* @param {TokenType} type
|
|
@@ -83,12 +93,12 @@ export function expectIdentifier(state) {
|
|
|
83
93
|
* Helper function to create consistent parser error messages.
|
|
84
94
|
* @param {ParserState} state
|
|
85
95
|
* @param {string} expected - Description of what was expected
|
|
86
|
-
* @returns {
|
|
96
|
+
* @returns {import('../parseErrors.js').ParseError}
|
|
87
97
|
*/
|
|
88
98
|
export function parseError(state, expected) {
|
|
89
99
|
const tok = current(state)
|
|
90
100
|
const prevToken = state.tokens[state.pos - 1]
|
|
91
101
|
const after = prevToken ? prevToken.originalValue ?? prevToken.value : undefined
|
|
92
102
|
const received = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
|
|
93
|
-
return syntaxError({ expected, received,
|
|
103
|
+
return syntaxError({ expected, received, positionStart: tok.positionStart, positionEnd: tok.positionEnd, after })
|
|
94
104
|
}
|
package/src/parse/tokenize.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
invalidLiteralError,
|
|
3
3
|
unexpectedCharError,
|
|
4
4
|
unterminatedError,
|
|
5
|
-
} from '../
|
|
5
|
+
} from '../parseErrors.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @import { Token } from '../types.d.ts'
|
|
@@ -117,24 +117,26 @@ export function tokenize(sql) {
|
|
|
117
117
|
return {
|
|
118
118
|
type: 'number',
|
|
119
119
|
value: text,
|
|
120
|
-
|
|
120
|
+
positionStart: startPos,
|
|
121
|
+
positionEnd: i,
|
|
121
122
|
numericValue: BigInt(text.slice(0, -1)),
|
|
122
123
|
}
|
|
123
124
|
} catch {
|
|
124
|
-
throw invalidLiteralError({ type: 'bigint', value: text.slice(0, -1),
|
|
125
|
+
throw invalidLiteralError({ type: 'bigint', value: text.slice(0, -1), positionStart: startPos, positionEnd: i })
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
if (isAlpha(peek())) {
|
|
128
|
-
throw invalidLiteralError({ type: 'number', value: text + peek(),
|
|
129
|
+
throw invalidLiteralError({ type: 'number', value: text + peek(), positionStart: startPos, positionEnd: i + 1 })
|
|
129
130
|
}
|
|
130
131
|
const num = parseFloat(text)
|
|
131
132
|
if (isNaN(num)) {
|
|
132
|
-
throw invalidLiteralError({ type: 'number', value: text,
|
|
133
|
+
throw invalidLiteralError({ type: 'number', value: text, positionStart: startPos, positionEnd: i })
|
|
133
134
|
}
|
|
134
135
|
return {
|
|
135
136
|
type: 'number',
|
|
136
137
|
value: text,
|
|
137
|
-
|
|
138
|
+
positionStart: startPos,
|
|
139
|
+
positionEnd: i,
|
|
138
140
|
numericValue: num,
|
|
139
141
|
}
|
|
140
142
|
}
|
|
@@ -204,13 +206,15 @@ export function tokenize(sql) {
|
|
|
204
206
|
type: 'keyword',
|
|
205
207
|
value: upper,
|
|
206
208
|
originalValue: text,
|
|
207
|
-
|
|
209
|
+
positionStart: pos,
|
|
210
|
+
positionEnd: i,
|
|
208
211
|
})
|
|
209
212
|
} else {
|
|
210
213
|
tokens.push({
|
|
211
214
|
type: 'identifier',
|
|
212
215
|
value: text,
|
|
213
|
-
|
|
216
|
+
positionStart: pos,
|
|
217
|
+
positionEnd: i,
|
|
214
218
|
})
|
|
215
219
|
}
|
|
216
220
|
continue
|
|
@@ -222,7 +226,7 @@ export function tokenize(sql) {
|
|
|
222
226
|
let text = ''
|
|
223
227
|
while (i <= length) {
|
|
224
228
|
if (i === length) {
|
|
225
|
-
throw unterminatedError('string', pos)
|
|
229
|
+
throw unterminatedError('string', pos, length)
|
|
226
230
|
}
|
|
227
231
|
const c = nextChar()
|
|
228
232
|
if (c === quote) {
|
|
@@ -239,7 +243,8 @@ export function tokenize(sql) {
|
|
|
239
243
|
tokens.push({
|
|
240
244
|
type: 'string',
|
|
241
245
|
value: text,
|
|
242
|
-
|
|
246
|
+
positionStart: pos,
|
|
247
|
+
positionEnd: i,
|
|
243
248
|
})
|
|
244
249
|
continue
|
|
245
250
|
}
|
|
@@ -250,7 +255,7 @@ export function tokenize(sql) {
|
|
|
250
255
|
let text = ''
|
|
251
256
|
while (i <= length) {
|
|
252
257
|
if (i === length) {
|
|
253
|
-
throw unterminatedError('identifier', pos)
|
|
258
|
+
throw unterminatedError('identifier', pos, length)
|
|
254
259
|
}
|
|
255
260
|
const c = nextChar()
|
|
256
261
|
if (c === quote) {
|
|
@@ -267,7 +272,8 @@ export function tokenize(sql) {
|
|
|
267
272
|
tokens.push({
|
|
268
273
|
type: 'identifier',
|
|
269
274
|
value: text,
|
|
270
|
-
|
|
275
|
+
positionStart: pos,
|
|
276
|
+
positionEnd: i,
|
|
271
277
|
})
|
|
272
278
|
continue
|
|
273
279
|
}
|
|
@@ -283,7 +289,8 @@ export function tokenize(sql) {
|
|
|
283
289
|
tokens.push({
|
|
284
290
|
type: 'operator',
|
|
285
291
|
value: op,
|
|
286
|
-
|
|
292
|
+
positionStart: pos,
|
|
293
|
+
positionEnd: i,
|
|
287
294
|
})
|
|
288
295
|
continue
|
|
289
296
|
}
|
|
@@ -294,7 +301,8 @@ export function tokenize(sql) {
|
|
|
294
301
|
tokens.push({
|
|
295
302
|
type: 'operator',
|
|
296
303
|
value: ch,
|
|
297
|
-
|
|
304
|
+
positionStart: pos,
|
|
305
|
+
positionEnd: i,
|
|
298
306
|
})
|
|
299
307
|
continue
|
|
300
308
|
}
|
|
@@ -304,7 +312,8 @@ export function tokenize(sql) {
|
|
|
304
312
|
tokens.push({
|
|
305
313
|
type: 'comma',
|
|
306
314
|
value: ',',
|
|
307
|
-
|
|
315
|
+
positionStart: pos,
|
|
316
|
+
positionEnd: i,
|
|
308
317
|
})
|
|
309
318
|
continue
|
|
310
319
|
}
|
|
@@ -314,7 +323,8 @@ export function tokenize(sql) {
|
|
|
314
323
|
tokens.push({
|
|
315
324
|
type: 'dot',
|
|
316
325
|
value: '.',
|
|
317
|
-
|
|
326
|
+
positionStart: pos,
|
|
327
|
+
positionEnd: i,
|
|
318
328
|
})
|
|
319
329
|
continue
|
|
320
330
|
}
|
|
@@ -324,7 +334,8 @@ export function tokenize(sql) {
|
|
|
324
334
|
tokens.push({
|
|
325
335
|
type: 'paren',
|
|
326
336
|
value: ch,
|
|
327
|
-
|
|
337
|
+
positionStart: pos,
|
|
338
|
+
positionEnd: i,
|
|
328
339
|
})
|
|
329
340
|
continue
|
|
330
341
|
}
|
|
@@ -334,21 +345,23 @@ export function tokenize(sql) {
|
|
|
334
345
|
tokens.push({
|
|
335
346
|
type: 'semicolon',
|
|
336
347
|
value: ';',
|
|
337
|
-
|
|
348
|
+
positionStart: pos,
|
|
349
|
+
positionEnd: i,
|
|
338
350
|
})
|
|
339
351
|
continue
|
|
340
352
|
}
|
|
341
353
|
|
|
342
354
|
if (tokens.length === 0) {
|
|
343
|
-
throw unexpectedCharError(ch, pos, true)
|
|
355
|
+
throw unexpectedCharError({ char: ch, positionStart: pos, expectsSelect: true })
|
|
344
356
|
}
|
|
345
|
-
throw unexpectedCharError(ch, pos)
|
|
357
|
+
throw unexpectedCharError({ char: ch, positionStart: pos })
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
tokens.push({
|
|
349
361
|
type: 'eof',
|
|
350
362
|
value: '',
|
|
351
|
-
|
|
363
|
+
positionStart: length,
|
|
364
|
+
positionEnd: length,
|
|
352
365
|
})
|
|
353
366
|
|
|
354
367
|
return tokens
|
|
@@ -0,0 +1,117 @@
|
|
|
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 {string} message - Human-readable error message
|
|
11
|
+
* @param {number} positionStart - Start position (0-based character offset)
|
|
12
|
+
* @param {number} positionEnd - End position (exclusive, 0-based character offset)
|
|
13
|
+
*/
|
|
14
|
+
constructor(message, positionStart, positionEnd) {
|
|
15
|
+
super(message)
|
|
16
|
+
this.name = 'ParseError'
|
|
17
|
+
this.positionStart = positionStart
|
|
18
|
+
this.positionEnd = positionEnd
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* General syntax error for unexpected tokens.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {string} options.expected - Description of what was expected
|
|
27
|
+
* @param {string} options.received - What was actually found
|
|
28
|
+
* @param {number} options.positionStart - Start character position in query
|
|
29
|
+
* @param {number} options.positionEnd - End character position in query
|
|
30
|
+
* @param {string} [options.after] - What token came before (for context)
|
|
31
|
+
* @returns {ParseError}
|
|
32
|
+
*/
|
|
33
|
+
export function syntaxError({ expected, received, positionStart, positionEnd, after }) {
|
|
34
|
+
const afterClause = after ? ` after "${after}"` : ''
|
|
35
|
+
return new ParseError(`Expected ${expected}${afterClause} but found ${received} at position ${positionStart}`, positionStart, positionEnd)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Error for unterminated literals (strings, identifiers).
|
|
40
|
+
*
|
|
41
|
+
* @param {'string' | 'identifier'} type - Type of unterminated literal
|
|
42
|
+
* @param {number} positionStart - Starting position
|
|
43
|
+
* @param {number} positionEnd - End position
|
|
44
|
+
* @returns {ParseError}
|
|
45
|
+
*/
|
|
46
|
+
export function unterminatedError(type, positionStart, positionEnd) {
|
|
47
|
+
const name = type === 'string' ? 'string literal' : 'identifier'
|
|
48
|
+
return new ParseError(`Unterminated ${name} starting at position ${positionStart}`, positionStart, positionEnd)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Error for invalid literals (numbers, intervals, etc).
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @param {string} options.type - Type of invalid literal (e.g., 'number', 'interval value', 'interval unit')
|
|
56
|
+
* @param {string} options.value - The invalid value
|
|
57
|
+
* @param {number} options.positionStart - Start position in query
|
|
58
|
+
* @param {number} options.positionEnd - End position in query
|
|
59
|
+
* @param {string} [options.validValues] - List of valid values (for enums like interval units)
|
|
60
|
+
* @returns {ParseError}
|
|
61
|
+
*/
|
|
62
|
+
export function invalidLiteralError({ type, value, positionStart, positionEnd, validValues }) {
|
|
63
|
+
const suffix = validValues ? `. Valid values: ${validValues}` : ''
|
|
64
|
+
return new ParseError(`Invalid ${type} ${value} at position ${positionStart}${suffix}`, positionStart, positionEnd)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Error for unexpected characters during tokenization.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} options
|
|
71
|
+
* @param {string} options.char - The unexpected character
|
|
72
|
+
* @param {number} options.positionStart - Position in query
|
|
73
|
+
* @param {boolean} [options.expectsSelect=false] - Whether SELECT was expected (first token)
|
|
74
|
+
* @returns {ParseError}
|
|
75
|
+
*/
|
|
76
|
+
export function unexpectedCharError({ char, positionStart, expectsSelect = false }) {
|
|
77
|
+
const positionEnd = positionStart + 1
|
|
78
|
+
if (expectsSelect) {
|
|
79
|
+
return new ParseError(`Expected SELECT but found "${char}" at position ${positionStart}. Queries must start with SELECT.`, positionStart, positionEnd)
|
|
80
|
+
}
|
|
81
|
+
return new ParseError(`Unexpected character "${char}" at position ${positionStart}`, positionStart, positionEnd)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Error for unknown/unsupported functions.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} options
|
|
88
|
+
* @param {string} options.funcName - The unknown function name
|
|
89
|
+
* @param {number} options.positionStart - Start position in query
|
|
90
|
+
* @param {number} options.positionEnd - End position in query
|
|
91
|
+
* @param {string} [options.validFunctions] - List of valid functions
|
|
92
|
+
* @returns {ParseError}
|
|
93
|
+
*/
|
|
94
|
+
export function unknownFunctionError({ funcName, positionStart, positionEnd, validFunctions }) {
|
|
95
|
+
const supported = validFunctions ||
|
|
96
|
+
'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'
|
|
97
|
+
|
|
98
|
+
return new ParseError(
|
|
99
|
+
`Unknown function "${funcName}" at position ${positionStart}. Supported: ${supported}`,
|
|
100
|
+
positionStart,
|
|
101
|
+
positionEnd
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Error for missing required clause or structure.
|
|
107
|
+
*
|
|
108
|
+
* @param {Object} options
|
|
109
|
+
* @param {string} options.missing - What is missing (e.g., 'WHEN clause', 'FROM clause', 'ON condition')
|
|
110
|
+
* @param {string} options.context - Where it's missing from (e.g., 'CASE expression', 'SELECT statement', 'JOIN')
|
|
111
|
+
* @param {number} [options.positionStart] - Start position in query
|
|
112
|
+
* @param {number} [options.positionEnd] - End position in query
|
|
113
|
+
* @returns {ParseError}
|
|
114
|
+
*/
|
|
115
|
+
export function missingClauseError({ missing, context, positionStart, positionEnd }) {
|
|
116
|
+
return new ParseError(`${context} requires ${missing}`, positionStart ?? 0, positionEnd ?? 0)
|
|
117
|
+
}
|
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
|
}
|
|
@@ -68,54 +73,59 @@ export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
|
68
73
|
|
|
69
74
|
export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
70
75
|
|
|
71
|
-
export interface
|
|
76
|
+
export interface ExprNodeBase {
|
|
77
|
+
positionStart: number
|
|
78
|
+
positionEnd: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface LiteralNode extends ExprNodeBase {
|
|
72
82
|
type: 'literal'
|
|
73
83
|
value: SqlPrimitive
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
export interface IdentifierNode {
|
|
86
|
+
export interface IdentifierNode extends ExprNodeBase {
|
|
77
87
|
type: 'identifier'
|
|
78
88
|
name: string
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
export interface UnaryNode {
|
|
91
|
+
export interface UnaryNode extends ExprNodeBase {
|
|
82
92
|
type: 'unary'
|
|
83
93
|
op: 'NOT' | 'IS NULL' | 'IS NOT NULL' | '-'
|
|
84
94
|
argument: ExprNode
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
export interface BinaryNode {
|
|
97
|
+
export interface BinaryNode extends ExprNodeBase {
|
|
88
98
|
type: 'binary'
|
|
89
99
|
op: BinaryOp
|
|
90
100
|
left: ExprNode
|
|
91
101
|
right: ExprNode
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
export interface FunctionNode {
|
|
104
|
+
export interface FunctionNode extends ExprNodeBase {
|
|
95
105
|
type: 'function'
|
|
96
106
|
name: string
|
|
97
107
|
args: ExprNode[]
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
export interface CastNode {
|
|
110
|
+
export interface CastNode extends ExprNodeBase {
|
|
101
111
|
type: 'cast'
|
|
102
112
|
expr: ExprNode
|
|
103
113
|
toType: string
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
export interface InSubqueryNode {
|
|
116
|
+
export interface InSubqueryNode extends ExprNodeBase {
|
|
107
117
|
type: 'in'
|
|
108
118
|
expr: ExprNode
|
|
109
119
|
subquery: SelectStatement
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
export interface InValuesNode {
|
|
122
|
+
export interface InValuesNode extends ExprNodeBase {
|
|
113
123
|
type: 'in valuelist'
|
|
114
124
|
expr: ExprNode
|
|
115
125
|
values: ExprNode[]
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
export interface ExistsNode {
|
|
128
|
+
export interface ExistsNode extends ExprNodeBase {
|
|
119
129
|
type: 'exists' | 'not exists'
|
|
120
130
|
subquery: SelectStatement
|
|
121
131
|
}
|
|
@@ -125,21 +135,21 @@ export interface WhenClause {
|
|
|
125
135
|
result: ExprNode
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
export interface CaseNode {
|
|
138
|
+
export interface CaseNode extends ExprNodeBase {
|
|
129
139
|
type: 'case'
|
|
130
140
|
caseExpr?: ExprNode
|
|
131
141
|
whenClauses: WhenClause[]
|
|
132
142
|
elseResult?: ExprNode
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
export interface SubqueryNode {
|
|
145
|
+
export interface SubqueryNode extends ExprNodeBase {
|
|
136
146
|
type: 'subquery'
|
|
137
147
|
subquery: SelectStatement
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
|
|
141
151
|
|
|
142
|
-
export interface IntervalNode {
|
|
152
|
+
export interface IntervalNode extends ExprNodeBase {
|
|
143
153
|
type: 'interval'
|
|
144
154
|
value: number
|
|
145
155
|
unit: IntervalUnit
|
|
@@ -167,6 +177,18 @@ export interface StarColumn {
|
|
|
167
177
|
|
|
168
178
|
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG'
|
|
169
179
|
|
|
180
|
+
export type MathFunc =
|
|
181
|
+
| 'FLOOR'
|
|
182
|
+
| 'CEIL'
|
|
183
|
+
| 'CEILING'
|
|
184
|
+
| 'ABS'
|
|
185
|
+
| 'MOD'
|
|
186
|
+
| 'EXP'
|
|
187
|
+
| 'LN'
|
|
188
|
+
| 'LOG10'
|
|
189
|
+
| 'POWER'
|
|
190
|
+
| 'SQRT'
|
|
191
|
+
|
|
170
192
|
export type StringFunc =
|
|
171
193
|
| 'UPPER'
|
|
172
194
|
| 'LOWER'
|
|
@@ -228,6 +250,7 @@ export interface JoinClause {
|
|
|
228
250
|
export interface ParserState {
|
|
229
251
|
tokens: Token[]
|
|
230
252
|
pos: number
|
|
253
|
+
lastPos?: number
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
// Tokenizer types
|
|
@@ -246,7 +269,8 @@ export type TokenType =
|
|
|
246
269
|
export interface Token {
|
|
247
270
|
type: TokenType
|
|
248
271
|
value: string
|
|
249
|
-
|
|
272
|
+
positionStart: number
|
|
273
|
+
positionEnd: number
|
|
250
274
|
numericValue?: number | bigint
|
|
251
275
|
originalValue?: string
|
|
252
276
|
}
|
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,17 @@ 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
|
+
].includes(name)
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* @param {string} name
|
|
13
24
|
* @returns {name is IntervalUnit}
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
|
|
42
|
+
// JSON functions
|
|
43
|
+
JSON_VALUE: 'expression, path',
|
|
44
|
+
JSON_QUERY: 'expression, path',
|
|
45
|
+
JSON_OBJECT: 'key1, value1[, ...]',
|
|
46
|
+
JSON_ARRAYAGG: 'expression',
|
|
47
|
+
|
|
48
|
+
// Aggregate functions
|
|
49
|
+
COUNT: 'expression',
|
|
50
|
+
SUM: 'expression',
|
|
51
|
+
AVG: 'expression',
|
|
52
|
+
MIN: 'expression',
|
|
53
|
+
MAX: 'expression',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Error for wrong number of function arguments.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} options
|
|
60
|
+
* @param {string} options.funcName - The function name
|
|
61
|
+
* @param {number | string} options.expected - Expected count (number or range like "2 or 3")
|
|
62
|
+
* @param {number} options.received - Actual argument count
|
|
63
|
+
* @param {number} options.positionStart - Start position in query
|
|
64
|
+
* @param {number} options.positionEnd - End position in query
|
|
65
|
+
* @param {number} [options.rowNumber] - 1-based row number where error occurred
|
|
66
|
+
* @returns {ExecutionError}
|
|
67
|
+
*/
|
|
68
|
+
export function argCountError({ funcName, expected, received, positionStart, positionEnd, rowNumber }) {
|
|
69
|
+
const signature = FUNCTION_SIGNATURES[funcName] ?? ''
|
|
70
|
+
let expectedStr = `${expected} arguments`
|
|
71
|
+
if (expected === 0) expectedStr = 'no arguments'
|
|
72
|
+
if (expected === 1) expectedStr = '1 argument'
|
|
73
|
+
if (typeof expected === 'string' && expected.endsWith(' 1')) {
|
|
74
|
+
expectedStr = `${expected} argument`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new ExecutionError(`${funcName}(${signature}) function requires ${expectedStr}, got ${received}`, positionStart, positionEnd, rowNumber)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Error for invalid argument type or value.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} options
|
|
84
|
+
* @param {string} options.funcName - The function name
|
|
85
|
+
* @param {string} options.message - Specific error message
|
|
86
|
+
* @param {number} options.positionStart - Start position in query
|
|
87
|
+
* @param {number} options.positionEnd - End position in query
|
|
88
|
+
* @param {string} [options.hint] - Recovery hint
|
|
89
|
+
* @param {number} [options.rowNumber] - 1-based row number where error occurred
|
|
90
|
+
* @returns {ExecutionError}
|
|
91
|
+
*/
|
|
92
|
+
export function argValueError({ funcName, message, positionStart, positionEnd, hint, rowNumber }) {
|
|
93
|
+
const signature = FUNCTION_SIGNATURES[funcName] ?? ''
|
|
94
|
+
const suffix = hint ? `. ${hint}` : ''
|
|
95
|
+
return new ExecutionError(`${funcName}(${signature}): ${message}${suffix}`, positionStart, positionEnd, rowNumber)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Error for aggregate function misuse (e.g., SUM(*)).
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} options
|
|
102
|
+
* @param {string} options.funcName - The aggregate function
|
|
103
|
+
* @param {string} options.issue - What's wrong (e.g., "(*) is not supported")
|
|
104
|
+
* @returns {Error}
|
|
105
|
+
*/
|
|
106
|
+
export function aggregateError({ funcName, issue }) {
|
|
107
|
+
return new Error(`${funcName}${issue}. Only COUNT supports *. Use a column name for ${funcName}.`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Error for unsupported CAST type.
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} options
|
|
114
|
+
* @param {string} options.toType - The unsupported target type
|
|
115
|
+
* @param {number} options.positionStart - Start position in query
|
|
116
|
+
* @param {number} options.positionEnd - End position in query
|
|
117
|
+
* @param {string} [options.fromType] - The source type (optional)
|
|
118
|
+
* @param {number} [options.rowNumber] - 1-based row number where error occurred
|
|
119
|
+
* @returns {ExecutionError}
|
|
120
|
+
*/
|
|
121
|
+
export function castError({ toType, positionStart, positionEnd, fromType, rowNumber }) {
|
|
122
|
+
const message = fromType
|
|
123
|
+
? `Cannot CAST ${fromType} to ${toType}`
|
|
124
|
+
: `Unsupported CAST to type ${toType}`
|
|
125
|
+
|
|
126
|
+
return new ExecutionError(`${message}. Supported types: TEXT, VARCHAR, INTEGER, INT, BIGINT, FLOAT, REAL, DOUBLE, BOOLEAN`, positionStart, positionEnd, rowNumber)
|
|
127
|
+
}
|