squirreling 0.7.9 → 0.8.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,55 +1,7 @@
1
1
  /**
2
- * @import { AsyncRow, BinaryOp, ExprNode, OrderByItem, SqlPrimitive } from '../types.js'
2
+ * @import { AsyncCells, AsyncRow, ExprNode, OrderByItem, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
- /**
6
- * Applies a binary operator to two values, handling nulls according to SQL semantics
7
- *
8
- * @param {BinaryOp} op
9
- * @param {SqlPrimitive} a
10
- * @param {SqlPrimitive} b
11
- * @returns {SqlPrimitive}
12
- */
13
- export function applyBinaryOp(op, a, b) {
14
- // Arithmetic operators return null if either operand is null
15
- if (op === '+' || op === '-' || op === '*' || op === '/' || op === '%') {
16
- if (a == null || b == null) return null
17
- const numA = Number(a)
18
- const numB = Number(b)
19
- if (op === '+') return numA + numB
20
- if (op === '-') return numA - numB
21
- if (op === '*') return numA * numB
22
- if (op === '/') return numB === 0 ? null : numA / numB
23
- if (op === '%') return numB === 0 ? null : numA % numB
24
- }
25
-
26
- // Comparison and logical operators
27
- if (a == null || b == null) {
28
- return false
29
- }
30
- if (op === 'AND') return Boolean(a) && Boolean(b)
31
- if (op === 'OR') return Boolean(a) || Boolean(b)
32
- if (op === '!=' || op === '<>') return a != b
33
- if (op === '=') return a == b
34
- if (op === '<') return a < b
35
- if (op === '<=') return a <= b
36
- if (op === '>') return a > b
37
- if (op === '>=') return a >= b
38
-
39
- if (op === 'LIKE') {
40
- const str = String(a)
41
- const pattern = String(b)
42
- const regexPattern = pattern
43
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
44
- .replace(/%/g, '.*')
45
- .replace(/_/g, '.')
46
- const regex = new RegExp(`^${regexPattern}$`, 'i')
47
- return regex.test(str)
48
- }
49
-
50
- return null
51
- }
52
-
53
5
  /**
54
6
  * Compares two values for a single ORDER BY term, handling nulls and direction
55
7
  *
@@ -157,3 +109,20 @@ export function stringify(value) {
157
109
  return val
158
110
  })
159
111
  }
112
+
113
+ /**
114
+ * Creates a stable string key for a row to enable deduplication
115
+ *
116
+ * @param {AsyncCells} cells
117
+ * @returns {Promise<string>}
118
+ */
119
+ export async function stableRowKey(cells) {
120
+ const keys = Object.keys(cells).sort()
121
+ /** @type {string[]} */
122
+ const parts = []
123
+ for (const k of keys) {
124
+ const v = await cells[k]()
125
+ parts.push(k + ':' + stringify(v))
126
+ }
127
+ return parts.join('|')
128
+ }
@@ -11,15 +11,15 @@ export class ExecutionError extends Error {
11
11
  * @param {string} options.message - Human-readable error message
12
12
  * @param {number} options.positionStart - Start position (0-based character offset)
13
13
  * @param {number} options.positionEnd - End position (exclusive, 0-based character offset)
14
- * @param {number} [options.rowNumber] - 1-based row number where error occurred
14
+ * @param {number} [options.rowIndex] - 1-based row number where error occurred
15
15
  */
16
- constructor({ message, positionStart, positionEnd, rowNumber }) {
17
- const rowSuffix = rowNumber != null ? ` (row ${rowNumber})` : ''
16
+ constructor({ message, positionStart, positionEnd, rowIndex }) {
17
+ const rowSuffix = rowIndex != null ? ` (row ${rowIndex})` : ''
18
18
  super(message + rowSuffix)
19
19
  this.name = 'ExecutionError'
20
20
  this.positionStart = positionStart
21
21
  this.positionEnd = positionEnd
22
- this.rowNumber = rowNumber
22
+ this.rowIndex = rowIndex
23
23
  }
24
24
  }
25
25
 
@@ -40,11 +40,11 @@ export function tableNotFoundError({ tableName }) {
40
40
  * @param {string} options.validContext - Where it can be used
41
41
  * @param {number} options.positionStart - Start position in query
42
42
  * @param {number} options.positionEnd - End position in query
43
- * @param {number} [options.rowNumber] - 1-based row number where error occurred
43
+ * @param {number} [options.rowIndex] - 1-based row number where error occurred
44
44
  * @returns {ExecutionError}
45
45
  */
46
- export function invalidContextError({ item, validContext, positionStart, positionEnd, rowNumber }) {
47
- return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowNumber })
46
+ export function invalidContextError({ item, validContext, positionStart, positionEnd, rowIndex }) {
47
+ return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowIndex })
48
48
  }
