squirreling 0.4.8 → 0.6.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.
@@ -1,12 +1,14 @@
1
+ import { unknownFunctionError } from '../parseErrors.js'
2
+ import { invalidContextError } from '../executionErrors.js'
1
3
  import {
2
4
  argCountError,
3
5
  argValueError,
4
6
  castError,
5
- invalidContextError,
6
- unknownFunctionError,
7
- } from '../errors.js'
7
+ } from '../validationErrors.js'
8
+ import { isMathFunc } from '../validation.js'
8
9
  import { applyIntervalToDate } from './date.js'
9
10
  import { executeSelect } from './execute.js'
11
+ import { evaluateMathFunc } from './math.js'
10
12
  import { applyBinaryOp, stringify } from './utils.js'
11
13
 
12
14
  /**
@@ -20,23 +22,24 @@ import { applyBinaryOp, stringify } from './utils.js'
20
22
  * @param {ExprNode} params.node - The expression node to evaluate
21
23
  * @param {AsyncRow} params.row - The data row to evaluate against
22
24
  * @param {Record<string, AsyncDataSource>} params.tables
25
+ * @param {number} [params.rowIndex] - 1-based row index for error reporting
23
26
  * @returns {Promise<SqlPrimitive>} The result of the evaluation
24
27
  */
