squirreling 0.6.0 → 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.
@@ -1,18 +1,18 @@
1
1
  import { unknownFunctionError } from '../parseErrors.js'
2
2
  import { invalidContextError } from '../executionErrors.js'
3
3
  import {
4
- argCountError,
4
+ aggregateError,
5
5
  argValueError,
6
6
  castError,
7
7
  } from '../validationErrors.js'
8
- import { isMathFunc } from '../validation.js'
8
+ import { isAggregateFunc, isMathFunc } from '../validation.js'
9
9
  import { applyIntervalToDate } from './date.js'
10
10
  import { executeSelect } from './execute.js'
11
11
  import { evaluateMathFunc } from './math.js'
12
12
  import { applyBinaryOp, stringify } from './utils.js'
13
13
 
14
14
  /**
15
- * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, IntervalUnit } from '../types.js'
15
+ * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, IntervalUnit, UserDefinedFunction } from '../types.js'
16
16
  */
17
17
 
18
18
  /**
@@ -22,10 +22,12 @@ import { applyBinaryOp, stringify } from './utils.js'
22
22
  * @param {ExprNode} params.node - The expression node to evaluate
23
23
  * @param {AsyncRow} params.row - The data row to evaluate against
24
24
  * @param {Record<string, AsyncDataSource>} params.tables
25
+ * @param {Record<string, UserDefinedFunction>} [params.functions] - User-defined functions
25
26
  * @param {number} [params.rowIndex] - 1-based row index for error reporting
27
+ * @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
26
28
  * @returns {Promise<SqlPrimitive>} The result of the evaluation
27
29
  */
28
- export async function evaluateExpr({ node, row, tables, rowIndex }) {
30
+ export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows }) {
29
31
  if (node.type === 'literal') {
30
32
  return node.value
31
33
  }
@@ -47,7 +49,7 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
47
49
 
48
50
  // Scalar subquery - returns a single value
49
51
  if (node.type === 'subquery') {
50
- const gen = executeSelect(node.subquery, tables)
52
+ const gen = executeSelect({ select: node.subquery, tables })
51
53
  const { value } = await gen.next() // Start the generator
52
54
  gen.return(undefined) // Stop further execution
53
55
  if (!value) return null
@@ -57,16 +59,16 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
57
59
  // Unary operators
58
60
  if (node.type === 'unary') {
59
61
  if (node.op === 'NOT') {
60
- return !await evaluateExpr({ node: node.argument, row, tables, rowIndex })
62
+ return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
61
63
  }
62
64
  if (node.op === 'IS NULL') {
63
- return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) == null
65
+ return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) == null
64
66
  }
65
67
  if (node.op === 'IS NOT NULL') {
66
- return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) != null
68
+ return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) != null
67
69
  }
68
70
  if (node.op === '-') {
69
- const val = await evaluateExpr({ node: node.argument, row, tables, rowIndex })
71
+ const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
70
72
  if (val == null) return null
71
73
  return -val
72
74
  }
@@ -76,15 +78,15 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
76
78
  if (node.type === 'binary') {
77
79
  // Handle date +/- interval at AST level
78
80
  if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
79
- const dateVal = await evaluateExpr({ node: node.left, row, tables, rowIndex })
81
+ const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
80
82
  return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
81
83
  }
82
84
  if (node.op === '+' && node.left.type === 'interval') {
83
- const dateVal = await evaluateExpr({ node: node.right, row, tables, rowIndex })
85
+ const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
84
86
  return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
85
87
  }
86
88
 
87
- const left = await evaluateExpr({ node: node.left, row, tables, rowIndex })
89
+ const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
88
90
 
89
91
  // Short-circuit evaluation for AND and OR
90
92
  if (node.op === 'AND') {
@@ -94,59 +96,123 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
94
96
  if (left) return true
95
97
  }
96
98
 
97
- const right = await evaluateExpr({ node: node.right, row, tables, rowIndex })
99
+ const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
98
100
  return applyBinaryOp(node.op, left, right)
99
101
  }
100
102
 
101
103
  // Function calls
102
104
  if (node.type === 'function') {
103
105
  const funcName = node.name.toUpperCase()
104
- /** @type {SqlPrimitive[]} */
105
- const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, rowIndex })))
106
106
 