49
49
 
50
50
  /**
@@ -64,10 +64,10 @@ export function unsupportedOperationError({ operation, hint }) {
64
64
  * @param {string[]} options.availableColumns - List of available column names
65
65
  * @param {number} options.positionStart - Start position in query
66
66
  * @param {number} options.positionEnd - End position in query
67
- * @param {number} [options.rowNumber] - 1-based row number where error occurred
67
+ * @param {number} [options.rowIndex] - 1-based row number where error occurred
68
68
  * @returns {ExecutionError}
69
69
  */
70
- export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd, rowNumber }) {
70
+ export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd, rowIndex }) {
71
71
  const available = availableColumns.length > 0
72
72
  ? `. Available columns: ${availableColumns.join(', ')}`
73
73
  : ''
@@ -75,6 +75,6 @@ export function columnNotFoundError({ columnName, availableColumns, positionStar
75
75
  message: `Column "${columnName}" not found${available}`,
76
76
  positionStart,
77
77
  positionEnd,
78
- rowNumber,
78
+ rowIndex,
79
79
  })
80
80
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @import { BinaryOp, SqlPrimitive } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * Applies a binary operator to two values, handling nulls according to SQL semantics
7
+ *
8
+ * @param {BinaryOp} op
9
+ * @param {SqlPrimitive} a
10
+ * @param {SqlPrimitive} b
11
+ * @returns {SqlPrimitive}
12
+ */
13
+ export function applyBinaryOp(op, a, b) {
14
+ // Arithmetic operators return null if either operand is null
15
+ if (op === '+' || op === '-' || op === '*' || op === '/' || op === '%') {
16
+ if (a == null || b == null) return null
17
+ const numA = Number(a)
18
+ const numB = Number(b)
19
+ if (op === '+') return numA + numB
20
+ if (op === '-') return numA - numB
21
+ if (op === '*') return numA * numB
22
+ if (op === '/') return numB === 0 ? null : numA / numB
23
+ if (op === '%') return numB === 0 ? null : numA % numB
24
+ }
25
+
26
+ // Comparison and logical operators
27
+ if (a == null || b == null) {
28
+ return false
29
+ }
30
+ if (op === 'AND') return Boolean(a) && Boolean(b)
31
+ if (op === 'OR') return Boolean(a) || Boolean(b)
32
+ if (op === '!=' || op === '<>') return a != b
33
+ if (op === '=') return a == b
34
+ if (op === '<') return a < b
35
+ if (op === '<=') return a <= b
36
+ if (op === '>') return a > b
37
+ if (op === '>=') return a >= b
38
+
39
+ if (op === 'LIKE') {
40
+ const str = String(a)
41
+ const pattern = String(b)
42
+ const regexPattern = pattern
43
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
44
+ .replace(/%/g, '.*')
45
+ .replace(/_/g, '.')
46
+ const regex = new RegExp(`^${regexPattern}$`, 'i')
47
+ return regex.test(str)
48
+ }
49
+
50
+ return null
51
+ }
@@ -2,29 +2,13 @@
2
2
  * @import { SqlPrimitive, IntervalUnit } from '../types.js'
3
3
  */
4
4
 
5
- /**
6
- * @param {SqlPrimitive} val
7
- * @returns {Date | null}
8
- */
9
- function toDate(val) {
10
- if (val instanceof Date) return val
11
- const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
12
- if (typeof val === 'string' && dateOrTime.test(val)) {
13
- const date = new Date(val)
14
- if (!isNaN(date.getTime())) {
15
- return date
16
- }
17
- }
18
- return null
19
- }
20
-
21
5
  /**
22
6
  * Apply an interval to a date
23
7
  * @param {SqlPrimitive} dateVal
24
8
  * @param {number} value
25
9
  * @param {IntervalUnit} unit
26
10
  * @param {'+' | '-'} op
27
- * @returns {string | null}
11
+ * @returns {Date | string | null}
28
12
  */
29
13
  export function applyIntervalToDate(dateVal, value, unit, op) {
30
14
  const date = toDate(dateVal)
@@ -48,10 +32,26 @@ export function applyIntervalToDate(dateVal, value, unit, op) {
48
32
  }
49
33
 
50
34
  // Return in same format as input
51
- if (dateVal instanceof Date) return date.toISOString()
35
+ if (dateVal instanceof Date) return date
52
36
  if (String(dateVal).includes('T')) {
53
37
  return date.toISOString()
54
38
  } else {
55
39
  return date.toISOString().split('T')[0]
56
40
  }
57
41
  }
42
+
43
+ /**
44
+ * @param {SqlPrimitive} val
45
+ * @returns {Date | null}
46
+ */
47
+ function toDate(val) {
48
+ if (val instanceof Date) return val
49
+ const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
50
+ if (typeof val === 'string' && dateOrTime.test(val)) {
51
+ const date = new Date(val)
52
+ if (!isNaN(date.getTime())) {
53
+ return date
54
+ }
55
+ }
56
+ return null
57
+ }
@@ -1,13 +1,14 @@
1
- import { unknownFunctionError } from '../parseErrors.js'
1
+ import { executeSelect } from '../execute/execute.js'
2
+ import { stringify } from '../execute/utils.js'
2
3
  import { columnNotFoundError, invalidContextError } from '../executionErrors.js'
4
+ import { unknownFunctionError } from '../parseErrors.js'
3
5
  import { aggregateError, argValueError, castError } from '../validationErrors.js'
4
6
  import { isAggregateFunc, isMathFunc, isRegexpFunc, isStringFunc } from '../validation.js'
7
+ import { applyBinaryOp } from './binary.js'
5
8
  import { applyIntervalToDate } from './date.js'
6
- import { executeSelect } from './execute.js'
7
9
  import { evaluateMathFunc } from './math.js'
8
10
  import { evaluateRegexpFunc } from './regexp.js'
9
11
  import { evaluateStringFunc } from './strings.js'
10
- import { applyBinaryOp, stringify } from './utils.js'
11
12
 
12
13
  /**
13
14
  * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, UserDefinedFunction } from '../types.js'
@@ -24,9 +25,10 @@ import { applyBinaryOp, stringify } from './utils.js'
24
25
  * @param {number} [params.rowIndex] - 1-based row index for error reporting
25
26
  * @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
26
27
  * @param {Map<string, ExprNode>} [params.aliases] - SELECT column aliases for ORDER BY resolution
28
+ * @param {AbortSignal} [params.signal] - abort signal for cancellation
27
29
  * @returns {Promise<SqlPrimitive>} The result of the evaluation
28
30
  */
29
- export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases }) {
31
+ export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases, signal }) {
30
32
  if (node.type === 'literal') {
31
33
  return node.value
32
34
  }
@@ -45,7 +47,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
45
47
  }