25
- export async function evaluateExpr({ node, row, tables }) {
28
+ export async function evaluateExpr({ node, row, tables, rowIndex }) {
26
29
  if (node.type === 'literal') {
27
30
  return node.value
28
31
  }
29
32
 
30
33
  if (node.type === 'identifier') {
31
34
  // Try exact match first (handles both qualified and unqualified names)
32
- if (row[node.name]) {
33
- return row[node.name]()
35
+ if (row.cells[node.name]) {
36
+ return row.cells[node.name]()
34
37
  }
35
38
  // For qualified names like 'users.id', also try just the column part
36
39
  if (node.name.includes('.')) {
37
40
  const colName = node.name.split('.').pop()
38
- if (colName && row[colName]) {
39
- return row[colName]()
41
+ if (colName && row.cells[colName]) {
42
+ return row.cells[colName]()
40
43
  }
41
44
  }
42
45
  return null
@@ -45,28 +48,25 @@ export async function evaluateExpr({ node, row, tables }) {
45
48
  // Scalar subquery - returns a single value
46
49
  if (node.type === 'subquery') {
47
50
  const gen = executeSelect(node.subquery, tables)
48
- const first = await gen.next() // Start the generator
51
+ const { value } = await gen.next() // Start the generator
49
52
  gen.return(undefined) // Stop further execution
50
- if (!first.value) return null
51
- /** @type {AsyncRow} */
52
- const firstRow = first.value
53
- const firstKey = Object.keys(firstRow)[0]
54
- return firstRow[firstKey]()
53
+ if (!value) return null
54
+ return value.cells[value.columns[0]]()
55
55
  }
56
56
 
57
57
  // Unary operators
58
58
  if (node.type === 'unary') {
59
59
  if (node.op === 'NOT') {
60
- return !await evaluateExpr({ node: node.argument, row, tables })
60
+ return !await evaluateExpr({ node: node.argument, row, tables, rowIndex })
61
61
  }
62
62
  if (node.op === 'IS NULL') {
63
- return await evaluateExpr({ node: node.argument, row, tables }) == null
63
+ return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) == null
64
64
  }
65
65
  if (node.op === 'IS NOT NULL') {
66
- return await evaluateExpr({ node: node.argument, row, tables }) != null
66
+ return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) != null
67
67
  }
68
68
  if (node.op === '-') {
69
- const val = await evaluateExpr({ node: node.argument, row, tables })
69
+ const val = await evaluateExpr({ node: node.argument, row, tables, rowIndex })
70
70
  if (val == null) return null
71
71
  return -val
72
72
  }
@@ -76,15 +76,15 @@ export async function evaluateExpr({ node, row, tables }) {
76
76
  if (node.type === 'binary') {
77
77
  // Handle date +/- interval at AST level
78
78
  if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
79
- const dateVal = await evaluateExpr({ node: node.left, row, tables })
79
+ const dateVal = await evaluateExpr({ node: node.left, row, tables, rowIndex })
80
80
  return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
81
81
  }
82
82
  if (node.op === '+' && node.left.type === 'interval') {
83
- const dateVal = await evaluateExpr({ node: node.right, row, tables })
83
+ const dateVal = await evaluateExpr({ node: node.right, row, tables, rowIndex })
84
84
  return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
85
85
  }
86
86
 
87
- const left = await evaluateExpr({ node: node.left, row, tables })
87
+ const left = await evaluateExpr({ node: node.left, row, tables, rowIndex })
88
88
 
89
89
  // Short-circuit evaluation for AND and OR
90
90
  if (node.op === 'AND') {
@@ -94,7 +94,7 @@ export async function evaluateExpr({ node, row, tables }) {
94
94
  if (left) return true
95
95
  }
96
96
 
97
- const right = await evaluateExpr({ node: node.right, row, tables })
97
+ const right = await evaluateExpr({ node: node.right, row, tables, rowIndex })
98
98
  return applyBinaryOp(node.op, left, right)
99
99
  }
100
100
 
@@ -102,38 +102,77 @@ export async function evaluateExpr({ node, row, tables }) {
102
102
  if (node.type === 'function') {
103
103
  const funcName = node.name.toUpperCase()
104
104
  /** @type {SqlPrimitive[]} */
105
- const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
105
+ const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, rowIndex })))
106
106
 
107
107
  if (funcName === 'UPPER') {
108
- if (args.length !== 1) throw argCountError('UPPER', 1, args.length)
108
+ if (args.length !== 1) {
109
+ throw argCountError({
110
+ funcName: 'UPPER',
111
+ expected: 1,
112
+ received: args.length,
113
+ positionStart: node.positionStart,
114
+ positionEnd: node.positionEnd,
115
+ rowNumber: rowIndex,
116
+ })
117
+ }
109
118
  const val = args[0]
110
119
  if (val == null) return null
111
120
  return String(val).toUpperCase()
112
121
  }
113
122
 
114
123
  if (funcName === 'LOWER') {
115
- if (args.length !== 1) throw argCountError('LOWER', 1, args.length)
124
+ if (args.length !== 1) {
125
+ throw argCountError({
126
+ funcName: 'LOWER',
127
+ expected: 1,
128
+ received: args.length,
129
+ positionStart: node.positionStart,
130
+ positionEnd: node.positionEnd,
131
+ rowNumber: rowIndex,
132
+ })
133
+ }
116
134
  const val = args[0]
117
135
  if (val == null) return null
118
136
  return String(val).toLowerCase()
119
137
  }
120
138
 
121
139
  if (funcName === 'CONCAT') {
122
- if (args.length < 1) throw argCountError('CONCAT', 'at least 1', args.length)
140
+ if (args.length < 1) {
141
+ throw argCountError({
142
+ funcName: 'CONCAT',
143
+ expected: 'at least 1',
144
+ received: args.length,
145
+ positionStart: node.positionStart,
146
+ positionEnd: node.positionEnd,
147
+ rowNumber: rowIndex,
148
+ })
149
+ }
123
150
  // SQL CONCAT returns NULL if any argument is NULL
124
151
  if (args.some(a => a == null)) return null
125
152
  if (args.some(a => typeof a === 'object')) {
126
153
  throw argValueError({
127
154
  funcName: 'CONCAT',
128
155
  message: 'does not support object arguments',
156
+ positionStart: node.positionStart,
157
+ positionEnd: node.positionEnd,
129
158
  hint: 'Use CAST to convert objects to strings first.',
159
+ rowNumber: rowIndex,
130
160
  })
131
161
  }
132
162
  return args.map(a => String(a)).join('')
133
163
  }
134
164
 
135
165
  if (funcName === 'LENGTH') {
136
- if (args.length !== 1) throw argCountError('LENGTH', 1, args.length)
166
+ if (args.length !== 1) {
167
+ throw argCountError({
168
+ funcName: 'LENGTH',
169
+ expected: 1,
170
+ received: args.length,
171
+ positionStart: node.positionStart,
172
+ positionEnd: node.positionEnd,
173
+ rowNumber: rowIndex,
174
+ })
175
+ }
137
176
  const val = args[0]
138
177
  if (val == null) return null
139
178
  return String(val).length
@@ -141,7 +180,14 @@ export async function evaluateExpr({ node, row, tables }) {
141
180
 
142
181
  if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
143
182
  if (args.length < 2 || args.length > 3) {
144
- throw argCountError(funcName, '2 or 3', args.length)
183
+ throw argCountError({
184
+ funcName,
185
+ expected: '2 or 3',
186
+ received: args.length,
187
+ positionStart: node.positionStart,
188
+ positionEnd: node.positionEnd,
189
+ rowNumber: rowIndex,
190
+ })
145
191
  }
146
192
  const str = args[0]
147
193
  if (str == null) return null
@@ -151,7 +197,10 @@ export async function evaluateExpr({ node, row, tables }) {
151
197
  throw argValueError({
152
198
  funcName,
153
199
  message: `start position must be a positive integer, got ${args[1]}`,
200
+ positionStart: node.positionStart,
201
+ positionEnd: node.positionEnd,
154
202
  hint: 'SQL uses 1-based indexing.',
203
+ rowNumber: rowIndex,
155
204
  })
156
205
  }
157
206
  // SQL uses 1-based indexing
@@ -162,6 +211,9 @@ export async function evaluateExpr({ node, row, tables }) {
162
211
  throw argValueError({
163
212
  funcName,
164
213
  message: `length must be a non-negative integer, got ${args[2]}`,
214
+ positionStart: node.positionStart,
215
+ positionEnd: node.positionEnd,
216
+ rowNumber: rowIndex,
165
217
  })
166
218
  }
167
219
  return strVal.substring(startIdx, startIdx + len)
@@ -170,14 +222,32 @@ export async function evaluateExpr({ node, row, tables }) {
170
222
  }
171
223
 
172
224
  if (funcName === 'TRIM') {
173
- if (args.length !== 1) throw argCountError('TRIM', 1, args.length)
225
+ if (args.length !== 1) {
226
+ throw argCountError({
227
+ funcName: 'TRIM',
228
+ expected: 1,
229
+ received: args.length,
230
+ positionStart: node.positionStart,
231
+ positionEnd: node.positionEnd,
232
+ rowNumber: rowIndex,
233
+ })
234
+ }
174
235
  const val = args[0]
175
236
  if (val == null) return null
176
237
  return String(val).trim()
177
238
  }
178
239
 
179
240
  if (funcName === 'REPLACE') {
180
- if (args.length !== 3) throw argCountError('REPLACE', 3, args.length)
241
+ if (args.length !== 3) {
242
+ throw argCountError({
243
+ funcName: 'REPLACE',
244
+ expected: 3,
245
+ received: args.length,
246
+ positionStart: node.positionStart,
247
+ positionEnd: node.positionEnd,
248
+ rowNumber: rowIndex,
249
+ })
250
+ }
181
251
  const str = args[0]
182
252
  const searchStr = args[1]
183
253
  const replaceStr = args[2]
@@ -187,28 +257,71 @@ export async function evaluateExpr({ node, row, tables }) {
187
257
  }
188
258
 
189
259
  if (funcName === 'RANDOM' || funcName === 'RAND') {
190
- if (args.length !== 0) throw argCountError(funcName, 0, args.length)
260
+ if (args.length !== 0) {
261
+ throw argCountError({
262
+ funcName,
263
+ expected: 0,
264
+ received: args.length,
265
+ positionStart: node.positionStart,
266
+ positionEnd: node.positionEnd,
267
+ rowNumber: rowIndex,
268
+ })
269
+ }
191
270
  return Math.random()
192
271
  }
193
272
 
194
273
  if (funcName === 'CURRENT_DATE') {
195
- if (args.length !== 0) throw argCountError('CURRENT_DATE', 0, args.length)
274
+ if (args.length !== 0) {
275
+ throw argCountError({
276
+ funcName: 'CURRENT_DATE',
277
+ expected: 0,
278
+ received: args.length,
279
+ positionStart: node.positionStart,
280
+ positionEnd: node.positionEnd,
281
+ rowNumber: rowIndex,
282
+ })
283
+ }
196
284
  return new Date().toISOString().split('T')[0]
197
285
  }
198
286
 
199
287
  if (funcName === 'CURRENT_TIME') {
200
- if (args.length !== 0) throw argCountError('CURRENT_TIME', 0, args.length)
288
+ if (args.length !== 0) {
289
+ throw argCountError({
290
+ funcName: 'CURRENT_TIME',
291
+ expected: 0,
292
+ received: args.length,
293
+ positionStart: node.positionStart,
294
+ positionEnd: node.positionEnd,
295
+ rowNumber: rowIndex,
296
+ })
297
+ }
201
298
  return new Date().toISOString().split('T')[1].replace('Z', '')
202
299
  }
203
300
 
204
301
  if (funcName === 'CURRENT_TIMESTAMP') {
205
- if (args.length !== 0) throw argCountError('CURRENT_TIMESTAMP', 0, args.length)
302
+ if (args.length !== 0) {
303
+ throw argCountError({
304
+ funcName: 'CURRENT_TIMESTAMP',
305
+ expected: 0,
306
+ received: args.length,
307
+ positionStart: node.positionStart,
308
+ positionEnd: node.positionEnd,
309
+ rowNumber: rowIndex,
310
+ })
311
+ }
206
312
  return new Date().toISOString()
207
313
  }
208
314
 
209
315
  if (funcName === 'JSON_OBJECT') {
210
316
  if (args.length % 2 !== 0) {
211
- throw argCountError('JSON_OBJECT', 'even number', args.length)
317
+ throw argCountError({
318
+ funcName: 'JSON_OBJECT',
319
+ expected: 'even number',
320
+ received: args.length,
321
+ positionStart: node.positionStart,
322
+ positionEnd: node.positionEnd,
323
+ rowNumber: rowIndex,
324
+ })
212
325
  }
213
326
  /** @type {Record<string, SqlPrimitive>} */
214
327
  const result = {}
@@ -219,7 +332,10 @@ export async function evaluateExpr({ node, row, tables }) {
219
332
  throw argValueError({
220
333
  funcName: 'JSON_OBJECT',
221
334
  message: 'key cannot be null',
335
+ positionStart: node.positionStart,
336
+ positionEnd: node.positionEnd,
222
337
  hint: 'All keys must be non-null values.',
338
+ rowNumber: rowIndex,
223
339
  })
224
340
  }
225
341
  result[String(key)] = value
@@ -228,7 +344,16 @@ export async function evaluateExpr({ node, row, tables }) {
228
344
  }
229
345
 
230
346
  if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
231
- if (args.length !== 2) throw argCountError(funcName, 2, args.length)
347
+ if (args.length !== 2) {
348
+ throw argCountError({
349
+ funcName,
350
+ expected: 2,
351
+ received: args.length,
352
+ positionStart: node.positionStart,
353
+ positionEnd: node.positionEnd,
354
+ rowNumber: rowIndex,
355
+ })
356
+ }
232
357
  let jsonArg = args[0]
233
358
  const pathArg = args[1]
234
359
  if (jsonArg == null || pathArg == null) return null
@@ -241,7 +366,10 @@ export async function evaluateExpr({ node, row, tables }) {
241
366
  throw argValueError({
242
367
  funcName,
243
368
  message: 'invalid JSON string',
369
+ positionStart: node.positionStart,
370
+ positionEnd: node.positionEnd,
244
371
  hint: 'First argument must be valid JSON.',
372
+ rowNumber: rowIndex,
245
373
  })
246
374
  }
247
375
  }
@@ -249,6 +377,9 @@ export async function evaluateExpr({ node, row, tables }) {
249
377
  throw argValueError({
250
378
  funcName,
251
379
  message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
380
+ positionStart: node.positionStart,
381
+ positionEnd: node.positionEnd,
382
+ rowNumber: rowIndex,
252
383
  })
253
384
  }
254
385
 
@@ -278,11 +409,25 @@ export async function evaluateExpr({ node, row, tables }) {
278
409
  return current
279
410
  }
280
411
 
281
- throw unknownFunctionError(funcName)
412
+ if (isMathFunc(funcName)) {
413
+ return evaluateMathFunc({
414
+ funcName,
415
+ args,
416
+ positionStart: node.positionStart,
417
+ positionEnd: node.positionEnd,
418
+ rowNumber: rowIndex,
419
+ })
420
+ }
421
+
422
+ throw unknownFunctionError({
423
+ funcName,
424
+ positionStart: node.positionStart,
425
+ positionEnd: node.positionEnd,
426
+ })
282
427
  }
283
428
 
284
429
  if (node.type === 'cast') {
285
- const val = await evaluateExpr({ node: node.expr, row, tables })
430
+ const val = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
286
431
  if (val == null) return null
287
432
  const toType = node.toType.toUpperCase()
288
433
  if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
@@ -290,7 +435,15 @@ export async function evaluateExpr({ node, row, tables }) {
290
435
  return String(val)
291
436
  }
292
437
  // Can only cast primitives to other primitive types
293
- if (typeof val === 'object') throw castError(node.toType, 'object')
438
+ if (typeof val === 'object') {
439
+ throw castError({
440
+ toType: node.toType,
441
+ positionStart: node.positionStart,
442
+ positionEnd: node.positionEnd,
443
+ fromType: 'object',
444
+ rowNumber: rowIndex,
445
+ })
446
+ }
294
447
  if (toType === 'INTEGER' || toType === 'INT') {
295
448
  const num = Number(val)
296
449
  if (isNaN(num)) return null
@@ -307,30 +460,32 @@ export async function evaluateExpr({ node, row, tables }) {
307
460
  if (toType === 'BOOLEAN' || toType === 'BOOL') {
308
461
  return Boolean(val)
309
462
  }
310
- throw castError(node.toType)
463
+ throw castError({
464
+ toType: node.toType,
465
+ positionStart: node.positionStart,
466
+ positionEnd: node.positionEnd,
467
+ rowNumber: rowIndex,
468
+ })
311
469
  }
312
470
 
313
471
  // IN and NOT IN with value lists
314
472
  if (node.type === 'in valuelist') {
315
- const exprVal = await evaluateExpr({ node: node.expr, row, tables })
473
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
316
474
  for (const valueNode of node.values) {
317
- const val = await evaluateExpr({ node: valueNode, row, tables })
475
+ const val = await evaluateExpr({ node: valueNode, row, tables, rowIndex })
318
476
  if (exprVal === val) return true
319
477
  }
320
478
  return false
321
479
  }
322
480
  // IN with subqueries
323
481
  if (node.type === 'in') {
324
- const exprVal = await evaluateExpr({ node: node.expr, row, tables })
482
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
325
483
  const results = executeSelect(node.subquery, tables)
326
- /** @type {SqlPrimitive[]} */
327
- const values = []
328
484
  for await (const resRow of results) {
329
- const firstKey = Object.keys(resRow)[0]
330
- const val = await resRow[firstKey]()
331
- values.push(val)
485
+ const value = await resRow.cells[resRow.columns[0]]()
486
+ if (exprVal === value) return true
332
487
  }
333
- return values.includes(exprVal)
488
+ return false
334
489
  }
335
490
 
336
491
  // EXISTS and NOT EXISTS with subqueries
@@ -346,28 +501,28 @@ export async function evaluateExpr({ node, row, tables }) {
346
501
  // CASE expressions
347
502
  if (node.type === 'case') {
348
503
  // For simple CASE: evaluate the case expression once
349
- const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
504
+ const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, rowIndex })
350
505
 
351
506
  // Iterate through WHEN clauses
352
507
  for (const whenClause of node.whenClauses) {
353
508
  let conditionResult
354
509
  if (caseValue !== undefined) {
355
510
  // Simple CASE: compare caseValue with condition
356
- const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables })
511
+ const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
357
512
  conditionResult = caseValue === whenValue
358
513
  } else {
359
514
  // Searched CASE: evaluate condition as boolean
360
- conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables })
515
+ conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
361
516
  }
362
517
 
363
518
  if (conditionResult) {
364
- return evaluateExpr({ node: whenClause.result, row, tables })
519
+ return evaluateExpr({ node: whenClause.result, row, tables, rowIndex })
365
520
  }
366
521
  }
367
522
 
368
523
  // No WHEN clause matched, return ELSE result or NULL
369
524
  if (node.elseResult) {
370
- return evaluateExpr({ node: node.elseResult, row, tables })
525
+ return evaluateExpr({ node: node.elseResult, row, tables, rowIndex })
371
526
  }
372
527
  return null
373
528
  }
@@ -378,6 +533,9 @@ export async function evaluateExpr({ node, row, tables }) {
378
533
  throw invalidContextError({
379
534
  item: 'INTERVAL',
380
535
  validContext: 'date arithmetic (+ or -)',
536
+ positionStart: node.positionStart,
537
+ positionEnd: node.positionEnd,
538
+ rowNumber: rowIndex,
381
539
  })
382
540
  }
383
541
 
@@ -1,4 +1,4 @@
1
- import { unknownFunctionError } from '../errors.js'
1
+ import { unknownFunctionError } from '../parseErrors.js'
2
2
  import { isAggregateFunc } from '../validation.js'
3
3
  import { evaluateExpr } from './expression.js'
4
4
  import { applyBinaryOp } from './utils.js'
@@ -153,5 +153,10 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
153
153
  return max
154
154
  }
155
155
 
156
- throw unknownFunctionError(funcName, undefined, 'COUNT, SUM, AVG, MIN, MAX')
156
+ throw unknownFunctionError({
157
+ funcName,
158
+ positionStart: 0,
159
+ positionEnd: 0,
160
+ validFunctions: 'COUNT, SUM, AVG, MIN, MAX',
161
+ })
157
162
  }