107
- if (funcName === 'UPPER') {
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,
107
+ // Handle aggregate functions
108
+ if (isAggregateFunc(funcName)) {
109
+ if (!rows) {
110
+ throw aggregateError({
111
+ funcName,
112
+ issue: 'requires GROUP BY or will act on the whole dataset',
116
113
  })
117
114
  }
115
+
116
+ // Check for star argument (COUNT(*))
117
+ if (node.args.length === 1 && node.args[0].type === 'identifier' && node.args[0].name === '*') {
118
+ if (funcName === 'COUNT') {
119
+ return rows.length
120
+ }
121
+ throw aggregateError({
122
+ funcName,
123
+ issue: '(*) is not supported, use a column name',
124
+ })
125
+ }
126
+
127
+ const argNode = node.args[0]
128
+
129
+ if (funcName === 'COUNT') {
130
+ if (node.distinct) {
131
+ const seen = new Set()
132
+ for (const r of rows) {
133
+ const v = await evaluateExpr({ node: argNode, row: r, tables, functions })
134
+ if (v != null) seen.add(v)
135
+ }
136
+ return seen.size
137
+ }
138
+ let count = 0
139
+ for (const r of rows) {
140
+ const v = await evaluateExpr({ node: argNode, row: r, tables, functions })
141
+ if (v != null) count++
142
+ }
143
+ return count
144
+ }
145
+
146
+ if (funcName === 'SUM' || funcName === 'AVG' || funcName === 'MIN' || funcName === 'MAX') {
147
+ let sum = 0
148
+ let count = 0
149
+ /** @type {number | null} */
150
+ let min = null
151
+ /** @type {number | null} */
152
+ let max = null
153
+
154
+ for (const r of rows) {
155
+ const raw = await evaluateExpr({ node: argNode, row: r, tables, functions })
156
+ if (raw == null) continue
157
+ const num = Number(raw)
158
+ if (!Number.isFinite(num)) continue
159
+
160
+ if (count === 0) {
161
+ min = num
162
+ max = num
163
+ } else {
164
+ if (min == null || num < min) min = num
165
+ if (max == null || num > max) max = num
166
+ }
167
+ sum += num
168
+ count++
169
+ }
170
+
171
+ if (funcName === 'SUM') return sum
172
+ if (funcName === 'AVG') return count === 0 ? null : sum / count
173
+ if (funcName === 'MIN') return min
174
+ if (funcName === 'MAX') return max
175
+ }
176
+
177
+ if (funcName === 'JSON_ARRAYAGG') {
178
+ /** @type {SqlPrimitive[]} */
179
+ const values = []
180
+ if (node.distinct) {
181
+ const seen = new Set()
182
+ for (const r of rows) {
183
+ const v = await evaluateExpr({ node: argNode, row: r, tables, functions })
184
+ const key = stringify(v)
185
+ if (!seen.has(key)) {
186
+ seen.add(key)
187
+ values.push(v)
188
+ }
189
+ }
190
+ } else {
191
+ for (const r of rows) {
192
+ const v = await evaluateExpr({ node: argNode, row: r, tables, functions })
193
+ values.push(v)
194
+ }
195
+ }
196
+ return values
197
+ }
198
+ }
199
+
200
+ /** @type {SqlPrimitive[]} */
201
+ const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows })))
202
+
203
+ if (funcName === 'UPPER') {
118
204
  const val = args[0]
119
205
  if (val == null) return null
120
206
  return String(val).toUpperCase()
121
207
  }
122
208
 
123
209
  if (funcName === 'LOWER') {
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
- }
134
210
  const val = args[0]
135
211
  if (val == null) return null
136
212
  return String(val).toLowerCase()
137
213
  }
138
214
 
139
215
  if (funcName === 'CONCAT') {
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
- }
150
216
  // SQL CONCAT returns NULL if any argument is NULL
151
217
  if (args.some(a => a == null)) return null
152
218
  if (args.some(a => typeof a === 'object')) {
@@ -163,32 +229,12 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
163
229
  }
164
230
 
165
231
  if (funcName === '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
- }
176
232
  const val = args[0]
177
233
  if (val == null) return null
178
234
  return String(val).length
179
235
  }
180
236
 
181
237
  if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
182
- if (args.length < 2 || args.length > 3) {
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
- })
191
- }
192
238
  const str = args[0]
193
239
  if (str == null) return null
194
240
  const strVal = String(str)
@@ -222,32 +268,12 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
222
268
  }
223
269
 
224
270
  if (funcName === 'TRIM') {
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
- }
235
271
  const val = args[0]
236
272
  if (val == null) return null
237
273
  return String(val).trim()
238
274
  }
239
275
 
240
276
  if (funcName === 'REPLACE') {
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
- }
251
277
  const str = args[0]
252
278
  const searchStr = args[1]
253
279
  const replaceStr = args[2]
@@ -257,67 +283,26 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
257
283
  }
258
284
 