46
48
  // Check if it's a SELECT alias (for ORDER BY)
47
49
  if (aliases?.has(node.name)) {
48
- return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases })
50
+ return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases, signal })
49
51
  }
50
52
  // Unknown identifier
51
53
  throw columnNotFoundError({
@@ -53,13 +55,13 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
53
55
  availableColumns: Object.keys(row.cells),
54
56
  positionStart: node.positionStart,
55
57
  positionEnd: node.positionEnd,
56
- rowNumber: rowIndex,
58
+ rowIndex,
57
59
  })
58
60
  }
59
61
 
60
62
  // Scalar subquery - returns a single value
61
63
  if (node.type === 'subquery') {
62
- const gen = executeSelect({ select: node.subquery, tables })
64
+ const gen = executeSelect({ select: node.subquery, tables, functions, signal })
63
65
  const { value } = await gen.next() // Start the generator
64
66
  gen.return(undefined) // Stop further execution
65
67
  if (!value) return null
@@ -68,45 +70,35 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
68
70
 
69
71
  // Unary operators
70
72
  if (node.type === 'unary') {
71
- if (node.op === 'NOT') {
72
- return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
73
- }
74
- if (node.op === 'IS NULL') {
75
- return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) == null
76
- }
77
- if (node.op === 'IS NOT NULL') {
78
- return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) != null
79
- }
73
+ const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases, signal })
80
74
  if (node.op === '-') {
81
- const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
82
75
  if (val == null) return null
83
76
  return -val
84
77
  }
