squirreling 0.6.1 → 0.7.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 +14 -10
- package/package.json +1 -1
- package/src/execute/execute.js +58 -66
- package/src/execute/expression.js +37 -167
- package/src/execute/having.js +37 -31
- package/src/execute/join.js +28 -22
- package/src/execute/math.js +10 -209
- package/src/index.d.ts +3 -2
- package/src/parse/expression.js +19 -11
- package/src/parse/parse.js +4 -4
- package/src/parseErrors.js +29 -0
- package/src/types.d.ts +5 -0
- package/src/validation.js +121 -1
- package/src/validationErrors.js +1 -25
package/src/validation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, MathFunc, StringFunc} from './types.js'
|
|
3
|
+
* @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction} from './types.js'
|
|
4
4
|
* @param {string} name
|
|
5
5
|
* @returns {name is AggregateFunc}
|
|
6
6
|
*/
|
|
@@ -62,6 +62,126 @@ export function isBinaryOp(op) {
|
|
|
62
62
|
return ['AND', 'OR', 'LIKE', '=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Function argument count specifications.
|
|
67
|
+
* min: minimum number of arguments
|
|
68
|
+
* max: maximum number of arguments (null = unlimited)
|
|
69
|
+
* @type {Record<string, {min: number, max: number | null}>}
|
|
70
|
+
*/
|
|
71
|
+
export const FUNCTION_ARG_COUNTS = {
|
|
72
|
+
// String functions
|
|
73
|
+
UPPER: { min: 1, max: 1 },
|
|
74
|
+
LOWER: { min: 1, max: 1 },
|
|
75
|
+
LENGTH: { min: 1, max: 1 },
|
|
76
|
+
TRIM: { min: 1, max: 1 },
|
|
77
|
+
REPLACE: { min: 3, max: 3 },
|
|
78
|
+
SUBSTRING: { min: 2, max: 3 },
|
|
79
|
+
SUBSTR: { min: 2, max: 3 },
|
|
80
|
+
CONCAT: { min: 1, max: null },
|
|
81
|
+
|
|
82
|
+
// Date/time functions
|
|
83
|
+
RANDOM: { min: 0, max: 0 },
|
|
84
|
+
RAND: { min: 0, max: 0 },
|
|
85
|
+
CURRENT_DATE: { min: 0, max: 0 },
|
|
86
|
+
CURRENT_TIME: { min: 0, max: 0 },
|
|
87
|
+
CURRENT_TIMESTAMP: { min: 0, max: 0 },
|
|
88
|
+
|
|
89
|
+
// Math functions
|
|
90
|
+
FLOOR: { min: 1, max: 1 },
|
|
91
|
+
CEIL: { min: 1, max: 1 },
|
|
92
|
+
CEILING: { min: 1, max: 1 },
|
|
93
|
+
ABS: { min: 1, max: 1 },
|
|
94
|
+
MOD: { min: 2, max: 2 },
|
|
95
|
+
EXP: { min: 1, max: 1 },
|
|
96
|
+
LN: { min: 1, max: 1 },
|
|
97
|
+
LOG10: { min: 1, max: 1 },
|
|
98
|
+
POWER: { min: 2, max: 2 },
|
|
99
|
+
SQRT: { min: 1, max: 1 },
|
|
100
|
+
SIN: { min: 1, max: 1 },
|
|
101
|
+
COS: { min: 1, max: 1 },
|
|
102
|
+
TAN: { min: 1, max: 1 },
|
|
103
|
+
COT: { min: 1, max: 1 },
|
|
104
|
+
ASIN: { min: 1, max: 1 },
|
|
105
|
+
ACOS: { min: 1, max: 1 },
|
|
106
|
+
ATAN: { min: 1, max: 2 },
|
|
107
|
+
ATAN2: { min: 2, max: 2 },
|
|
108
|
+
DEGREES: { min: 1, max: 1 },
|
|
109
|
+
RADIANS: { min: 1, max: 1 },
|
|
110
|
+
PI: { min: 0, max: 0 },
|
|
111
|
+
|
|
112
|
+
// JSON functions
|
|
113
|
+
JSON_VALUE: { min: 2, max: 2 },
|
|
114
|
+
JSON_QUERY: { min: 2, max: 2 },
|
|
115
|
+
JSON_OBJECT: { min: 0, max: null },
|
|
116
|
+
JSON_ARRAYAGG: { min: 1, max: 1 },
|
|
117
|
+
|
|
118
|
+
// Aggregate functions
|
|
119
|
+
COUNT: { min: 1, max: 1 },
|
|
120
|
+
SUM: { min: 1, max: 1 },
|
|
121
|
+
AVG: { min: 1, max: 1 },
|
|
122
|
+
MIN: { min: 1, max: 1 },
|
|
123
|
+
MAX: { min: 1, max: 1 },
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format expected argument count for error messages.
|
|
128
|
+
* @param {number} min
|
|
129
|
+
* @param {number | null} max
|
|
130
|
+
* @returns {string | number}
|
|
131
|
+
*/
|
|
132
|
+
function formatExpected(min, max) {
|
|
133
|
+
if (max === null) return `at least ${min}`
|
|
134
|
+
if (min === max) return min
|
|
135
|
+
return `${min} or ${max}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validates function argument count.
|
|
140
|
+
* @param {string} funcName - The function name (uppercase)
|
|
141
|
+
* @param {number} argCount - Number of arguments provided
|
|
142
|
+
* @returns {{ valid: boolean, expected: string | number }}
|
|
143
|
+
*/
|
|
144
|
+
export function validateFunctionArgCount(funcName, argCount) {
|
|
145
|
+
const spec = FUNCTION_ARG_COUNTS[funcName]
|
|
146
|
+
if (!spec) return { valid: true, expected: 0 }
|
|
147
|
+
|
|
148
|
+
const { min, max } = spec
|
|
149
|
+
|
|
150
|
+
if (argCount < min) {
|
|
151
|
+
return { valid: false, expected: formatExpected(min, max) }
|
|
152
|
+
}
|
|
153
|
+
if (max !== null && argCount > max) {
|
|
154
|
+
return { valid: false, expected: formatExpected(min, max) }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { valid: true, expected: formatExpected(min, max) }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Checks if a function is known (either built-in or user-defined).
|
|
162
|
+
* @param {string} funcName - The function name (uppercase)
|
|
163
|
+
* @param {Record<string, UserDefinedFunction>} [functions] - User-defined functions
|
|
164
|
+
* @returns {boolean}
|
|
165
|
+
*/
|
|
166
|
+
export function isKnownFunction(funcName, functions) {
|
|
167
|
+
// Check built-in functions
|
|
168
|
+
if (isAggregateFunc(funcName) || isMathFunc(funcName) || isStringFunc(funcName)) {
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Special case: CAST is not in any function list but is a built-in
|
|
173
|
+
if (funcName === 'CAST') {
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check user-defined functions (case-insensitive)
|
|
178
|
+
if (functions) {
|
|
179
|
+
return Object.keys(functions).some(k => k.toUpperCase() === funcName)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
65
185
|
// Keywords that cannot be used as implicit aliases after a column
|
|
66
186
|
export const RESERVED_AFTER_COLUMN = new Set([
|
|
67
187
|
'FROM', 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
package/src/validationErrors.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ExecutionError } from './executionErrors.js'
|
|
|
9
9
|
* Maps function name to its parameter signature.
|
|
10
10
|
* @type {Record<string, string>}
|
|
11
11
|
*/
|
|
12
|
-
const FUNCTION_SIGNATURES = {
|
|
12
|
+
export const FUNCTION_SIGNATURES = {
|
|
13
13
|
// String functions
|
|
14
14
|
UPPER: 'string',
|
|
15
15
|
LOWER: 'string',
|
|
@@ -64,30 +64,6 @@ const FUNCTION_SIGNATURES = {
|
|
|
64
64
|
MAX: 'expression',
|
|
65
65
|
}
|
|
66
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
67
|
/**
|
|
92
68
|
* Error for invalid argument type or value.
|
|
93
69
|
*
|