squirreling 0.10.2 → 0.11.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,5 +1,5 @@
1
1
  import { evaluateExpr } from '../expression/evaluate.js'
2
- import { stringify } from './utils.js'
2
+ import { keyify } from './utils.js'
3
3
  import { executePlan } from './execute.js'
4
4
 
5
5
  /**
@@ -26,13 +26,12 @@ export async function* executeNestedLoopJoin(plan, context) {
26
26
  rightRows.push(row)
27
27
  }
28
28
 
29
- const rightCols = rightRows.length ? rightRows[0].columns : []
30
- const rightPrefixedCols = prefixColumns(rightCols, rightTable)
29
+ const rightPrefixedCols = rightRows.length ? prefixColumns(rightRows[0].columns, rightTable) : []
31
30
 
32
- /** @type {string[] | null} */
33
- let leftPrefixedCols = null
34
- /** @type {Set<AsyncRow> | null} */
35
- const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : null
31
+ /** @type {string[] | undefined} */
32
+ let leftPrefixedCols = undefined
33
+ /** @type {Set<AsyncRow> | undefined} */
34
+ const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
36
35
 
37
36
  for await (const leftRow of executePlan({ plan: plan.left, context })) {
38
37
  if (context.signal?.aborted) break
@@ -53,7 +52,7 @@ export async function* executeNestedLoopJoin(plan, context) {
53
52
 
54
53
  if (matches) {
55
54
  hasMatch = true
56
- if (matchedRightRows) matchedRightRows.add(rightRow)
55
+ matchedRightRows?.add(rightRow)
57
56
  yield tempMerged
58
57
  }
59
58
  }
@@ -68,7 +67,7 @@ export async function* executeNestedLoopJoin(plan, context) {
68
67
  if (matchedRightRows) {
69
68
  for (const rightRow of rightRows) {
70
69
  if (!matchedRightRows.has(rightRow)) {
71
- const nullLeft = createNullRow(leftPrefixedCols || [])
70
+ const nullLeft = createNullRow(leftPrefixedCols ?? [])
72
71
  yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
73
72
  }
74
73
  }
@@ -135,7 +134,7 @@ export async function* executeHashJoin(plan, context) {
135
134
  rightRows.push(row)
136
135
  }
137
136
 
138
- /** @type {Map<string, AsyncRow[]>} */
137
+ /** @type {Map<any, AsyncRow[]>} */
139
138
  const hashMap = new Map()
140
139
  for (const rightRow of rightRows) {
141
140
  const keyValue = await evaluateExpr({
@@ -144,11 +143,11 @@ export async function* executeHashJoin(plan, context) {
144
143
  context,
145
144
  })
146
145
  if (keyValue == null) continue
147
- const keyStr = stringify(keyValue)
148
- let bucket = hashMap.get(keyStr)
146
+ const key = keyify(keyValue)
147
+ let bucket = hashMap.get(key)
149
148
  if (!bucket) {
150
149
  bucket = []
151
- hashMap.set(keyStr, bucket)
150
+ hashMap.set(key, bucket)
152
151
  }
153
152
  bucket.push(rightRow)
154
153
  }
@@ -157,10 +156,10 @@ export async function* executeHashJoin(plan, context) {
157
156
  const rightCols = rightRows.length ? rightRows[0].columns : []
158
157
  const rightPrefixedCols = prefixColumns(rightCols, rightTable)
159
158
 
160
- /** @type {string[] | null} */
161
- let leftPrefixedCols = null
162
- /** @type {Set<AsyncRow> | null} */
163
- const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : null
159
+ /** @type {string[] | undefined} */
160
+ let leftPrefixedCols
161
+ /** @type {Set<AsyncRow> | undefined} */
162
+ const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
164
163
 
165
164
  // Probe phase: stream left rows
166
165
  for await (const leftRow of executePlan({ plan: plan.left, context })) {
@@ -175,12 +174,12 @@ export async function* executeHashJoin(plan, context) {
175
174
  row: leftRow,
176
175
  context,
177
176
  })
178
- const keyStr = stringify(keyValue)
179
- const matchingRightRows = hashMap.get(keyStr)
177
+ const key = keyify(keyValue)
178
+ const matchingRightRows = hashMap.get(key)
180
179
 
181
180
  if (matchingRightRows?.length) {
182
181
  for (const rightRow of matchingRightRows) {
183
- if (matchedRightRows) matchedRightRows.add(rightRow)
182
+ matchedRightRows?.add(rightRow)
184
183
  yield mergeRows(leftRow, rightRow, leftTable, rightTable)
185
184
  }
186
185
  } else if (plan.joinType === 'LEFT' || plan.joinType === 'FULL') {
@@ -193,7 +192,7 @@ export async function* executeHashJoin(plan, context) {
193
192
  if (matchedRightRows) {
194
193
  for (const rightRow of rightRows) {
195
194
  if (!matchedRightRows.has(rightRow)) {
196
- const nullLeft = createNullRow(leftPrefixedCols || [])
195
+ const nullLeft = createNullRow(leftPrefixedCols ?? [])
197
196
  yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
198
197
  }
199
198
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { AsyncCells, AsyncRow, OrderByItem, SqlPrimitive } from '../types.js'
2
+ * @import { AsyncRow, OrderByItem, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
  /**
@@ -76,14 +76,26 @@ export function stringify(value) {
76
76
  })
77
77
  }
78
78
 
79
+ /**
80
+ * Returns a value suitable for use as a Set/Map key.
81
+ * Primitives are returned as-is (fast path), objects are stringified.
82
+ *
83
+ * @param {SqlPrimitive[]} values
84
+ * @returns {string | number | bigint | boolean}
85
+ */
86
+ export function keyify(...values) {
87
+ if (values.length === 1 && typeof values[0] !== 'object') return values[0]
88
+ // Strings must be stringified to avoid collisions when joined
89
+ return values.map(v => typeof v === 'object' ? stringify(v) : v).join('|')
90
+ }
91
+
79
92
  /**
80
93
  * Creates a stable string key for a row to enable deduplication
81
94
  *
82
- * @param {AsyncCells} cells
83
- * @returns {Promise<string>}
95
+ * @param {AsyncRow} row
96
+ * @returns {Promise<string | number | bigint | boolean>}
84
97
  */
85
- export async function stableRowKey(cells) {
86
- const keys = Object.keys(cells).sort()
87
- const values = await Promise.all(keys.map(k => cells[k]()))
88
- return keys.map((k, i) => k + ':' + stringify(values[i])).join('|')
98
+ export function stableRowKey(row) {
99
+ return Promise.all(row.columns.map(k => row.cells[k]()))
100
+ .then(values => keyify(...values))
89
101
  }
@@ -11,8 +11,9 @@
11
11
  export function derivedAlias(expr) {
12
12
  if (expr.type === 'identifier') {
13
13
  // For qualified names like 'users.name', use just the column part as alias
14
- if (expr.name.includes('.')) {
15
- return expr.name.split('.').pop()
14
+ const dotIndex = expr.name.indexOf('.')
15
+ if (dotIndex >= 0) {
16
+ return expr.name.substring(dotIndex + 1)
16
17
  }
17
18
  return expr.name
18
19
  }
@@ -1,10 +1,9 @@
1
- import { executeSelect } from '../execute/execute.js'
2
- import { stringify } from '../execute/utils.js'
3
- import { invalidContextError } from '../validation/executionErrors.js'
4
- import { aggregateError, argValueError, castError } from '../validation/expressionErrors.js'
1
+ import { executeStatement } from '../execute/execute.js'
2
+ import { keyify, stringify } from '../execute/utils.js'
3
+ import { ArgValueError, ExecutionError } from '../validation/executionErrors.js'
5
4
  import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
6
- import { unknownFunctionError } from '../validation/parseErrors.js'
7
- import { columnNotFoundError } from '../validation/planErrors.js'
5
+ import { UnknownFunctionError } from '../validation/parseErrors.js'
6
+ import { ColumnNotFoundError } from '../validation/planErrors.js'
8
7
  import { derivedAlias } from './alias.js'
9
8
  import { applyBinaryOp } from './binary.js'
10
9
  import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
@@ -46,18 +45,17 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
46
45
  }
47
46
  }
48
47
  // Unknown identifier
49
- throw columnNotFoundError({
48
+ throw new ColumnNotFoundError({
50
49
  columnName: node.name,
51
50
  availableColumns: row.columns,
52
- positionStart: node.positionStart,
53
- positionEnd: node.positionEnd,
54
51
  rowIndex,
52
+ ...node,
55
53
  })
56
54
  }
57
55
 
58
56
  // Scalar subquery - returns a single value
59
57
  if (node.type === 'subquery') {
60
- const gen = executeSelect({ select: node.subquery, context })
58
+ const gen = executeStatement({ query: node.subquery, context })
61
59
  const { value } = await gen.next() // Start the generator
62
60
  gen.return(undefined) // Stop further execution
63
61
  if (!value) return null
@@ -111,15 +109,19 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
111
109
  if (row.columns.includes(alias)) {
112
110
  return row.cells[alias]()
113
111
  } else {
114
- throw aggregateError(node)
112
+ throw new ExecutionError({
113
+ message: `Aggregate function ${funcName} is not available in this context`,
114
+ ...node,
115
+ })
115
116
  }
116
117
  }
117
118
 
118
119
  // Apply FILTER clause if present
119
120
  let filteredRows = rows
120
121
  if (node.filter) {
122
+ const filterNode = node.filter
121
123
  const passes = await Promise.all(rows.map(row =>
122
- evaluateExpr({ node: node.filter, row, context })
124
+ evaluateExpr({ node: filterNode, row, context })
123
125
  ))
124
126
  filteredRows = rows.filter((_, i) => passes[i])
125
127
  }
@@ -137,7 +139,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
137
139
  if (node.distinct) {
138
140
  const seen = new Set()
139
141
  for (const v of values) {
140
- if (v != null) seen.add(v)
142
+ if (v != null) seen.add(keyify(v))
141
143
  }
142
144
  return seen.size
143
145
  }
@@ -154,23 +156,17 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
154
156
  ))
155
157
  let sum = 0
156
158
  let count = 0
157
- /** @type {number | null} */
159
+ /** @type {SqlPrimitive} */
158
160
  let min = null
159
- /** @type {number | null} */
161
+ /** @type {SqlPrimitive} */
160
162
  let max = null
161
163
 
162
164
  for (const raw of rawValues) {
163
165
  if (raw == null) continue
166
+ if (min === null || raw < min) min = raw
167
+ if (max === null || raw > max) max = raw
164
168
  const num = Number(raw)
165
169
  if (!Number.isFinite(num)) continue
166
-
167
- if (count === 0) {
168
- min = num
169
- max = num
170
- } else {
171
- if (min == null || num < min) min = num
172
- if (max == null || num > max) max = num
173
- }
174
170
  sum += num
175
171
  count++
176
172
  }
@@ -185,6 +181,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
185
181
  const rawValues = await Promise.all(filteredRows.map(row =>
186
182
  evaluateExpr({ node: argNode, row, context })
187
183
  ))
184
+ let sum = 0
188
185
  /** @type {number[]} */
189
186
  const values = []
190
187
  for (const raw of rawValues) {
@@ -192,37 +189,77 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
192
189
  const num = Number(raw)
193
190
  if (!Number.isFinite(num)) continue
194
191
  values.push(num)
192
+ sum += num
195
193
  }
196
194
  const n = values.length
197
195
  if (n === 0) return null
198
196
  if (funcName === 'STDDEV_SAMP' && n === 1) return null
199
197
 
200
- const mean = values.reduce((a, b) => a + b, 0) / n
198
+ const mean = sum / n
201
199
  const squaredDiffs = values.reduce((acc, val) => acc + (val - mean) ** 2, 0)
202
200
  const divisor = funcName === 'STDDEV_SAMP' ? n - 1 : n
203
201
  return Math.sqrt(squaredDiffs / divisor)
204
202
  }
205
203
 
206
- if (funcName === 'JSON_ARRAYAGG') {
207
- /** @type {SqlPrimitive[]} */
204
+ if (funcName === 'MEDIAN' || funcName === 'PERCENTILE_CONT' || funcName === 'APPROX_QUANTILE') {
205
+ let fraction
206
+ let valueNode
207
+ if (funcName === 'MEDIAN') {
208
+ fraction = 0.5
209
+ valueNode = argNode
210
+ } else if (funcName === 'PERCENTILE_CONT') {
211
+ fraction = Number(await evaluateExpr({ node: node.args[0], row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
212
+ valueNode = node.args[1]
213
+ } else {
214
+ // APPROX_QUANTILE: (expression, fraction)
215
+ fraction = Number(await evaluateExpr({ node: node.args[1], row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
216
+ valueNode = argNode
217
+ }
218
+ if (!Number.isFinite(fraction) || fraction < 0 || fraction > 1) {
219
+ throw new ExecutionError({
220
+ message: `${funcName}: fraction must be between 0 and 1, got ${fraction}`,
221
+ ...node,
222
+ })
223
+ }
224
+ const rawValues = await Promise.all(filteredRows.map(row =>
225
+ evaluateExpr({ node: valueNode, row, context })
226
+ ))
227
+ /** @type {number[]} */
208
228
  const values = []
229
+ for (const raw of rawValues) {
230
+ if (raw == null) continue
231
+ const num = Number(raw)
232
+ if (!Number.isFinite(num)) continue
233
+ values.push(num)
234
+ }
235
+ if (values.length === 0) return null
236
+ values.sort((a, b) => a - b)
237
+ const pos = fraction * (values.length - 1)
238
+ const lower = Math.floor(pos)
239
+ const upper = Math.ceil(pos)
240
+ if (lower === upper) return values[lower]
241
+ return values[lower] + (values[upper] - values[lower]) * (pos - lower)
242
+ }
243
+
244
+ if (funcName === 'JSON_ARRAYAGG') {
209
245
  if (node.distinct) {
246
+ /** @type {SqlPrimitive[]} */
247
+ const values = []
210
248
  const seen = new Set()
211
249
  for (const row of filteredRows) {
212
250
  const v = await evaluateExpr({ node: argNode, row, context })
213
- const key = stringify(v)
251
+ const key = keyify(v)
214
252
  if (!seen.has(key)) {
215
253
  seen.add(key)
216
254
  values.push(v)
217
255
  }
218
256
  }
257
+ return values
219
258
  } else {
220
- for (const row of filteredRows) {
221
- const v = await evaluateExpr({ node: argNode, row, context })
222
- values.push(v)
223
- }
259
+ return await Promise.all(filteredRows.map(row =>
260
+ evaluateExpr({ node: argNode, row, context })
261
+ ))
224
262
  }
225
- return values
226
263
  }
227
264
  }
228
265
 
@@ -258,9 +295,9 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
258
295
 
259
296
  if (funcName === 'NULLIF') {
260
297
  // NULLIF(a, b) returns null if a = b, otherwise returns a
298
+ const val2 = evaluateExpr({ node: node.args[1], row, rowIndex, rows, context })
261
299
  const val1 = await evaluateExpr({ node: node.args[0], row, rowIndex, rows, context })
262
- const val2 = await evaluateExpr({ node: node.args[1], row, rowIndex, rows, context })
263
- return val1 == val2 ? null : val1
300
+ return val1 == await val2 ? null : val1
264
301
  }
265
302
 
266
303
  if (funcName === 'DATE_TRUNC') {
@@ -285,7 +322,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
285
322
 
286
323
  if (funcName === 'JSON_OBJECT') {
287
324
  if (args.length % 2 !== 0) {
288
- throw argValueError({
325
+ throw new ArgValueError({
289
326
  ...node,
290
327
  message: 'requires an even number of arguments (key-value pairs)',
291
328
  rowIndex,
@@ -297,7 +334,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
297
334
  const key = args[i]
298
335
  const value = args[i + 1]
299
336
  if (key == null) {
300
- throw argValueError({
337
+ throw new ArgValueError({
301
338
  ...node,
302
339
  message: 'key cannot be null',
303
340
  hint: 'All keys must be non-null values.',
@@ -335,7 +372,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
335
372
  })
336
373
  }
337
374
 
338
- if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
375
+ if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY' || funcName === 'JSON_EXTRACT') {
339
376
  let jsonArg = args[0]
340
377
  const pathArg = args[1]
341
378
  if (jsonArg == null || pathArg == null) return null
@@ -345,7 +382,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
345
382
  try {
346
383
  jsonArg = JSON.parse(jsonArg)
347
384
  } catch {
348
- throw argValueError({
385
+ throw new ArgValueError({
349
386
  ...node,
350
387
  message: 'invalid JSON string',
351
388
  hint: 'First argument must be valid JSON.',
@@ -354,7 +391,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
354
391
  }
355
392
  }
356
393
  if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
357
- throw argValueError({
394
+ throw new ArgValueError({
358
395
  ...node,
359
396
  message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
360
397
  rowIndex,
@@ -396,7 +433,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
396
433
  }
397
434
  }
398
435
 
399
- throw unknownFunctionError(node)
436
+ throw new UnknownFunctionError(node)
400
437
  }
401
438
 
402
439
  if (node.type === 'cast') {
@@ -409,7 +446,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
409
446
  }
410
447
  // Can only cast primitives to other primitive types
411
448
  if (typeof val === 'object') {
412
- throw castError({ ...node, fromType: 'object', rowIndex })
449
+ throw new ExecutionError({ message: `Cannot CAST object to ${toType}`, rowIndex, ...node })
413
450
  }
414
451
  if (toType === 'INTEGER' || toType === 'INT') {
415
452
  const num = Number(val)
@@ -427,7 +464,6 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
427
464
  if (toType === 'BOOLEAN' || toType === 'BOOL') {
428
465
  return Boolean(val)
429
466
  }
430
- throw castError({ ...node, rowIndex })
431
467
  }
432
468
 
433
469
  // IN and NOT IN with value lists
@@ -442,7 +478,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
442
478
  // IN with subqueries
443
479
  if (node.type === 'in') {
444
480
  const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
445
- const results = executeSelect({ select: node.subquery, context })
481
+ const results = executeStatement({ query: node.subquery, context })
446
482
  for await (const resRow of results) {
447
483
  const value = await resRow.cells[resRow.columns[0]]()
448
484
  if (exprVal == value) return true
@@ -452,11 +488,11 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
452
488
 
453
489
  // EXISTS and NOT EXISTS with subqueries
454
490
  if (node.type === 'exists') {
455
- const results = await executeSelect({ select: node.subquery, context }).next()
491
+ const results = await executeStatement({ query: node.subquery, context }).next()
456
492
  return results.done === false
457
493
  }
458
494
  if (node.type === 'not exists') {
459
- const results = await executeSelect({ select: node.subquery, context }).next()
495
+ const results = await executeStatement({ query: node.subquery, context }).next()
460
496
  return results.done === true
461
497
  }
462
498
 
@@ -467,17 +503,9 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
467
503
 
468
504
  // Iterate through WHEN clauses
469
505
  for (const whenClause of node.whenClauses) {
470
- let conditionResult
471
- if (caseValue !== undefined) {
472
- // Simple CASE: compare caseValue with condition
473
- const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
474
- conditionResult = caseValue == whenValue
475
- } else {
476
- // Searched CASE: evaluate condition as boolean
477
- conditionResult = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
478
- }
479
-
480
- if (conditionResult) {
506
+ const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
507
+ // compare caseValue with condition or evaluate as boolean
508
+ if (caseValue !== undefined ? caseValue == whenValue : whenValue) {
481
509
  return evaluateExpr({ node: whenClause.result, row, rowIndex, rows, context })
482
510
  }
483
511
  }
@@ -492,12 +520,10 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
492
520
  // INTERVAL expressions should only appear as part of binary +/- operations
493
521
  // which are handled above. A standalone interval is an error.
494
522
  if (node.type === 'interval') {
495
- throw invalidContextError({
496
- item: 'INTERVAL',
497
- validContext: 'date arithmetic (+ or -)',
498
- positionStart: node.positionStart,
499
- positionEnd: node.positionEnd,
523
+ throw new ExecutionError({
524
+ message: 'INTERVAL can only be used with date arithmetic (+ or -)',
500
525
  rowIndex,
526
+ ...node,
501
527
  })
502
528
  }
503
529
 
@@ -122,4 +122,6 @@ export function evaluateMathFunc({ funcName, args }) {
122
122
  if (funcName === 'RADIANS') {
123
123
  return Number(val) * Math.PI / 180
124
124
  }
125
+
126
+ throw new Error(`Unsupported math function: ${funcName}`)
125
127
  }
@@ -1,4 +1,4 @@
1
- import { argValueError } from '../validation/expressionErrors.js'
1
+ import { ArgValueError } from '../validation/executionErrors.js'
2
2
 
3
3
  /**
4
4
  * @import { FunctionNode, RegExpFunction, SqlPrimitive } from '../types.js'
@@ -11,11 +11,11 @@ import { argValueError } from '../validation/expressionErrors.js'
11
11
  * @param {RegExpFunction} options.funcName
12
12
  * @param {FunctionNode} options.node
13
13
  * @param {SqlPrimitive[]} options.args - Function arguments
14
- * @param {number} options.rowIndex - Row index for error reporting
14
+ * @param {number} [options.rowIndex] - Row index for error reporting
15
15
  * @returns {SqlPrimitive}
16
16
  */
17
17
  export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
18
- if (funcName === 'REGEXP_SUBSTR') {
18
+ if (funcName === 'REGEXP_SUBSTR' || funcName === 'REGEXP_EXTRACT') {
19
19
  const str = args[0]
20
20
  const pattern = args[1]
21
21
  if (str == null || pattern == null) return null
@@ -27,7 +27,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
27
27
  if (args.length >= 3 && args[2] != null) {
28
28
  position = Number(args[2])
29
29
  if (!Number.isInteger(position) || position < 1) {
30
- throw argValueError({
30
+ throw new ArgValueError({
31
31
  ...node,
32
32
  message: `position must be a positive integer, got ${args[2]}`,
33
33
  hint: 'SQL uses 1-based indexing.',
@@ -41,7 +41,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
41
41
  if (args.length >= 4 && args[3] != null) {
42
42
  occurrence = Number(args[3])
43
43
  if (!Number.isInteger(occurrence) || occurrence < 1) {
44
- throw argValueError({
44
+ throw new ArgValueError({
45
45
  ...node,
46
46
  message: `occurrence must be a positive integer, got ${args[3]}`,
47
47
  hint: 'SQL uses 1-based indexing.',
@@ -55,7 +55,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
55
55
  try {
56
56
  regex = new RegExp(patternStr, 'g')
57
57
  } catch (/** @type {any} */ error) {
58
- throw argValueError({
58
+ throw new ArgValueError({
59
59
  ...node,
60
60
  message: `invalid regex pattern: ${error.message}`,
61
61
  rowIndex,
@@ -92,7 +92,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
92
92
  if (args.length >= 4 && args[3] != null) {
93
93
  position = Number(args[3])
94
94
  if (!Number.isInteger(position) || position < 1) {
95
- throw argValueError({
95
+ throw new ArgValueError({
96
96
  ...node,
97
97
  message: `position must be a positive integer, got ${args[3]}`,
98
98
  hint: 'SQL uses 1-based indexing.',
@@ -106,7 +106,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
106
106
  if (args.length >= 5 && args[4] != null) {
107
107
  occurrence = Number(args[4])
108
108
  if (!Number.isInteger(occurrence) || occurrence < 0) {
109
- throw argValueError({
109
+ throw new ArgValueError({
110
110
  ...node,
111
111
  message: `occurrence must be a non-negative integer, got ${args[4]}`,
112
112
  hint: 'Use 0 to replace all occurrences.',
@@ -120,7 +120,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
120
120
  try {
121
121
  regex = new RegExp(patternStr, 'g')
122
122
  } catch (/** @type {any} */ error) {
123
- throw argValueError({
123
+ throw new ArgValueError({
124
124
  ...node,
125
125
  message: `invalid regex pattern: ${error.message}`,
126
126
  rowIndex,
@@ -144,4 +144,6 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
144
144
  })
145
145
  return prefix + result
146
146
  }
147
+
148
+ throw new Error(`Unsupported regexp function: ${funcName}`)
147
149
  }
@@ -1,9 +1,9 @@
1
+ import { ArgValueError } from '../validation/executionErrors.js'
2
+
1
3
  /**
2
4
  * @import { FunctionNode, SqlPrimitive, StringFunc } from '../types.js'
3
5
  */
4
6
 
5
- import { argValueError } from '../validation/expressionErrors.js'
6
-
7
7
  /**
8
8
  * Evaluate a string function
9
9
  *
@@ -11,7 +11,7 @@ import { argValueError } from '../validation/expressionErrors.js'
11
11
  * @param {StringFunc} options.funcName
12
12
  * @param {FunctionNode} options.node
13
13
  * @param {SqlPrimitive[]} options.args - Function arguments
14
- * @param {number} options.rowIndex - Row index for error reporting
14
+ * @param {number} [options.rowIndex] - Row index for error reporting
15
15
  * @returns {SqlPrimitive}
16
16
  */
17
17
  export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
@@ -19,7 +19,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
19
19
  // Returns NULL if any argument is NULL
20
20
  if (args.some(a => a == null)) return null
21
21
  if (args.some(a => typeof a === 'object')) {
22
- throw argValueError({
22
+ throw new ArgValueError({
23
23
  ...node,
24
24
  message: 'does not support object arguments',
25
25
  hint: 'Use CAST to convert objects to strings first.',
@@ -49,7 +49,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
49
49
  if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
50
50
  const start = Number(args[1])
51
51
  if (!Number.isInteger(start) || start < 1) {
52
- throw argValueError({
52
+ throw new ArgValueError({
53
53
  ...node,
54
54
  message: `start position must be a positive integer, got ${args[1]}`,
55
55
  hint: 'SQL uses 1-based indexing.',
@@ -61,7 +61,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
61
61
  if (args.length === 3) {
62
62
  const len = Number(args[2])
63
63
  if (!Number.isInteger(len) || len < 0) {
64
- throw argValueError({
64
+ throw new ArgValueError({
65
65
  ...node,
66
66
  message: `length must be a non-negative integer, got ${args[2]}`,
67
67
  hint: 'SQL uses 1-based indexing.',
@@ -90,7 +90,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
90
90
  if (n == null) return null
91
91
  const len = Number(n)
92
92
  if (!Number.isInteger(len) || len < 0) {
93
- throw argValueError({
93
+ throw new ArgValueError({
94
94
  ...node,
95
95
  message: `length must be a non-negative integer, got ${n}`,
96
96
  hint: 'SQL uses 1-based indexing.',
@@ -105,7 +105,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
105
105
  if (n == null) return null
106
106
  const len = Number(n)
107
107
  if (!Number.isInteger(len) || len < 0) {
108
- throw argValueError({
108
+ throw new ArgValueError({
109
109
  ...node,
110
110
  message: `length must be a non-negative integer, got ${n}`,
111
111
  hint: 'SQL uses 1-based indexing.',
@@ -116,10 +116,12 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
116
116
  return str.substring(str.length - len)
117
117
  }
118
118
 
119
- if (funcName === 'INSTR') {
119
+ if (funcName === 'INSTR' || funcName === 'POSITION' || funcName === 'STRPOS') {
120
120
  const search = args[1]
121
121
  if (search == null) return null
122
122
  // INSTR returns 1-based position, 0 if not found
123
123
  return str.indexOf(String(search)) + 1
124
124
  }
125
+
126
+ throw new Error(`Unsupported string function: ${funcName}`)
125
127
  }