78
+ if (node.op === 'NOT') return !val
79
+ if (node.op === 'IS NULL') return val == null
80
+ if (node.op === 'IS NOT NULL') return val != null
85
81
  }
86
82
 
87
83
  // Binary operators
88
84
  if (node.type === 'binary') {
89
- // Handle date +/- interval at AST level
85
+ // Handle date +/- interval
90
86
  if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
91
- const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
87
+ const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases, signal })
92
88
  return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
93
89
  }
94
90
  if (node.op === '+' && node.left.type === 'interval') {
95
- const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
91
+ const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases, signal })
96
92
  return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
97
93
  }
98
94
 
99
- const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
95
+ const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases, signal })
100
96
 
101
97
  // Short-circuit evaluation for AND and OR
102
- if (node.op === 'AND') {
103
- if (!left) return false
104
- }
105
- if (node.op === 'OR') {
106
- if (left) return true
107
- }
98
+ if (node.op === 'AND' && !left) return false
99
+ if (node.op === 'OR' && left) return true
108
100
 
109
- const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
101
+ const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases, signal })
110
102
  return applyBinaryOp(node.op, left, right)
111
103
  }
112
104
 
@@ -119,7 +111,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
119
111
  if (!rows) {
120
112
  throw aggregateError({
121
113
  funcName,
122
- issue: 'requires GROUP BY or will act on the whole dataset',
114
+ issue: ' is not allowed outside of aggregate context',
123
115
  })
124
116
  }
125
117
 
@@ -128,7 +120,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
128
120
  if (node.filter) {
129
121
  filteredRows = []
130
122
  for (const row of rows) {
131
- const passes = await evaluateExpr({ node: node.filter, row, tables, functions })
123
+ const passes = await evaluateExpr({ node: node.filter, row, tables, functions, signal })
132
124
  if (passes) filteredRows.push(row)
133
125
  }
134
126
  }
@@ -140,7 +132,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
140
132
  }
141
133
  throw aggregateError({
142
134
  funcName,
143
- issue: '(*) is not supported, use a column name',
135
+ issue: '(*) is not supported. Only COUNT supports *.',
144
136
  })
145
137
  }
146
138
 
@@ -150,14 +142,14 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
150
142
  if (node.distinct) {
151
143
  const seen = new Set()
152
144
  for (const row of filteredRows) {
153
- const v = await evaluateExpr({ node: argNode, row, tables, functions })
145
+ const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
154
146
  if (v != null) seen.add(v)
155
147
  }
156
148
  return seen.size
157
149
  }
158
150
  let count = 0
159
151
  for (const row of filteredRows) {
160
- const v = await evaluateExpr({ node: argNode, row, tables, functions })
152
+ const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
161
153
  if (v != null) count++
162
154
  }
163
155
  return count
@@ -172,7 +164,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
172
164
  let max = null
173
165
 