259
285
  if (funcName === 'RANDOM' || funcName === 'RAND') {
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
- }
270
286
  return Math.random()
271
287
  }
272
288
 
273
289
  if (funcName === 'CURRENT_DATE') {
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
- }
284
290
  return new Date().toISOString().split('T')[0]
285
291
  }
286
292
 
287
293
  if (funcName === 'CURRENT_TIME') {
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
- }
298
294
  return new Date().toISOString().split('T')[1].replace('Z', '')
299
295
  }
300
296
 
301
297
  if (funcName === 'CURRENT_TIMESTAMP') {
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
- }
312
298
  return new Date().toISOString()
313
299
  }
314
300
 
315
301
  if (funcName === 'JSON_OBJECT') {
316
302
  if (args.length % 2 !== 0) {
317
- throw argCountError({
303
+ throw argValueError({
318
304
  funcName: 'JSON_OBJECT',
319
- expected: 'even number',
320
- received: args.length,
305
+ message: 'requires an even number of arguments (key-value pairs)',
321
306
  positionStart: node.positionStart,
322
307
  positionEnd: node.positionEnd,
323
308
  rowNumber: rowIndex,
@@ -344,16 +329,6 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
344
329
  }
345
330
 
346
331
  if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
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
- }
357
332
  let jsonArg = args[0]
358
333
  const pathArg = args[1]
359
334
  if (jsonArg == null || pathArg == null) return null
@@ -410,13 +385,15 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
410
385
  }
411
386
 
412
387
  if (isMathFunc(funcName)) {
413
- return evaluateMathFunc({
414
- funcName,
415
- args,
416
- positionStart: node.positionStart,
417
- positionEnd: node.positionEnd,
418
- rowNumber: rowIndex,
419
- })
388
+ return evaluateMathFunc({ funcName, args })
389
+ }
390
+
391
+ // Check user-defined functions (case-insensitive lookup)
392
+ if (functions) {
393
+ const udfName = Object.keys(functions).find(k => k.toUpperCase() === funcName)
394
+ if (udfName) {
395
+ return await functions[udfName](...args)
396
+ }
420
397
  }
421
398
 
422
399
  throw unknownFunctionError({
@@ -427,7 +404,7 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
427
404
  }
428
405
 
429
406
  if (node.type === 'cast') {
430
- const val = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
407
+ const val = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
431
408
  if (val == null) return null
432
409
  const toType = node.toType.toUpperCase()
433
410
  if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
@@ -470,17 +447,17 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
470
447
 
471
448
  // IN and NOT IN with value lists
472
449
  if (node.type === 'in valuelist') {
473
- const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
450
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
474
451
  for (const valueNode of node.values) {
475
- const val = await evaluateExpr({ node: valueNode, row, tables, rowIndex })
452
+ const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows })
476
453
  if (exprVal === val) return true
477
454
  }
478
455
  return false
479
456
  }
480
457
  // IN with subqueries
481
458
  if (node.type === 'in') {
482
- const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
483
- const results = executeSelect(node.subquery, tables)
459
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
460
+ const results = executeSelect({ select: node.subquery, tables })
484
461
  for await (const resRow of results) {
485
462
  const value = await resRow.cells[resRow.columns[0]]()
486
463
  if (exprVal === value) return true
@@ -490,39 +467,39 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
490
467
 
491
468
  // EXISTS and NOT EXISTS with subqueries
492
469
  if (node.type === 'exists') {
493
- const results = await executeSelect(node.subquery, tables).next()
470
+ const results = await executeSelect({ select: node.subquery, tables }).next()
494
471
  return results.done === false
495
472
  }
496
473
  if (node.type === 'not exists') {
497
- const results = await executeSelect(node.subquery, tables).next()
474
+ const results = await executeSelect({ select: node.subquery, tables }).next()
498
475
  return results.done === true
499
476
  }
500
477
 
501
478
  // CASE expressions
502
479
  if (node.type === 'case') {
503
480
  // For simple CASE: evaluate the case expression once
504
- const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, rowIndex })
481
+ const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, functions, rowIndex, rows })
505
482
 
506
483
  // Iterate through WHEN clauses
507
484
  for (const whenClause of node.whenClauses) {
508
485
  let conditionResult
509
486
  if (caseValue !== undefined) {
510
487
  // Simple CASE: compare caseValue with condition
511
- const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
488
+ const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
512
489
  conditionResult = caseValue === whenValue
513
490
  } else {
514
491
  // Searched CASE: evaluate condition as boolean
515
- conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
492
+ conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
516
493
  }
517
494
 
518
495
  if (conditionResult) {
519
- return evaluateExpr({ node: whenClause.result, row, tables, rowIndex })
496
+ return evaluateExpr({ node: whenClause.result, row, tables, functions, rowIndex, rows })
520
497
  }
521
498
  }
522
499
 
523
500
  // No WHEN clause matched, return ELSE result or NULL
524
501
  if (node.elseResult) {
525
- return evaluateExpr({ node: node.elseResult, row, tables, rowIndex })
502
+ return evaluateExpr({ node: node.elseResult, row, tables, functions, rowIndex, rows })
526
503
  }
527
504
  return null
528
505
  }
@@ -4,19 +4,21 @@ import { evaluateExpr } from './expression.js'
4
4
  import { applyBinaryOp } from './utils.js'
5
5
 
6
6
  /**
7
- * @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
7
+ * @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive, UserDefinedFunction } from '../types.js'
8
8
  */
9
9
 
10
10
  /**
11
11
  * Evaluates a HAVING expression with support for aggregate functions
12
12
  *
13
- * @param {ExprNode} expr - the HAVING expression
14
- * @param {AsyncRow} row - the aggregated result row
15
- * @param {AsyncRow[]} group - the group of rows for re-evaluating aggregates
16
- * @param {Record<string, AsyncDataSource>} tables
13
+ * @param {Object} options
14
+ * @param {ExprNode} options.expr - the HAVING expression
15
+ * @param {AsyncRow} options.row - the aggregated result row
16
+ * @param {AsyncRow[]} options.group - the group of rows for re-evaluating aggregates
17
+ * @param {Record<string, AsyncDataSource>} options.tables
18
+ * @param {Record<string, UserDefinedFunction>} [options.functions]
17
19
  * @returns {Promise<boolean>} whether the HAVING condition is satisfied
18
20
  */
19
- export async function evaluateHavingExpr(expr, row, group, tables) {
21
+ export async function evaluateHavingExpr({ expr, row, group, tables, functions }) {
20
22
  // Having context
21
23
  const context = { ...group[0] ?? {}, ...row }
22
24
 
@@ -26,12 +28,12 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
26
28
  const funcName = expr.name.toUpperCase()
27
29
  if (isAggregateFunc(funcName)) {
28
30
  // Evaluate aggregate function on the group
29
- return Boolean(await evaluateAggregateFunction(funcName, expr.args, group, tables))
31
+ return Boolean(await evaluateAggregateFunction({ funcName, args: expr.args, group, tables, functions }))
30
32
  }
31
33
  }
32
34
 
33
35
  if (expr.type === 'binary') {
34
- const left = await evaluateHavingValue(expr.left, context, group, tables)
36
+ const left = await evaluateHavingValue({ expr: expr.left, context, group, tables, functions })
35
37
 
36
38
  // Short-circuit evaluation for AND and OR
37
39
  if (expr.op === 'AND') {
@@ -41,61 +43,65 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
41
43
  if (left) return true
42
44
  }
43
45
 
44
- const right = await evaluateHavingValue(expr.right, context, group, tables)
46
+ const right = await evaluateHavingValue({ expr: expr.right, context, group, tables, functions })
45
47
  return Boolean(applyBinaryOp(expr.op, left, right))
46
48
  }
47
49
 
48
50
  if (expr.type === 'unary') {
49
51
  if (expr.op === 'NOT') {
50
- return !await evaluateHavingExpr(expr.argument, context, group, tables)
52
+ return !await evaluateHavingExpr({ expr: expr.argument, row: context, group, tables, functions })
51
53
  }
52
54
  if (expr.op === 'IS NULL') {
53
- return await evaluateHavingValue(expr.argument, context, group, tables) == null
55
+ return await evaluateHavingValue({ expr: expr.argument, context, group, tables, functions }) == null
54
56
  }
55
57
  if (expr.op === 'IS NOT NULL') {
56
- return await evaluateHavingValue(expr.argument, context, group, tables) != null
58
+ return await evaluateHavingValue({ expr: expr.argument, context, group, tables, functions }) != null
57
59
  }
58
60
  }
59
61
 
60
62
  // For other expression types, use the context row
61
- return Boolean(await evaluateExpr({ node: expr, row: context, tables }))
63
+ return Boolean(await evaluateExpr({ node: expr, row: context, tables, functions }))
62
64
  }
63
65
 
64
66
  /**
65
67
  * Evaluates a value in a HAVING expression
66
68
  *
67
- * @param {ExprNode} expr
68
- * @param {AsyncRow} context - the context row
69
- * @param {AsyncRow[]} group - the group of rows
70
- * @param {Record<string, AsyncDataSource>} tables
69
+ * @param {Object} options
70
+ * @param {ExprNode} options.expr
71
+ * @param {AsyncRow} options.context - the context row
72
+ * @param {AsyncRow[]} options.group - the group of rows
73
+ * @param {Record<string, AsyncDataSource>} options.tables
74
+ * @param {Record<string, UserDefinedFunction>} [options.functions]
71
75
  * @returns {Promise<SqlPrimitive>} the evaluated value
72
76
  */
73
- function evaluateHavingValue(expr, context, group, tables) {
77
+ function evaluateHavingValue({ expr, context, group, tables, functions }) {
74
78
  if (expr.type === 'function') {
75
79
  const funcName = expr.name.toUpperCase()
76
80
  if (isAggregateFunc(funcName)) {
77
- return evaluateAggregateFunction(funcName, expr.args, group, tables)
81
+ return evaluateAggregateFunction({ funcName, args: expr.args, group, tables, functions })
78
82
  }
79
83
  }
80
84
 
81
85
  // For binary expressions, we need to use evaluateHavingExpr to properly handle aggregates
82
86
  if (expr.type === 'binary' || expr.type === 'unary') {
83
- return evaluateHavingExpr(expr, context, group, tables)
87
+ return evaluateHavingExpr({ expr, row: context, group, tables, functions })
84
88
  }
85
89
 
86
- return evaluateExpr({ node: expr, row: context, tables })
90
+ return evaluateExpr({ node: expr, row: context, tables, functions })
87
91
  }
88
92
 
89
93
  /**
90
94
  * Evaluates an aggregate function on a group
91
95
  *
92
- * @param {AggregateFunc} funcName - aggregate function name
93
- * @param {ExprNode[]} args - function arguments
94
- * @param {AsyncRow[]} group - the group of rows
95
- * @param {Record<string, AsyncDataSource>} tables
96
+ * @param {Object} options
97
+ * @param {AggregateFunc} options.funcName - aggregate function name
98
+ * @param {ExprNode[]} options.args - function arguments
99
+ * @param {AsyncRow[]} options.group - the group of rows
100
+ * @param {Record<string, AsyncDataSource>} options.tables
101
+ * @param {Record<string, UserDefinedFunction>} [options.functions]
96
102
  * @returns {Promise<SqlPrimitive>} the aggregate result
97
103
  */
98
- async function evaluateAggregateFunction(funcName, args, group, tables) {
104
+ async function evaluateAggregateFunction({ funcName, args, group, tables, functions }) {
99
105
  if (funcName === 'COUNT') {
100
106
  if (args.length === 1 && args[0].type === 'identifier' && args[0].name === '*') {
101
107
  return group.length
@@ -103,7 +109,7 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
103
109
  // COUNT(column) - count non-null values
104
110
  let count = 0
105
111
  for (const row of group) {
106
- const val = await evaluateExpr({ node: args[0], row, tables })
112
+ const val = await evaluateExpr({ node: args[0], row, tables, functions })
107
113
  if (val != null) count++
108
114
  }
109
115
  return count
@@ -112,7 +118,7 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
112
118
  if (funcName === 'SUM') {
113
119
  let sum = 0
114
120
  for (const row of group) {
115
- const val = await evaluateExpr({ node: args[0], row, tables })
121
+ const val = await evaluateExpr({ node: args[0], row, tables, functions })
116
122
  if (val != null) sum += Number(val)
117
123
  }
118
124
  return sum
@@ -122,7 +128,7 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
122
128
  let sum = 0
123
129
  let count = 0
124
130
  for (const row of group) {
125
- const val = await evaluateExpr({ node: args[0], row, tables })
131
+ const val = await evaluateExpr({ node: args[0], row, tables, functions })
126
132
  if (val != null) {
127
133
  sum += Number(val)
128
134
  count++
@@ -134,7 +140,7 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
134
140
  if (funcName === 'MIN') {
135
141
  let min = null
136
142
  for (const row of group) {
137
- const val = await evaluateExpr({ node: args[0], row, tables })
143
+ const val = await evaluateExpr({ node: args[0], row, tables, functions })
138
144
  if (val != null && (min == null || val < min)) {
139
145
  min = val
140
146
  }
@@ -145,7 +151,7 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
145
151
  if (funcName === 'MAX') {
146
152
  let max = null
147
153
  for (const row of group) {
148
- const val = await evaluateExpr({ node: args[0], row, tables })
154
+ const val = await evaluateExpr({ node: args[0], row, tables, functions })
149
155
  if (val != null && (max == null || val > max)) {
150
156
  max = val
151
157
  }