squirreling 0.4.7 → 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.
@@ -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
  }
@@ -27,7 +32,15 @@ export interface ExecuteSqlOptions {
27
32
  query: string
28
33
  }
29
34
 
30
- export type SqlPrimitive = string | number | bigint | boolean | SqlPrimitive[] | Record<string, any> | null
35
+ export type SqlPrimitive =
36
+ | string
37
+ | number
38
+ | bigint
39
+ | boolean
40
+ | Date
41
+ | null
42
+ | SqlPrimitive[]
43
+ | Record<string, any>
31
44
 
32
45
  export interface SelectStatement {
33
46
  distinct: boolean
@@ -60,54 +73,59 @@ export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
60
73
 
61
74
  export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
62
75
 
63
- export interface LiteralNode {
76
+ export interface ExprNodeBase {
77
+ positionStart: number
78
+ positionEnd: number
79
+ }
80
+
81
+ export interface LiteralNode extends ExprNodeBase {
64
82
  type: 'literal'
65
83
  value: SqlPrimitive
66
84
  }
67
85
 
68
- export interface IdentifierNode {
86
+ export interface IdentifierNode extends ExprNodeBase {
69
87
  type: 'identifier'
70
88
  name: string
71
89
  }
72
90
 
73
- export interface UnaryNode {
91
+ export interface UnaryNode extends ExprNodeBase {
74
92
  type: 'unary'
75
93
  op: 'NOT' | 'IS NULL' | 'IS NOT NULL' | '-'
76
94
  argument: ExprNode
77
95
  }
78
96
 
79
- export interface BinaryNode {
97
+ export interface BinaryNode extends ExprNodeBase {
80
98
  type: 'binary'
81
99
  op: BinaryOp
82
100
  left: ExprNode
83
101
  right: ExprNode
84
102
  }
85
103
 
86
- export interface FunctionNode {
104
+ export interface FunctionNode extends ExprNodeBase {
87
105
  type: 'function'
88
106
  name: string
89
107
  args: ExprNode[]
90
108
  }
91
109
 
92
- export interface CastNode {
110
+ export interface CastNode extends ExprNodeBase {
93
111
  type: 'cast'
94
112
  expr: ExprNode
95
113
  toType: string
96
114
  }
97
115
 
98
- export interface InSubqueryNode {
116
+ export interface InSubqueryNode extends ExprNodeBase {
99
117
  type: 'in'
100
118
  expr: ExprNode
101
119
  subquery: SelectStatement
102
120
  }
103
121
 
104
- export interface InValuesNode {
122
+ export interface InValuesNode extends ExprNodeBase {
105
123
  type: 'in valuelist'
106
124
  expr: ExprNode
107
125
  values: ExprNode[]
108
126
  }
109
127
 
110
- export interface ExistsNode {
128
+ export interface ExistsNode extends ExprNodeBase {
111
129
  type: 'exists' | 'not exists'
112
130
  subquery: SelectStatement
113
131
  }
@@ -117,18 +135,26 @@ export interface WhenClause {
117
135
  result: ExprNode
118
136
  }
119
137
 
120
- export interface CaseNode {
138
+ export interface CaseNode extends ExprNodeBase {
121
139
  type: 'case'
122
140
  caseExpr?: ExprNode
123
141
  whenClauses: WhenClause[]
124
142
  elseResult?: ExprNode
125
143
  }
126
144
 
127
- export interface SubqueryNode {
145
+ export interface SubqueryNode extends ExprNodeBase {
128
146
  type: 'subquery'
129
147
  subquery: SelectStatement
130
148
  }
131
149
 
150
+ export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
151
+
152
+ export interface IntervalNode extends ExprNodeBase {
153
+ type: 'interval'
154
+ value: number
155
+ unit: IntervalUnit
156
+ }
157
+
132
158
  export type ExprNode =
133
159
  | LiteralNode
134
160
  | IdentifierNode
@@ -141,6 +167,7 @@ export type ExprNode =
141
167
  | ExistsNode
142
168
  | CaseNode
143
169
  | SubqueryNode
170
+ | IntervalNode
144
171
 
145
172
  export interface StarColumn {
146
173
  kind: 'star'
@@ -150,6 +177,18 @@ export interface StarColumn {
150
177
 
151
178
  export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG'
152
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
+
153
192
  export type StringFunc =
154
193
  | 'UPPER'
155
194
  | 'LOWER'
@@ -162,6 +201,9 @@ export type StringFunc =
162
201
  | 'JSON_VALUE'
163
202
  | 'JSON_QUERY'
164
203
  | 'JSON_OBJECT'
204
+ | 'CURRENT_DATE'
205
+ | 'CURRENT_TIME'
206
+ | 'CURRENT_TIMESTAMP'
165
207
 
166
208
  export interface AggregateArgStar {
167
209
  kind: 'star'
@@ -208,6 +250,7 @@ export interface JoinClause {
208
250
  export interface ParserState {
209
251
  tokens: Token[]
210
252
  pos: number
253
+ lastPos?: number
211
254
  }
212
255
 
213
256
  // Tokenizer types
@@ -226,7 +269,8 @@ export type TokenType =
226
269
  export interface Token {
227
270
  type: TokenType
228
271
  value: string
229
- position: number
230
- numericValue?: number
272
+ positionStart: number
273
+ positionEnd: number
274
+ numericValue?: number | bigint
231
275
  originalValue?: string
232
276
  }
package/src/validation.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * @import {AggregateFunc, BinaryOp, ComparisonOp, 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,25 @@ 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
+
22
+ /**
23
+ * @param {string} name
24
+ * @returns {name is IntervalUnit}
25
+ */
26
+ export function isIntervalUnit(name) {
27
+ return ['DAY', 'MONTH', 'YEAR', 'HOUR', 'MINUTE', 'SECOND'].includes(name)
28
+ }
29
+
11
30
  /**
12
31
  * @param {string} name
13
32
  * @returns {name is StringFunc}
@@ -27,6 +46,9 @@ export function isStringFunc(name) {
27
46
  'JSON_VALUE',
28
47
  'JSON_QUERY',
29
48
  'JSON_OBJECT',
49
+ 'CURRENT_DATE',
50
+ 'CURRENT_TIME',
51
+ 'CURRENT_TIMESTAMP',
30
52
  ].includes(name)
31
53
  }
32
54
 
@@ -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
+ }