174
166
  for (const row of filteredRows) {
175
- const raw = await evaluateExpr({ node: argNode, row, tables, functions })
167
+ const raw = await evaluateExpr({ node: argNode, row, tables, functions, signal })
176
168
  if (raw == null) continue
177
169
  const num = Number(raw)
178
170
  if (!Number.isFinite(num)) continue
@@ -197,7 +189,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
197
189
  if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
198
190
  const values = []
199
191
  for (const row of filteredRows) {
200
- const raw = await evaluateExpr({ node: argNode, row, tables, functions })
192
+ const raw = await evaluateExpr({ node: argNode, row, tables, functions, signal })
201
193
  if (raw == null) continue
202
194
  const num = Number(raw)
203
195
  if (!Number.isFinite(num)) continue
@@ -219,7 +211,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
219
211
  if (node.distinct) {
220
212
  const seen = new Set()
221
213
  for (const row of filteredRows) {
222
- const v = await evaluateExpr({ node: argNode, row, tables, functions })
214
+ const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
223
215
  const key = stringify(v)
224
216
  if (!seen.has(key)) {
225
217
  seen.add(key)
@@ -228,7 +220,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
228
220
  }
229
221
  } else {
230
222
  for (const row of filteredRows) {
231
- const v = await evaluateExpr({ node: argNode, row, tables, functions })
223
+ const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
232
224
  values.push(v)
233
225
  }
234
226
  }
@@ -237,7 +229,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
237
229
  }
238
230
 
239
231
  /** @type {SqlPrimitive[]} */
240
- const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases })))
232
+ const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases, signal })))
241
233
 
242
234
  if (isStringFunc(funcName)) {
243
235
  return evaluateStringFunc({
@@ -259,15 +251,26 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
259
251
  })
260
252
  }
261
253
 
254
+ if (isMathFunc(funcName)) {
255
+ return evaluateMathFunc({ funcName, args })
256
+ }
257
+
262
258
  if (funcName === 'COALESCE') {
263
259
  // Short-circuit: evaluate args one at a time, return first non-null
264
260
  for (const arg of node.args) {
265
- const val = await evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows })
261
+ const val = await evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, signal })
266
262
  if (val != null) return val
267
263
  }
268
264
  return null
269
265
  }
270
266
 
267
+ if (funcName === 'NULLIF') {
268
+ // NULLIF(a, b) returns null if a = b, otherwise returns a
269
+ const val1 = await evaluateExpr({ node: node.args[0], row, tables, functions, rowIndex, rows, signal })
270
+ const val2 = await evaluateExpr({ node: node.args[1], row, tables, functions, rowIndex, rows, signal })
271
+ return val1 == val2 ? null : val1
272
+ }
273
+
271
274
  if (funcName === 'CURRENT_DATE') {
272
275
  return new Date().toISOString().split('T')[0]
273
276
  }
@@ -287,7 +290,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
287
290
  message: 'requires an even number of arguments (key-value pairs)',
288
291
  positionStart: node.positionStart,
289
292
  positionEnd: node.positionEnd,
290
- rowNumber: rowIndex,
293
+ rowIndex,
291
294
  })
292
295
  }
293
296
  /** @type {Record<string, SqlPrimitive>} */
@@ -302,7 +305,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
302
305
  positionStart: node.positionStart,
303
306
  positionEnd: node.positionEnd,
304
307
  hint: 'All keys must be non-null values.',
305
- rowNumber: rowIndex,
308
+ rowIndex,
306
309
  })
307
310
  }
308
311
  result[String(key)] = value
@@ -326,7 +329,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
326
329
  positionStart: node.positionStart,
327
330
  positionEnd: node.positionEnd,
328
331
  hint: 'First argument must be valid JSON.',
329
- rowNumber: rowIndex,
332
+ rowIndex,
330
333
  })
331
334
  }
332
335
  }
@@ -336,7 +339,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
336
339
  message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
337
340
  positionStart: node.positionStart,
338
341
  positionEnd: node.positionEnd,
339
- rowNumber: rowIndex,
342
+ rowIndex,
340
343
  })
341
344
  }
342
345
 
@@ -366,10 +369,6 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
366
369
  return current
367
370
  }
368
371
 
369
- if (isMathFunc(funcName)) {
370
- return evaluateMathFunc({ funcName, args })
371
- }
372
-
373
372
  // Check user-defined functions (case-insensitive lookup)
