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/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',
@@ -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
  *