squirreling 0.10.0 → 0.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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,11 +39,11 @@
37
39
  "test": "vitest run"
38
40
  },
39
41
  "devDependencies": {
40
- "@types/node": "25.3.5",
41
- "@vitest/coverage-v8": "4.0.18",
42
+ "@types/node": "25.5.0",
43
+ "@vitest/coverage-v8": "4.1.0",
42
44
  "eslint": "9.39.2",
43
- "eslint-plugin-jsdoc": "62.7.1",
45
+ "eslint-plugin-jsdoc": "62.8.0",
44
46
  "typescript": "5.9.3",
45
- "vitest": "4.0.18"
47
+ "vitest": "4.1.0"
46
48
  }
47
49
  }
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
+ }
@@ -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'
@@ -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
 
@@ -98,7 +98,7 @@ async function* executeScan(plan, context) {
98
98
  // check table
99
99
  const table = tables[plan.table]
100
100
  if (!table) {
101
- throw tableNotFoundError({ tableName: plan.table })
101
+ throw tableNotFoundError({ table: plan.table, tables })
102
102
  }
103
103
  // check columns
104
104
  const missingColumn = plan.hints.columns?.find(col => !table.columns.includes(col))
@@ -119,7 +119,7 @@ async function* executeScan(plan, context) {
119
119
 
120
120
  // Apply WHERE if data source did not
121
121
  if (!appliedWhere && plan.hints.where) {
122
- result = filterRows(result, plan.hints.where, context)
122
+ result = filterRows(result, plan.hints.where, context, plan.hints.limit)
123
123
  }
124
124
 
125
125
  // Apply LIMIT/OFFSET if data source did not
@@ -140,7 +140,7 @@ async function* executeScan(plan, context) {
140
140
  async function* executeCount(plan, { tables, signal }) {
141
141
  const table = tables[plan.table]
142
142
  if (!table) {
143
- throw tableNotFoundError({ tableName: plan.table })
143
+ throw tableNotFoundError({ table: plan.table, tables })
144
144
  }
145
145
 
146
146
  // Use source numRows if available
@@ -174,15 +174,42 @@ async function* executeCount(plan, { tables, signal }) {
174
174
  * @param {AsyncIterable<AsyncRow>} rows
175
175
  * @param {ExprNode} condition
176
176
  * @param {ExecuteContext} context
177
+ * @param {number} [limit] - downstream LIMIT hint for chunk sizing
177
178
  * @yields {AsyncRow}
178
179
  */
179
- async function* filterRows(rows, condition, context) {
180
+ async function* filterRows(rows, condition, context, limit) {
181
+ const MAX_CHUNK = 256
182
+ let chunkSize = limit ?? Infinity
180
183
  let rowIndex = 0
184
+
185
+ /** @type {{ row: AsyncRow, rowIndex: number }[]} */
186
+ let buffer = []
187
+
181
188
  for await (const row of rows) {
182
189
  if (context.signal?.aborted) return
183
190
  rowIndex++
184
- const pass = await evaluateExpr({ node: condition, row, rowIndex, context })
185
- if (pass) yield row
191
+ buffer.push({ row, rowIndex })
192
+
193
+ if (buffer.length >= chunkSize) {
194
+ const results = await Promise.all(buffer.map(b =>
195
+ evaluateExpr({ node: condition, row: b.row, rowIndex: b.rowIndex, context })
196
+ ))
197
+ for (let i = 0; i < buffer.length; i++) {
198
+ if (results[i]) yield buffer[i].row
199
+ }
200
+ buffer = []
201
+ chunkSize = Math.min(chunkSize * 2, MAX_CHUNK)
202
+ }
203
+ }
204
+
205
+ // Flush remaining rows
206
+ if (buffer.length > 0) {
207
+ const results = await Promise.all(buffer.map(b =>
208
+ evaluateExpr({ node: condition, row: b.row, rowIndex: b.rowIndex, context })
209
+ ))
210
+ for (let i = 0; i < buffer.length; i++) {
211
+ if (results[i]) yield buffer[i].row
212
+ }
186
213
  }
187
214
  }
188
215
 
@@ -275,17 +302,38 @@ async function* executeProject(plan, context) {
275
302
  */
276
303
  async function* executeDistinct(plan, context) {
277
304
  const { signal } = context
305
+ const MAX_CHUNK = 256
278
306
 
279
307
  /** @type {Set<string>} */
280
308
  const seen = new Set()
281
309
 
310
+ /** @type {AsyncRow[]} */
311
+ let buffer = []
312
+
282
313
  for await (const row of executePlan({ plan: plan.child, context })) {
283
314
  if (signal?.aborted) return
315
+ buffer.push(row)
284
316
 
285
- const key = await stableRowKey(row.cells)
286
- if (!seen.has(key)) {
287
- seen.add(key)
288
- yield row
317
+ if (buffer.length >= MAX_CHUNK) {
318
+ const keys = await Promise.all(buffer.map(r => stableRowKey(r.cells)))
319
+ for (let i = 0; i < buffer.length; i++) {
320
+ if (!seen.has(keys[i])) {
321
+ seen.add(keys[i])
322
+ yield buffer[i]
323
+ }
324
+ }
325
+ buffer = []
326
+ }
327
+ }
328
+
329
+ // Flush remaining
330
+ if (buffer.length > 0) {
331
+ const keys = await Promise.all(buffer.map(r => stableRowKey(r.cells)))
332
+ for (let i = 0; i < buffer.length; i++) {
333
+ if (!seen.has(keys[i])) {
334
+ seen.add(keys[i])
335
+ yield buffer[i]
336
+ }
289
337
  }
290
338
  }
291
339
  }
@@ -44,17 +44,22 @@ export function compareForTerm(a, b, term) {
44
44
  * @returns {Promise<Record<string, SqlPrimitive>[]>} array of all yielded values
45
45
  */
46
46
  export async function collect(asyncRows) {
47
- /** @type {Record<string, SqlPrimitive>[]} */
48
- const results = []
47
+ // Collect all rows first, then materialize cells concurrently
48
+ // This enables dataloader-style batching of cell accessors
49
+ /** @type {AsyncRow[]} */
50
+ const rows = []
49
51
  for await (const asyncRow of asyncRows) {
52
+ rows.push(asyncRow)
53
+ }
54
+ return Promise.all(rows.map(async asyncRow => {
55
+ const values = await Promise.all(asyncRow.columns.map(k => asyncRow.cells[k]()))
50
56
  /** @type {Record<string, SqlPrimitive>} */
51
57
  const item = {}
52
- for (const key of asyncRow.columns) {
53
- item[key] = await asyncRow.cells[key]()
58
+ for (let i = 0; i < asyncRow.columns.length; i++) {
59
+ item[asyncRow.columns[i]] = values[i]
54
60
  }
55
- results.push(item)
56
- }
57
- return results
61
+ return item
62
+ }))
58
63
  }
59
64
 
60
65
  /**
@@ -79,11 +84,6 @@ export function stringify(value) {
79
84
  */
80
85
  export async function stableRowKey(cells) {
81
86
  const keys = Object.keys(cells).sort()
82
- /** @type {string[]} */
83
- const parts = []
84
- for (const k of keys) {
85
- const v = await cells[k]()
86
- parts.push(k + ':' + stringify(v))
87
- }
88
- return parts.join('|')
87
+ const values = await Promise.all(keys.map(k => cells[k]()))
88
+ return keys.map((k, i) => k + ':' + stringify(values[i])).join('|')
89
89
  }
@@ -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'
@@ -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,22 +111,17 @@ 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
 
121
118
  // Apply FILTER clause if present
122
119
  let filteredRows = rows
123
120
  if (node.filter) {
124
- filteredRows = []
125
- for (const row of rows) {
126
- const passes = await evaluateExpr({ node: node.filter, row, context })
127
- if (passes) filteredRows.push(row)
128
- }
121
+ const passes = await Promise.all(rows.map(row =>
122
+ evaluateExpr({ node: node.filter, row, context })
123
+ ))
124
+ filteredRows = rows.filter((_, i) => passes[i])
129
125
  }
130
126
 
131
127
  const argNode = node.args[0]
@@ -135,23 +131,27 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
135
131
  return filteredRows.length
136
132
  }
137
133
 
134
+ const values = await Promise.all(filteredRows.map(row =>
135
+ evaluateExpr({ node: argNode, row, context })
136
+ ))
138
137
  if (node.distinct) {
139
138
  const seen = new Set()
140
- for (const row of filteredRows) {
141
- const v = await evaluateExpr({ node: argNode, row, context })
139
+ for (const v of values) {
142
140
  if (v != null) seen.add(v)
143
141
  }
144
142
  return seen.size
145
143
  }
146
144
  let count = 0
147
- for (const row of filteredRows) {
148
- const v = await evaluateExpr({ node: argNode, row, context })
145
+ for (const v of values) {
149
146
  if (v != null) count++
150
147
  }
151
148
  return count
152
149
  }
153
150
 
154
151
  if (funcName === 'SUM' || funcName === 'AVG' || funcName === 'MIN' || funcName === 'MAX') {
152
+ const rawValues = await Promise.all(filteredRows.map(row =>
153
+ evaluateExpr({ node: argNode, row, context })
154
+ ))
155
155
  let sum = 0
156
156
  let count = 0
157
157
  /** @type {number | null} */
@@ -159,8 +159,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
159
159
  /** @type {number | null} */
160
160
  let max = null
161
161
 
162
- for (const row of filteredRows) {
163
- const raw = await evaluateExpr({ node: argNode, row, context })
162
+ for (const raw of rawValues) {
164
163
  if (raw == null) continue
165
164
  const num = Number(raw)
166
165
  if (!Number.isFinite(num)) continue
@@ -183,9 +182,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
183
182
  }
184
183
 
185
184
  if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
185
+ const rawValues = await Promise.all(filteredRows.map(row =>
186
+ evaluateExpr({ node: argNode, row, context })
187
+ ))
188
+ /** @type {number[]} */
186
189
  const values = []
187
- for (const row of filteredRows) {
188
- const raw = await evaluateExpr({ node: argNode, row, context })
190
+ for (const raw of rawValues) {
189
191
  if (raw == null) continue
190
192
  const num = Number(raw)
191
193
  if (!Number.isFinite(num)) continue
@@ -230,23 +232,11 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
230
232
  : await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, rowIndex, rows, context })))
231
233
 
232
234
  if (isStringFunc(funcName)) {
233
- return evaluateStringFunc({
234
- funcName,
235
- args,
236
- positionStart: node.positionStart,
237
- positionEnd: node.positionEnd,
238
- rowIndex,
239
- })
235
+ return evaluateStringFunc({ funcName, node, args, rowIndex })
240
236
  }
241
237
 
242
238
  if (isRegexpFunc(funcName)) {
243
- return evaluateRegexpFunc({
244
- funcName,
245
- args,
246
- positionStart: node.positionStart,
247
- positionEnd: node.positionEnd,
248
- rowIndex,
249
- })
239
+ return evaluateRegexpFunc({ funcName, node, args, rowIndex })
250
240
  }
251
241
 
252
242
  if (isMathFunc(funcName)) {
@@ -296,10 +286,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
296
286
  if (funcName === 'JSON_OBJECT') {
297
287
  if (args.length % 2 !== 0) {
298
288
  throw argValueError({
299
- funcName: 'JSON_OBJECT',
289
+ ...node,
300
290
  message: 'requires an even number of arguments (key-value pairs)',
301
- positionStart: node.positionStart,
302
- positionEnd: node.positionEnd,
303
291
  rowIndex,
304
292
  })
305
293
  }
@@ -310,10 +298,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
310
298
  const value = args[i + 1]
311
299
  if (key == null) {
312
300
  throw argValueError({
313
- funcName: 'JSON_OBJECT',
301
+ ...node,
314
302
  message: 'key cannot be null',
315
- positionStart: node.positionStart,
316
- positionEnd: node.positionEnd,
317
303
  hint: 'All keys must be non-null values.',
318
304
  rowIndex,
319
305
  })
@@ -360,10 +346,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
360
346
  jsonArg = JSON.parse(jsonArg)
361
347
  } catch {
362
348
  throw argValueError({
363
- funcName,
349
+ ...node,
364
350
  message: 'invalid JSON string',
365
- positionStart: node.positionStart,
366
- positionEnd: node.positionEnd,
367
351
  hint: 'First argument must be valid JSON.',
368
352
  rowIndex,
369
353
  })
@@ -371,10 +355,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
371
355
  }
372
356
  if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
373
357
  throw argValueError({
374
- funcName,
358
+ ...node,
375
359
  message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
376
- positionStart: node.positionStart,
377
- positionEnd: node.positionEnd,
378
360
  rowIndex,
379
361
  })
380
362
  }
@@ -414,30 +396,20 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
414
396
  }
415
397
  }
416
398
 
417
- throw unknownFunctionError({
418
- funcName,
419
- positionStart: node.positionStart,
420
- positionEnd: node.positionEnd,
421
- })
399
+ throw unknownFunctionError(node)
422
400
  }
423
401
 
424
402
  if (node.type === 'cast') {
425
403
  const val = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
426
404
  if (val == null) return null
427
- const toType = node.toType.toUpperCase()
405
+ const { toType } = node
428
406
  if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
429
407
  if (typeof val === 'object') return stringify(val)
430
408
  return String(val)
431
409
  }
432
410
  // Can only cast primitives to other primitive types
433
411
  if (typeof val === 'object') {
434
- throw castError({
435
- toType: node.toType,
436
- positionStart: node.positionStart,
437
- positionEnd: node.positionEnd,
438
- fromType: 'object',
439
- rowIndex,
440
- })
412
+ throw castError({ ...node, fromType: 'object', rowIndex })
441
413
  }
442
414
  if (toType === 'INTEGER' || toType === 'INT') {
443
415
  const num = Number(val)
@@ -455,12 +427,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
455
427
  if (toType === 'BOOLEAN' || toType === 'BOOL') {
456
428
  return Boolean(val)
457
429
  }
458
- throw castError({
459
- toType: node.toType,
460
- positionStart: node.positionStart,
461
- positionEnd: node.positionEnd,
462
- rowIndex,
463
- })
430
+ throw castError({ ...node, rowIndex })
464
431
  }
465
432
 
466
433
  // IN and NOT IN with value lists