374
373
  if (functions) {
375
374
  const udfName = Object.keys(functions).find(k => k.toUpperCase() === funcName)
@@ -386,7 +385,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
386
385
  }
387
386
 
388
387
  if (node.type === 'cast') {
389
- const val = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
388
+ const val = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
390
389
  if (val == null) return null
391
390
  const toType = node.toType.toUpperCase()
392
391
  if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
@@ -400,7 +399,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
400
399
  positionStart: node.positionStart,
401
400
  positionEnd: node.positionEnd,
402
401
  fromType: 'object',
403
- rowNumber: rowIndex,
402
+ rowIndex,
404
403
  })
405
404
  }
406
405
  if (toType === 'INTEGER' || toType === 'INT') {
@@ -423,23 +422,23 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
423
422
  toType: node.toType,
424
423
  positionStart: node.positionStart,
425
424
  positionEnd: node.positionEnd,
426
- rowNumber: rowIndex,
425
+ rowIndex,
427
426
  })
428
427
  }
429
428
 
430
429
  // IN and NOT IN with value lists
431
430
  if (node.type === 'in valuelist') {
432
- const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
431
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
433
432
  for (const valueNode of node.values) {
434
- const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows })
433
+ const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows, signal })
435
434
  if (exprVal == val) return true
436
435
  }
437
436
  return false
438
437
  }
439
438
  // IN with subqueries
440
439
  if (node.type === 'in') {
441
- const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
442
- const results = executeSelect({ select: node.subquery, tables })
440
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
441
+ const results = executeSelect({ select: node.subquery, tables, functions, signal })
443
442
  for await (const resRow of results) {
444
443
  const value = await resRow.cells[resRow.columns[0]]()
445
444
  if (exprVal == value) return true
@@ -449,39 +448,39 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
449
448
 
450
449
  // EXISTS and NOT EXISTS with subqueries
451
450
  if (node.type === 'exists') {
452
- const results = await executeSelect({ select: node.subquery, tables }).next()
451
+ const results = await executeSelect({ select: node.subquery, tables, functions, signal }).next()
453
452
  return results.done === false
454
453
  }
455
454
  if (node.type === 'not exists') {
456
- const results = await executeSelect({ select: node.subquery, tables }).next()
455
+ const results = await executeSelect({ select: node.subquery, tables, functions, signal }).next()
457
456
  return results.done === true
458
457
  }
459
458
 
460
459
  // CASE expressions
461
460
  if (node.type === 'case') {
462
461
  // For simple CASE: evaluate the case expression once
463
- const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, functions, rowIndex, rows })
462
+ const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, functions, rowIndex, rows, signal })
464
463
 
465
464
  // Iterate through WHEN clauses
466
465
  for (const whenClause of node.whenClauses) {
467
466
  let conditionResult
468
467
  if (caseValue !== undefined) {
469
468
  // Simple CASE: compare caseValue with condition
470
- const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
469
+ const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows, signal })
471
470
  conditionResult = caseValue == whenValue
472
471
  } else {
473
472
  // Searched CASE: evaluate condition as boolean
474
- conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
473
+ conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows, signal })
475
474
  }
476
475
 
477
476
  if (conditionResult) {
478
- return evaluateExpr({ node: whenClause.result, row, tables, functions, rowIndex, rows })
477
+ return evaluateExpr({ node: whenClause.result, row, tables, functions, rowIndex, rows, signal })
479
478
  }
480
479
  }
481
480
 
482
481
  // No WHEN clause matched, return ELSE result or NULL
483
482
  if (node.elseResult) {
484
- return evaluateExpr({ node: node.elseResult, row, tables, functions, rowIndex, rows })
483
+ return evaluateExpr({ node: node.elseResult, row, tables, functions, rowIndex, rows, signal })
485
484
  }
486
485
  return null
487
486
  }
@@ -494,7 +493,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
494
493
  validContext: 'date arithmetic (+ or -)',
495
494
  positionStart: node.positionStart,
496
495
  positionEnd: node.positionEnd,
497
- rowNumber: rowIndex,
496
+ rowIndex,
498
497
  })
499
498
  }
500
499