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,165 @@
1
+ /**
2
+ * @import { SqlPrimitive } from '../types.js'
3
+ */
4
+ import { argCountError } from '../validationErrors.js'
5
+
6
+ /**
7
+ * Evaluate a math function
8
+ *
9
+ * @param {Object} options
10
+ * @param {string} options.funcName - Uppercase function name
11
+ * @param {SqlPrimitive[]} options.args - Function arguments
12
+ * @param {number} options.positionStart - Start position in query
13
+ * @param {number} options.positionEnd - End position in query
14
+ * @param {number} [options.rowNumber] - 1-based row number for error reporting
15
+ * @returns {SqlPrimitive} Result
16
+ */
17
+ export function evaluateMathFunc({ funcName, args, positionStart, positionEnd, rowNumber }) {
18
+ if (funcName === 'FLOOR') {
19
+ if (args.length !== 1) {
20
+ throw argCountError({
21
+ funcName: 'FLOOR',
22
+ expected: 1,
23
+ received: args.length,
24
+ positionStart,
25
+ positionEnd,
26
+ rowNumber,
27
+ })
28
+ }
29
+ const val = args[0]
30
+ if (val == null) return null
31
+ return Math.floor(Number(val))
32
+ }
33
+
34
+ if (funcName === 'CEIL' || funcName === 'CEILING') {
35
+ if (args.length !== 1) {
36
+ throw argCountError({
37
+ funcName,
38
+ expected: 1,
39
+ received: args.length,
40
+ positionStart,
41
+ positionEnd,
42
+ rowNumber,
43
+ })
44
+ }
45
+ const val = args[0]
46
+ if (val == null) return null
47
+ return Math.ceil(Number(val))
48
+ }
49
+
50
+ if (funcName === 'ABS') {
51
+ if (args.length !== 1) {
52
+ throw argCountError({
53
+ funcName: 'ABS',
54
+ expected: 1,
55
+ received: args.length,
56
+ positionStart,
57
+ positionEnd,
58
+ rowNumber,
59
+ })
60
+ }
61
+ const val = args[0]
62
+ if (val == null) return null
63
+ return Math.abs(Number(val))
64
+ }
65
+
66
+ if (funcName === 'MOD') {
67
+ if (args.length !== 2) {
68
+ throw argCountError({
69
+ funcName: 'MOD',
70
+ expected: 2,
71
+ received: args.length,
72
+ positionStart,
73
+ positionEnd,
74
+ rowNumber,
75
+ })
76
+ }
77
+ const dividend = args[0]
78
+ const divisor = args[1]
79
+ if (dividend == null || divisor == null) return null
80
+ return Number(dividend) % Number(divisor)
81
+ }
82
+
83
+ if (funcName === 'EXP') {
84
+ if (args.length !== 1) {
85
+ throw argCountError({
86
+ funcName: 'EXP',
87
+ expected: 1,
88
+ received: args.length,
89
+ positionStart,
90
+ positionEnd,
91
+ rowNumber,
92
+ })
93
+ }
94
+ const val = args[0]
95
+ if (val == null) return null
96
+ return Math.exp(Number(val))
97
+ }
98
+
99
+ if (funcName === 'LN') {
100
+ if (args.length !== 1) {
101
+ throw argCountError({
102
+ funcName: 'LN',
103
+ expected: 1,
104
+ received: args.length,
105
+ positionStart,
106
+ positionEnd,
107
+ rowNumber,
108
+ })
109
+ }
110
+ const val = args[0]
111
+ if (val == null) return null
112
+ return Math.log(Number(val))
113
+ }
114
+
115
+ if (funcName === 'LOG10') {
116
+ if (args.length !== 1) {
117
+ throw argCountError({
118
+ funcName: 'LOG10',
119
+ expected: 1,
120
+ received: args.length,
121
+ positionStart,
122
+ positionEnd,
123
+ rowNumber,
124
+ })
125
+ }
126
+ const val = args[0]
127
+ if (val == null) return null
128
+ return Math.log10(Number(val))
129
+ }
130
+
131
+ if (funcName === 'POWER') {
132
+ if (args.length !== 2) {
133
+ throw argCountError({
134
+ funcName: 'POWER',
135
+ expected: 2,
136
+ received: args.length,
137
+ positionStart,
138
+ positionEnd,
139
+ rowNumber,
140
+ })
141
+ }
142
+ const base = args[0]
143
+ const exponent = args[1]
144
+ if (base == null || exponent == null) return null
145
+ return Number(base) ** Number(exponent)
146
+ }
147
+
148
+ if (funcName === 'SQRT') {
149
+ if (args.length !== 1) {
150
+ throw argCountError({
151
+ funcName: 'SQRT',
152
+ expected: 1,
153
+ received: args.length,
154
+ positionStart,
155
+ positionEnd,
156
+ rowNumber,
157
+ })
158
+ }
159
+ const val = args[0]
160
+ if (val == null) return null
161
+ return Math.sqrt(Number(val))
162
+ }
163
+
164
+ return undefined
165
+ }
@@ -134,6 +134,9 @@ export function defaultDerivedAlias(expr) {
134
134
  if (expr.type === 'function') {
135
135
  return expr.name.toLowerCase() + '_' + expr.args.map(defaultDerivedAlias).join('_')
136
136
  }
137
+ if (expr.type === 'interval') {
138
+ return `interval_${expr.value}_${expr.unit.toLowerCase()}`
139
+ }
137
140
  return 'expr'
138
141
  }
