squirreling 0.9.5 → 0.10.1

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.
package/README.md CHANGED
@@ -91,6 +91,8 @@ Squirreling can work with any data source that implements the `AsyncDataSource`
91
91
 
92
92
  ```typescript
93
93
  interface AsyncDataSource {
94
+ numRows?: number
95
+ columns: string[]
94
96
  scan(options: ScanOptions): ScanResults
95
97
  }
96
98
 
@@ -113,6 +115,8 @@ The `scan()` method returns a `ScanResults` object containing a row stream and f
113
115
 
114
116
  ```typescript
115
117
  const customSource: AsyncDataSource = {
118
+ numRows: 1000000, // optional total row count for planning
119
+ columns: ['id', 'name', 'active'], // columns available in this source
116
120
  scan({ columns, where, limit, offset, signal }) {
117
121
  // Use hints to optimize your scan, or ignore them
118
122
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.9.5",
3
+ "version": "0.10.1",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -10,7 +10,9 @@
10
10
  "dataset",
11
11
  "hyperparam",
12
12
  "hyparquet",
13
- "parquet"
13
+ "parquet",
14
+ "query",
15
+ "relational"
14
16
  ],
15
17
  "license": "MIT",
16
18
  "repository": {
@@ -37,7 +39,7 @@
37
39
  "test": "vitest run"
38
40
  },
39
41
  "devDependencies": {
40
- "@types/node": "25.3.3",
42
+ "@types/node": "25.4.0",
41
43
  "@vitest/coverage-v8": "4.0.18",
42
44
  "eslint": "9.39.2",
43
45
  "eslint-plugin-jsdoc": "62.7.1",
package/src/ast.d.ts ADDED
@@ -0,0 +1,184 @@
1
+ export type SqlPrimitive =
2
+ | string
3
+ | number
4
+ | bigint
5
+ | boolean
6
+ | Date
7
+ | null
8
+ | SqlPrimitive[]
9
+ | Record<string, any>
10
+
11
+ export interface SelectStatement {
12
+ with?: WithClause
13
+ distinct: boolean
14
+ columns: SelectColumn[]
15
+ from: FromTable | FromSubquery
16
+ joins: JoinClause[]
17
+ where?: ExprNode
18
+ groupBy: ExprNode[]
19
+ having?: ExprNode
20
+ orderBy: OrderByItem[]
21
+ limit?: number
22
+ offset?: number
23
+ }
24
+
25
+ export interface WithClause {
26
+ ctes: CTEDefinition[]
27
+ }
28
+
29
+ export interface CTEDefinition {
30
+ name: string
31
+ query: SelectStatement
32
+ }
33
+
34
+ export interface FromTable extends AstBase {
35
+ kind: 'table'
36
+ table: string
37
+ alias?: string
38
+ }
39
+
40
+ export interface FromSubquery {
41
+ kind: 'subquery'
42
+ query: SelectStatement
43
+ alias: string
44
+ }
45
+
46
+ export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
47
+
48
+ export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
49
+
50
+ export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
51
+
52
+ export interface LiteralNode extends AstBase {
53
+ type: 'literal'
54
+ value: SqlPrimitive
55
+ }
56
+
57
+ export interface IdentifierNode extends AstBase {
58
+ type: 'identifier'
59
+ name: string
60
+ }
61
+
62
+ export interface UnaryNode extends AstBase {
63
+ type: 'unary'
64
+ op: 'NOT' | 'IS NULL' | 'IS NOT NULL' | '-'
65
+ argument: ExprNode
66
+ }
67
+
68
+ export interface BinaryNode extends AstBase {
69
+ type: 'binary'
70
+ op: BinaryOp
71
+ left: ExprNode
72
+ right: ExprNode
73
+ }
74
+
75
+ export interface FunctionNode extends AstBase {
76
+ type: 'function'
77
+ funcName: string
78
+ args: ExprNode[]
79
+ distinct?: boolean
80
+ filter?: ExprNode
81
+ }
82
+
83
+ export type CastType = 'TEXT' | 'STRING' | 'VARCHAR' | 'INTEGER' | 'INT' | 'BIGINT' | 'FLOAT' | 'REAL' | 'DOUBLE' | 'BOOLEAN' | 'BOOL'
84
+
85
+ export interface CastNode extends AstBase {
86
+ type: 'cast'
87
+ expr: ExprNode
88
+ toType: CastType
89
+ }
90
+
91
+ export interface InSubqueryNode extends AstBase {
92
+ type: 'in'
93
+ expr: ExprNode
94
+ subquery: SelectStatement
95
+ }
96
+
97
+ export interface InValuesNode extends AstBase {
98
+ type: 'in valuelist'
99
+ expr: ExprNode
100
+ values: ExprNode[]
101
+ }
102
+
103
+ export interface ExistsNode extends AstBase {
104
+ type: 'exists' | 'not exists'
105
+ subquery: SelectStatement
106
+ }
107
+
108
+ export interface WhenClause extends AstBase {
109
+ condition: ExprNode
110
+ result: ExprNode
111
+ }
112
+
113
+ export interface CaseNode extends AstBase {
114
+ type: 'case'
115
+ caseExpr?: ExprNode
116
+ whenClauses: WhenClause[]
117
+ elseResult?: ExprNode
118
+ }
119
+
120
+ export interface SubqueryNode extends AstBase {
121
+ type: 'subquery'
122
+ subquery: SelectStatement
123
+ }
124
+
125
+ export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
126
+
127
+ export interface IntervalNode extends AstBase {
128
+ type: 'interval'
129
+ value: number
130
+ unit: IntervalUnit
131
+ }
132
+
133
+ export interface StarNode extends AstBase {
134
+ type: 'star'
135
+ }
136
+
137
+ export type ExprNode =
138
+ | LiteralNode
139
+ | IdentifierNode
140
+ | UnaryNode
141
+ | BinaryNode
142
+ | FunctionNode
143
+ | CastNode
144
+ | InSubqueryNode
145
+ | InValuesNode
146
+ | ExistsNode
147
+ | CaseNode
148
+ | SubqueryNode
149
+ | IntervalNode
150
+ | StarNode
151
+
152
+ export interface StarColumn {
153
+ kind: 'star'
154
+ table?: string
155
+ }
156
+
157
+ export interface DerivedColumn {
158
+ kind: 'derived'
159
+ expr: ExprNode
160
+ alias?: string
161
+ }
162
+
163
+ export type SelectColumn = StarColumn | DerivedColumn
164
+
165
+ export interface OrderByItem {
166
+ expr: ExprNode
167
+ direction: 'ASC' | 'DESC'
168
+ nulls?: 'FIRST' | 'LAST'
169
+ }
170
+
171
+ export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS' | 'POSITIONAL'
172
+
173
+ export interface JoinClause extends AstBase {
174
+ joinType: JoinType
175
+ table: string
176
+ alias?: string
177
+ on?: ExprNode
178
+ }
179
+
180
+ // All AST node derive from this base, which includes position info for error reporting and other purposes
181
+ interface AstBase {
182
+ positionStart: number // start position in query (0-based, inclusive)
183
+ positionEnd: number // end position in query (0-based, exclusive)
184
+ }
@@ -6,27 +6,50 @@
6
6
  * Creates an async row accessor that wraps a plain JavaScript object
7
7
  *
8
8
  * @param {Record<string, SqlPrimitive>} obj - the plain object
9
+ * @param {string[]} columns - list of column names (keys in the object)
9
10
  * @returns {AsyncRow} a row accessor interface
10
11
  */
11
- export function asyncRow(obj) {
12
+ export function asyncRow(obj, columns) {
12
13
  /** @type {AsyncCells} */
13
14
  const cells = {}
14
- for (const [key, value] of Object.entries(obj)) {
15
- cells[key] = () => Promise.resolve(value)
15
+ for (const key of columns) {
16
+ cells[key] = () => Promise.resolve(obj[key])
16
17
  }
17
- return { columns: Object.keys(obj), cells }
18
+ return { columns, cells }
18
19
  }
19
20
 
20
21
  /**
21
22
  * Creates an async memory-backed data source from an array of plain objects
22
23
  *
23
- * @param {Record<string, SqlPrimitive>[]} data - array of plain objects
24
+ * @param {Object} options
25
+ * @param {Record<string, SqlPrimitive>[]} options.data - array of plain objects
26
+ * @param {string[]} [options.columns] - optional list of column names (if not provided, inferred from first row)
24
27
  * @returns {AsyncDataSource} an async data source interface
25
28
  */
26
- export function memorySource(data) {
29
+ export function memorySource({ data, columns }) {
30
+ if (!columns) {
31
+ // Columns not provided, infer from data
32
+ if (!data.length) {
33
+ throw new Error('Unknown columns: data is empty and no columns provided')
34
+ }
35
+ columns = Object.keys(data[0])
36
+ // Check first 1000 rows for consistent columns
37
+ for (let i = 1; i < data.length && i < 1000; i++) {
38
+ const rowColumns = Object.keys(data[i])
39
+ const missing = columns.find(col => !rowColumns.includes(col))
40
+ if (missing) {
41
+ throw new Error(`Inconsistent data, column "${missing}" not found in row ${i}`)
42
+ }
43
+ const extra = rowColumns.find(col => !columns.includes(col))
44
+ if (extra) {
45
+ throw new Error(`Inconsistent data, unexpected column "${extra}" found in row ${i}`)
46
+ }
47
+ }
48
+ }
27
49
  return {
28
50
  numRows: data.length,
29
- scan({ where, limit, offset, signal }) {
51
+ columns,
52
+ scan({ columns: scanColumns, where, limit, offset, signal }) {
30
53
  // Only apply offset and limit if no where clause
31
54
  const start = !where ? offset ?? 0 : 0
32
55
  const end = !where && limit !== undefined ? start + limit : data.length
@@ -34,7 +57,7 @@ export function memorySource(data) {
34
57
  rows: (async function* () {
35
58
  for (let i = start; i < end && i < data.length; i++) {
36
59
  if (signal?.aborted) break
37
- yield asyncRow(data[i])
60
+ yield asyncRow(data[i], scanColumns ?? columns)
38
61
  }
39
62
  })(),
40
63
  appliedWhere: false,
@@ -53,6 +76,7 @@ export function cachedDataSource(source) {
53
76
  /** @type {Map<string, Promise<SqlPrimitive>>} */
54
77
  const cache = new Map()
55
78
  return {
79
+ ...source,
56
80
  scan(options) {
57
81
  // Does re-run the scan, but cache avoids re-computing expensive async cells
58
82
  // TODO: check cache first to avoid re-scanning when possible
@@ -91,7 +91,11 @@ export async function* executeHashAggregate(plan, context) {
91
91
 
92
92
  // Apply HAVING filter
93
93
  if (plan.having) {
94
- const havingRow = { ...group[0], ...asyncRow }
94
+ /** @type {AsyncRow} */
95
+ const havingRow = {
96
+ columns: [...group[0].columns, ...asyncRow.columns],
97
+ cells: { ...group[0].cells, ...asyncRow.cells },
98
+ }
95
99
  const passes = await evaluateExpr({
96
100
  node: plan.having,
97
101
  row: havingRow,
@@ -125,7 +129,11 @@ export async function* executeScalarAggregate(plan, context) {
125
129
 
126
130
  // Apply HAVING filter
127
131
  if (plan.having) {
128
- const havingRow = { ...group[0], ...asyncRow }
132
+ /** @type {AsyncRow} */
133
+ const havingRow = {
134
+ columns: [...group[0].columns, ...asyncRow.columns],
135
+ cells: { ...group[0].cells, ...asyncRow.cells },
136
+ }
129
137
  const passes = await evaluateExpr({
130
138
  node: plan.having,
131
139
  row: havingRow,
@@ -1,9 +1,9 @@
1
1
  import { memorySource } from '../backend/dataSource.js'
2
- import { tableNotFoundError } from '../executionErrors.js'
3
2
  import { derivedAlias } from '../expression/alias.js'
4
3
  import { evaluateExpr } from '../expression/evaluate.js'
5
4
  import { parseSql } from '../parse/parse.js'
6
5
  import { planSql } from '../plan/plan.js'
6
+ import { tableNotFoundError } from '../validation/planErrors.js'
7
7
  import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
8
8
  import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
9
9
  import { executeSort } from './sort.js'
@@ -26,11 +26,11 @@ export async function* executeSql({ tables, query, functions, signal }) {
26
26
  // Normalize tables: convert arrays to AsyncDataSource
27
27
  /** @type {Record<string, AsyncDataSource>} */
28
28
  const normalizedTables = {}
29
- for (const [name, source] of Object.entries(tables)) {
30
- if (Array.isArray(source)) {
31
- normalizedTables[name] = memorySource(source)
29
+ for (const [name, data] of Object.entries(tables)) {
30
+ if (Array.isArray(data)) {
31
+ normalizedTables[name] = memorySource({ data })
32
32
  } else {
33
- normalizedTables[name] = source
33
+ normalizedTables[name] = data
34
34
  }
35
35
  }
36
36
 
@@ -46,7 +46,7 @@ export async function* executeSql({ tables, query, functions, signal }) {
46
46
  * @yields {AsyncRow}
47
47
  */
48
48
  export async function* executeSelect({ select, context }) {
49
- const plan = planSql({ query: select, functions: context.functions })
49
+ const plan = planSql({ query: select, functions: context.functions, tables: context.tables })
50
50
  yield* executePlan({ plan, context })
51
51
  }
52
52
 
@@ -95,16 +95,19 @@ export async function* executePlan({ plan, context }) {
95
95
  */
96
96
  async function* executeScan(plan, context) {
97
97
  const { tables, signal } = context
98
- const dataSource = tables[plan.table]
99
- if (dataSource === undefined) {
100
- throw tableNotFoundError({ tableName: plan.table })
98
+ // check table
99
+ const table = tables[plan.table]
100
+ if (!table) {
101
+ throw tableNotFoundError({ table: plan.table, tables })
101
102
  }
102
-
103
- const scanResult = dataSource.scan({ ...plan.hints, signal })
104
- if (!scanResult.rows) {
105
- throw new Error(`Data source "${plan.table}" scan() must return a ScanResults object with { rows, appliedWhere, appliedLimitOffset }`)
103
+ // check columns
104
+ const missingColumn = plan.hints.columns?.find(col => !table.columns.includes(col))
105
+ if (missingColumn) {
106
+ throw new Error(`Column "${missingColumn}" not found. Available columns: ${table.columns.join(', ') || '[]'}`)
106
107
  }
107
- const { rows, appliedWhere, appliedLimitOffset } = scanResult
108
+
109
+ // do the scan
110
+ const { rows, appliedWhere, appliedLimitOffset } = table.scan({ ...plan.hints, signal })
108
111
 
109
112
  // Applied limit/offset without applied where is invalid
110
113
  const hasLimitOffset = plan.hints.limit !== undefined || plan.hints.offset // 0 offset is noop
@@ -135,17 +138,17 @@ async function* executeScan(plan, context) {
135
138
  * @yields {AsyncRow}
136
139
  */
137
140
  async function* executeCount(plan, { tables, signal }) {
138
- const dataSource = tables[plan.table]
139
- if (dataSource === undefined) {
140
- throw tableNotFoundError({ tableName: plan.table })
141
+ const table = tables[plan.table]
142
+ if (!table) {
143
+ throw tableNotFoundError({ table: plan.table, tables })
141
144
  }
142
145
 
143
146
  // Use source numRows if available
144
- let count = dataSource.numRows
145
- if (dataSource.numRows === undefined) {
147
+ let count = table.numRows
148
+ if (table.numRows === undefined) {
146
149
  // Fall back to counting rows via scan
147
150
  count = 0
148
- const { rows } = dataSource.scan({ signal })
151
+ const { rows } = table.scan({ signal })
149
152
  // eslint-disable-next-line no-unused-vars
150
153
  for await (const _ of rows) {
151
154
  if (signal?.aborted) return
@@ -203,16 +203,16 @@ export async function* executeHashJoin(plan, context) {
203
203
  /**
204
204
  * Creates a NULL-filled row with the given column names
205
205
  *
206
- * @param {string[]} columnNames
206
+ * @param {string[]} columns
207
207
  * @returns {AsyncRow}
208
208
  */
209
- function createNullRow(columnNames) {
209
+ function createNullRow(columns) {
210
210
  /** @type {AsyncCells} */
211
211
  const cells = {}
212
- for (const col of columnNames) {
212
+ for (const col of columns) {
213
213
  cells[col] = () => Promise.resolve(null)
214
214
  }
215
- return { columns: columnNames, cells }
215
+ return { columns, cells }
216
216
  }
217
217
 
218
218
  /**
@@ -234,6 +234,7 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
234
234
  // Skip already-prefixed keys (from previous joins)
235
235
  if (!key.includes('.')) {
236
236
  const alias = `${leftTable}.${key}`
237
+ columns.push(alias)
237
238
  cells[alias] = cell
238
239
  }
239
240
  // Also keep unqualified name for convenience
@@ -244,9 +245,9 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
244
245
  // Add right table columns with prefix
245
246
  for (const [key, cell] of Object.entries(rightRow.cells)) {
246
247
  if (!key.includes('.')) {
247
- cells[`${rightTable}.${key}`] = cell
248
- } else {
249
- cells[key] = cell
248
+ const alias = `${rightTable}.${key}`
249
+ columns.push(alias)
250
+ cells[alias] = cell
250
251
  }
251
252
  // Unqualified name (overwrites if same name exists in left table)
252
253
  columns.push(key)
@@ -31,9 +31,9 @@ export function derivedAlias(expr) {
31
31
  if (expr.type === 'function') {
32
32
  // Handle aggregate functions with star (COUNT(*) -> count_all)
33
33
  if (expr.args.length === 1 && expr.args[0].type === 'star') {
34
- return expr.name.toLowerCase() + '_all'
34
+ return expr.funcName.toLowerCase() + '_all'
35
35
  }
36
- return expr.name.toLowerCase() + '_' + expr.args.map(derivedAlias).join('_')
36
+ return expr.funcName.toLowerCase() + '_' + expr.args.map(derivedAlias).join('_')
37
37
  }
38
38
  if (expr.type === 'interval') {
39
39
  return `interval_${expr.value}_${expr.unit.toLowerCase()}`
@@ -1,9 +1,10 @@
1
1
  import { executeSelect } from '../execute/execute.js'
2
2
  import { stringify } from '../execute/utils.js'
3
- import { columnNotFoundError, invalidContextError } from '../executionErrors.js'
4
- import { unknownFunctionError } from '../parseErrors.js'
5
- import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation.js'
6
- import { aggregateError, argValueError, castError } from '../validationErrors.js'
3
+ import { invalidContextError } from '../validation/executionErrors.js'
4
+ import { aggregateError, argValueError, castError } from '../validation/expressionErrors.js'
5
+ import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
6
+ import { unknownFunctionError } from '../validation/parseErrors.js'
7
+ import { columnNotFoundError } from '../validation/planErrors.js'
7
8
  import { derivedAlias } from './alias.js'
8
9
  import { applyBinaryOp } from './binary.js'
9
10
  import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
@@ -47,7 +48,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
47
48
  // Unknown identifier
48
49
  throw columnNotFoundError({
49
50
  columnName: node.name,
50
- availableColumns: Object.keys(row.cells),
51
+ availableColumns: row.columns,
51
52
  positionStart: node.positionStart,
52
53
  positionEnd: node.positionEnd,
53
54
  rowIndex,
@@ -99,7 +100,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
99
100
 
100
101
  // Function calls
101
102
  if (node.type === 'function') {
102
- const funcName = node.name.toUpperCase()
103
+ const funcName = node.funcName.toUpperCase()
103
104
 
104
105
  // Handle aggregate functions
105
106
  if (isAggregateFunc(funcName)) {
@@ -110,11 +111,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
110
111
  if (row.columns.includes(alias)) {
111
112
  return row.cells[alias]()
112
113
  } else {
113
- throw aggregateError({
114
- funcName,
115
- positionStart: node.positionStart,
116
- positionEnd: node.positionEnd,
117
- })
114
+ throw aggregateError(node)
118
115
  }
119
116
  }
120
117
 
@@ -225,26 +222,16 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
225
222
  }
226
223
 
227
224
  /** @type {SqlPrimitive[]} */
228
- const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, rowIndex, rows, context })))
225
+ const args = node.args.length === 1
226
+ ? [await evaluateExpr({ node: node.args[0], row, rowIndex, rows, context })]
227
+ : await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, rowIndex, rows, context })))
229
228
 
230
229
  if (isStringFunc(funcName)) {
231
- return evaluateStringFunc({
232
- funcName,
233
- args,
234
- positionStart: node.positionStart,
235
- positionEnd: node.positionEnd,
236
- rowIndex,
237
- })
230
+ return evaluateStringFunc({ funcName, node, args, rowIndex })
238
231
  }
239
232
 
240
233
  if (isRegexpFunc(funcName)) {
241
- return evaluateRegexpFunc({
242
- funcName,
243
- args,
244
- positionStart: node.positionStart,
245
- positionEnd: node.positionEnd,
246
- rowIndex,
247
- })
234
+ return evaluateRegexpFunc({ funcName, node, args, rowIndex })
248
235
  }
249
236
 
250
237
  if (isMathFunc(funcName)) {
@@ -294,10 +281,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
294
281
  if (funcName === 'JSON_OBJECT') {
295
282
  if (args.length % 2 !== 0) {
296
283
  throw argValueError({
297
- funcName: 'JSON_OBJECT',
284
+ ...node,
298
285
  message: 'requires an even number of arguments (key-value pairs)',
299
- positionStart: node.positionStart,
300
- positionEnd: node.positionEnd,
301
286
  rowIndex,
302
287
  })
303
288
  }
@@ -308,10 +293,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
308
293
  const value = args[i + 1]
309
294
  if (key == null) {
310
295
  throw argValueError({
311
- funcName: 'JSON_OBJECT',
296
+ ...node,
312
297
  message: 'key cannot be null',
313
- positionStart: node.positionStart,
314
- positionEnd: node.positionEnd,
315
298
  hint: 'All keys must be non-null values.',
316
299
  rowIndex,
317
300
  })
@@ -358,10 +341,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
358
341
  jsonArg = JSON.parse(jsonArg)
359
342
  } catch {
360
343
  throw argValueError({
361
- funcName,
344
+ ...node,
362
345
  message: 'invalid JSON string',
363
- positionStart: node.positionStart,
364
- positionEnd: node.positionEnd,
365
346
  hint: 'First argument must be valid JSON.',
366
347
  rowIndex,
367
348
  })
@@ -369,10 +350,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
369
350
  }
370
351
  if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
371
352
  throw argValueError({
372
- funcName,
353
+ ...node,
373
354
  message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
374
- positionStart: node.positionStart,
375
- positionEnd: node.positionEnd,
376
355
  rowIndex,
377
356
  })
378
357
  }
@@ -412,30 +391,20 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
412
391
  }
413
392
  }
414
393
 
415
- throw unknownFunctionError({
416
- funcName,
417
- positionStart: node.positionStart,
418
- positionEnd: node.positionEnd,
419
- })
394
+ throw unknownFunctionError(node)
420
395
  }
421
396
 
422
397
  if (node.type === 'cast') {
423
398
  const val = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
424
399
  if (val == null) return null
425
- const toType = node.toType.toUpperCase()
400
+ const { toType } = node
426
401
  if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
427
402
  if (typeof val === 'object') return stringify(val)
428
403
  return String(val)
429
404
  }
430
405
  // Can only cast primitives to other primitive types
431
406
  if (typeof val === 'object') {
432
- throw castError({
433
- toType: node.toType,
434
- positionStart: node.positionStart,
435
- positionEnd: node.positionEnd,
436
- fromType: 'object',
437
- rowIndex,
438
- })
407
+ throw castError({ ...node, fromType: 'object', rowIndex })
439
408
  }
440
409
  if (toType === 'INTEGER' || toType === 'INT') {
441
410
  const num = Number(val)
@@ -453,12 +422,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
453
422
  if (toType === 'BOOLEAN' || toType === 'BOOL') {
454
423
  return Boolean(val)
455
424
  }
456
- throw castError({
457
- toType: node.toType,
458
- positionStart: node.positionStart,
459
- positionEnd: node.positionEnd,
460
- rowIndex,
461
- })
425
+ throw castError({ ...node, rowIndex })
462
426
  }
463
427
 
464
428
  // IN and NOT IN with value lists