139
142
 
@@ -0,0 +1,62 @@
1
+ // ============================================================================
2
+ // EXECUTION ERRORS - Issues during query execution
3
+ // ============================================================================
4
+
5
+ /**
6
+ * Structured execution error with position range and optional row number.
7
+ */
8
+ export class ExecutionError 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
+ * @param {number} [rowNumber] - 1-based row number where error occurred
14
+ */
15
+ constructor(message, positionStart, positionEnd, rowNumber) {
16
+ const rowSuffix = rowNumber != null ? ` (row ${rowNumber})` : ''
17
+ super(message + rowSuffix)
18
+ this.name = 'ExecutionError'
19
+ this.positionStart = positionStart
20
+ this.positionEnd = positionEnd
21
+ this.rowNumber = rowNumber
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Error for missing table.
27
+ *
28
+ * @param {Object} options
29
+ * @param {string} options.tableName - The missing table name
30
+ * @returns {Error}
31
+ */
32
+ export function tableNotFoundError({ tableName }) {
33
+ return new Error(`Table "${tableName}" not found. Check spelling or add it to the tables parameter.`)
34
+ }
35
+
36
+ /**
37
+ * Error for invalid context (e.g., INTERVAL without date arithmetic).
38
+ *
39
+ * @param {Object} options
40
+ * @param {string} options.item - What was used incorrectly
41
+ * @param {string} options.validContext - Where it can be used
42
+ * @param {number} options.positionStart - Start position in query
43
+ * @param {number} options.positionEnd - End position in query
44
+ * @param {number} [options.rowNumber] - 1-based row number where error occurred
45
+ * @returns {ExecutionError}
46
+ */
47
+ export function invalidContextError({ item, validContext, positionStart, positionEnd, rowNumber }) {
48
+ return new ExecutionError(`${item} can only be used with ${validContext}`, positionStart, positionEnd, rowNumber)
49
+ }
50
+
51
+ /**
52
+ * Error for unsupported operation combinations.
53
+ *
54
+ * @param {Object} options
55
+ * @param {string} options.operation - The unsupported operation
56
+ * @param {string} [options.hint] - How to fix it
57
+ * @returns {Error}
58
+ */
59
+ export function unsupportedOperationError({ operation, hint }) {
60
+ const suffix = hint ? `. ${hint}` : ''
61
+ return new Error(`${operation}${suffix}`)
62
+ }
package/src/index.js CHANGED
@@ -2,3 +2,4 @@ export { executeSql } from './execute/execute.js'
2
2
  export { parseSql } from './parse/parse.js'
3
3
  export { collect } from './execute/utils.js'
4
4
  export { cachedDataSource } from './backend/dataSource.js'
5
+ export { ParseError } from './parseErrors.js'
@@ -1,6 +1,7 @@
1
+ import { syntaxError } from '../parseErrors.js'
1
2
  import { isBinaryOp } from '../validation.js'
2
3
  import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
3
- import { consume, current, expect, match, peekToken } from './state.js'
4
+ import { consume, current, expect, lastPosition, match, peekToken } from './state.js'
4
5
 
5
6
  /**
6
7
  * @import { ExprNode, ParserState } from '../types.js'
@@ -25,6 +26,8 @@ export function parseComparison(state) {
25
26
  type: 'unary',
26
27
  op: 'IS NOT NULL',
27
28
  argument: left,
29
+ positionStart: left.positionStart,
30
+ positionEnd: lastPosition(state),
28
31
  }
29
32
  }
30
33
  expect(state, 'keyword', 'NULL')
@@ -32,6 +35,8 @@ export function parseComparison(state) {
32
35
  type: 'unary',
33
36
  op: 'IS NULL',
34
37
  argument: left,
38
+ positionStart: left.positionStart,
39
+ positionEnd: lastPosition(state),
35
40
  }
36
41
  }
37
42
 
@@ -39,6 +44,7 @@ export function parseComparison(state) {
39
44
  if (tok.type === 'keyword' && tok.value === 'NOT') {
40
45
  const nextTok = peekToken(state, 1)
41
46
  if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
47
+ const notPositionStart = tok.positionStart
42
48
  consume(state) // NOT
43
49
  consume(state) // LIKE
44
50
  const right = parseAdditive(state)
@@ -50,7 +56,11 @@ export function parseComparison(state) {
50
56
  op: 'LIKE',
51
57
  left,
52
58
  right,
59
+ positionStart: left.positionStart,
60
+ positionEnd: right.positionEnd,
53
61
  },
62
+ positionStart: notPositionStart,
63
+ positionEnd: right.positionEnd,
54
64
  }
55
65
  }
56
66
  }
@@ -63,6 +73,8 @@ export function parseComparison(state) {
63
73
  op: 'LIKE',
64
74
  left,
65
75
  right,
76
+ positionStart: left.positionStart,
77
+ positionEnd: right.positionEnd,
66
78
  }
67
79
  }
68
80
 
@@ -70,6 +82,7 @@ export function parseComparison(state) {
70
82
  if (tok.type === 'keyword' && tok.value === 'NOT') {
71
83
  const nextTok = peekToken(state, 1)
72
84
  if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
85
+ const notPositionStart = tok.positionStart
73
86
  consume(state) // NOT
74
87
  consume(state) // BETWEEN
75
88
  const lower = parseAdditive(state)
@@ -79,8 +92,10 @@ export function parseComparison(state) {
79
92
  return {
80
93
  type: 'binary',
81
94
  op: 'OR',
82
- left: { type: 'binary', op: '<', left, right: lower },
83
- right: { type: 'binary', op: '>', left, right: upper },
95
+ left: { type: 'binary', op: '<', left, right: lower, positionStart: left.positionStart, positionEnd: lower.positionEnd },
96
+ right: { type: 'binary', op: '>', left, right: upper, positionStart: left.positionStart, positionEnd: upper.positionEnd },
97
+ positionStart: notPositionStart,
98
+ positionEnd: upper.positionEnd,
84
99
  }
85
100
  }
86
101
  }
@@ -94,8 +109,10 @@ export function parseComparison(state) {
94
109
  return {
95
110
  type: 'binary',
96
111
  op: 'AND',
97
- left: { type: 'binary', op: '>=', left, right: lower },
98
- right: { type: 'binary', op: '<=', left, right: upper },
112
+ left: { type: 'binary', op: '>=', left, right: lower, positionStart: left.positionStart, positionEnd: lower.positionEnd },
113
+ right: { type: 'binary', op: '<=', left, right: upper, positionStart: left.positionStart, positionEnd: upper.positionEnd },
114
+ positionStart: left.positionStart,
115
+ positionEnd: upper.positionEnd,
99
116
  }
100
117
  }
101
118
 
@@ -103,6 +120,7 @@ export function parseComparison(state) {
103
120
  if (tok.type === 'keyword' && tok.value === 'NOT') {
104
121
  const nextTok = peekToken(state, 1)
105
122
  if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
123
+ const notPositionStart = tok.positionStart
106
124
  consume(state) // NOT
107
125
  consume(state) // IN
108
126
 
@@ -110,12 +128,13 @@ export function parseComparison(state) {
110
128
  // parseSubquery expects to consume the opening paren itself
111
129
  const parenTok = current(state)
112
130
  if (parenTok.type !== 'paren' || parenTok.value !== '(') {
113
- throw new Error('Expected ( after IN')
131
+ throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, positionStart: parenTok.positionStart, positionEnd: parenTok.positionEnd, after: 'IN' })
114
132
  }
115
133
  const peekTok = peekToken(state, 1)
116
134
  if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
117
135
  // Subquery - let parseSubquery handle the parens
118
136
  const subquery = parseSubquery(state)
137
+ const positionEnd = lastPosition(state)
119
138
  return {
120
139
  type: 'unary',
121
140
  op: 'NOT',
@@ -123,7 +142,11 @@ export function parseComparison(state) {
123
142
  type: 'in',
124
143
  expr: left,
125
144
  subquery,
145
+ positionStart: left.positionStart,
146
+ positionEnd,
126
147
  },
148
+ positionStart: notPositionStart,
149
+ positionEnd,
127
150
  }
128
151
  } else {
129
152
  // Parse list of values - we handle the parens
@@ -135,6 +158,7 @@ export function parseComparison(state) {
135
158
  if (!match(state, 'comma')) break
136
159
  }
137
160
  expect(state, 'paren', ')')
161
+ const positionEnd = lastPosition(state)
138
162
  return {
139
163
  type: 'unary',
140
164
  op: 'NOT',
@@ -142,7 +166,11 @@ export function parseComparison(state) {
142
166
  type: 'in valuelist',
143
167
  expr: left,
144
168
  values,
169
+ positionStart: left.positionStart,
170
+ positionEnd,
145
171
  },
172
+ positionStart: notPositionStart,
173
+ positionEnd,
146
174
  }
147
175
  }
148
176
  }
@@ -155,7 +183,7 @@ export function parseComparison(state) {
155
183
  // parseSubquery expects to consume the opening paren itself
156
184
  const parenTok = current(state)
157
185
  if (parenTok.type !== 'paren' || parenTok.value !== '(') {
158
- throw new Error('Expected ( after IN')
186
+ throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, positionStart: parenTok.positionStart, positionEnd: parenTok.positionEnd, after: 'IN' })
159
187
  }
160
188
  const peekTok = peekToken(state, 1)
161
189
  if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
@@ -165,6 +193,8 @@ export function parseComparison(state) {
165
193
  type: 'in',
166
194
  expr: left,
167
195
  subquery,
196
+ positionStart: left.positionStart,
197
+ positionEnd: lastPosition(state),
168
198
  }
169
199
  } else {
170
200
  // Parse list of values - we handle the parens
@@ -180,6 +210,8 @@ export function parseComparison(state) {
180
210
  type: 'in valuelist',
181
211
  expr: left,
182
212
  values,
213
+ positionStart: left.positionStart,
214
+ positionEnd: lastPosition(state),
183
215
  }
184
216
  }
185
217
  }
@@ -192,6 +224,8 @@ export function parseComparison(state) {
192
224
  op: tok.value,
193
225
  left,
194
226
  right,
227
+ positionStart: left.positionStart,
228
+ positionEnd: right.positionEnd,
195
229
  }
196
230
  }
197
231