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.
package/src/plan/plan.js CHANGED
@@ -1,34 +1,93 @@
1
+ import { derivedAlias } from '../expression/alias.js'
1
2
  import { parseSql } from '../parse/parse.js'
2
3
  import { findAggregate } from '../validation/aggregates.js'
3
- import { columnNotFoundError, tableNotFoundError } from '../validation/planErrors.js'
4
- import { extractColumns } from './columns.js'
4
+ import { ColumnNotFoundError, TableNotFoundError } from '../validation/planErrors.js'
5
+ import { extractColumns, fromAlias, inferStatementColumns } from './columns.js'
5
6
 
6
7
  /**
7
- * @import { AsyncDataSource, ExprNode, DerivedColumn, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement } from '../types.js'
8
+ * @import { AsyncDataSource, ExprNode, DerivedColumn, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement } from '../types.js'
8
9
  * @import { QueryPlan } from './types.d.ts'
9
10
  */
10
11
 
11
12
  /**
12
- * Builds a query plan from a SELECT statement AST.
13
+ * Builds a query plan from a statement AST.
13
14
  * Resolves CTEs at plan time so no planning occurs during execution.
14
15
  *
15
16
  * @param {PlanSqlOptions} options
16
17
  * @returns {QueryPlan} the root of the query plan tree
17
18
  */
18
19
  export function planSql({ query, functions, tables }) {
19
- const select = typeof query === 'string' ? parseSql({ query, functions }) : query
20
-
21
- // Build CTE plans in order (each CTE can reference preceding CTEs)
22
- /** @type {Map<string, QueryPlan>} */
23
- const ctePlans = new Map()
24
- if (select.with) {
25
- for (const cte of select.with.ctes) {
26
- const ctePlan = planSelect({ select: cte.query, ctePlans, tables })
20
+ /** @type {Statement} */
21
+ const stmt = typeof query === 'string' ? parseSql({ query, functions }) : query
22
+ return planStatement({ stmt, tables })
23
+ }
24
+
25
+ /**
26
+ * Plans a Statement (SelectStatement, SetOperationStatement, or WithStatement).
27
+ *
28
+ * @param {object} options
29
+ * @param {Statement} options.stmt
30
+ * @param {Map<string, QueryPlan>} [options.ctePlans]
31
+ * @param {Map<string, string[]>} [options.cteColumns]
32
+ * @param {Record<string, AsyncDataSource>} [options.tables]
33
+ * @param {string[]} [options.parentColumns] - columns needed by the parent query (for subquery pushdown)
34
+ * @returns {QueryPlan}
35
+ */
36
+ function planStatement({ stmt, ctePlans, cteColumns, tables, parentColumns }) {
37
+ if (stmt.type === 'with') {
38
+ // Build CTE plans in order (each CTE can reference preceding CTEs)
39
+ ctePlans ??= new Map()
40
+ cteColumns ??= new Map()
41
+ for (const cte of stmt.ctes) {
42
+ const ctePlan = planStatement({ stmt: cte.query, ctePlans, cteColumns, tables })
27
43
  ctePlans.set(cte.name.toLowerCase(), ctePlan)
44
+ cteColumns.set(cte.name.toLowerCase(), inferStatementColumns({ stmt: cte.query, cteColumns, tables }))
28
45
  }
46
+ return planStatement({ stmt: stmt.query, ctePlans, cteColumns, tables, parentColumns })
47
+ }
48
+ if (stmt.type === 'compound') {
49
+ return planSetOperation({ compound: stmt, ctePlans, cteColumns, tables })
50
+ }
51
+ return planSelect({ select: stmt, ctePlans, cteColumns, tables, parentColumns })
52
+ }
53
+
54
+ /**
55
+ * Plans a SetOperationStatement (UNION/INTERSECT/EXCEPT).
56
+ *
57
+ * @param {object} options
58
+ * @param {SetOperationStatement} options.compound
59
+ * @param {Map<string, QueryPlan>} [options.ctePlans]
60
+ * @param {Map<string, string[]>} [options.cteColumns]
61
+ * @param {Record<string, AsyncDataSource>} [options.tables]
62
+ * @returns {QueryPlan}
63
+ */
64
+ function planSetOperation({ compound, ctePlans, cteColumns, tables }) {
65
+ const left = planStatement({ stmt: compound.left, ctePlans, cteColumns, tables })
66
+ const right = planStatement({ stmt: compound.right, ctePlans, cteColumns, tables })
67
+ const leftColumns = inferStatementColumns({ stmt: compound.left, cteColumns, tables })
68
+ const rightColumns = inferStatementColumns({ stmt: compound.right, cteColumns, tables })
69
+
70
+ if (leftColumns.length !== rightColumns.length || leftColumns.some((col, idx) => col !== rightColumns[idx])) {
71
+ throw new Error(`Set operation operands must have identical columns, got left [${leftColumns.join(', ')}] and right [${rightColumns.join(', ')}]`)
72
+ }
73
+
74
+ /** @type {QueryPlan} */
75
+ let plan = {
76
+ type: 'SetOperation',
77
+ operator: compound.operator,
78
+ all: compound.all,
79
+ left,
80
+ right,
29
81
  }
30
82
 
31
- return planSelect({ select, ctePlans, tables })
83
+ if (compound.orderBy.length) {
84
+ plan = { type: 'Sort', orderBy: compound.orderBy, child: plan }
85
+ }
86
+ if (compound.limit !== undefined || compound.offset) {
87
+ plan = { type: 'Limit', limit: compound.limit, offset: compound.offset, child: plan }
88
+ }
89
+
90
+ return plan
32
91
  }
33
92
 
34
93
  /**
@@ -36,28 +95,28 @@ export function planSql({ query, functions, tables }) {
36
95
  *
37
96
  * @param {object} options
38
97
  * @param {SelectStatement} options.select
39
- * @param {Map<string, QueryPlan>} options.ctePlans
98
+ * @param {Map<string, QueryPlan>} [options.ctePlans]
99
+ * @param {Map<string, string[]>} [options.cteColumns]
40
100
  * @param {Record<string, AsyncDataSource>} [options.tables]
101
+ * @param {string[]} [options.parentColumns] - columns needed by the parent query (for subquery pushdown)
41
102
  * @returns {QueryPlan}
42
103
  */
43
- function planSelect({ select, ctePlans, tables }) {
104
+ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
44
105
  // Check for aggregation
45
106
  const hasAggregate = select.columns.some(col =>
46
- col.kind === 'derived' && findAggregate(col.expr)
107
+ col.type === 'derived' && findAggregate(col.expr)
47
108
  )
48
109
  const useGrouping = hasAggregate || select.groupBy.length > 0
49
110
  const needsBuffering = useGrouping || select.orderBy.length > 0
50
111
 
51
112
  // Source alias for FROM clause
52
- const sourceAlias = select.from.kind === 'table'
53
- ? select.from.alias ?? select.from.table
54
- : select.from.alias
113
+ const sourceAlias = fromAlias(select.from)
55
114
 
56
115
  // Determine scan hints for direct table scans (WHERE and LIMIT/OFFSET are
57
116
  // included so they are only applied to fresh scans, not CTE/subquery plans)
58
117
  /** @type {ScanOptions} */
59
118
  const hints = {}
60
- const perTableColumns = extractColumns(select)
119
+ const perTableColumns = extractColumns({ select, parentColumns })
61
120
  hints.columns = perTableColumns.get(sourceAlias)
62
121
  if (!select.joins.length) {
63
122
  hints.where = select.where
@@ -69,11 +128,11 @@ function planSelect({ select, ctePlans, tables }) {
69
128
 
70
129
  // Start with the data source (FROM clause)
71
130
  /** @type {QueryPlan} */
72
- let plan = planFrom({ select, ctePlans, hints, tables })
131
+ let plan = planFrom({ select, ctePlans, cteColumns, hints, tables })
73
132
 
74
133
  // Add JOINs
75
134
  if (select.joins.length) {
76
- plan = planJoin({ left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, perTableColumns, tables })
135
+ plan = planJoin({ left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, cteColumns, perTableColumns, tables })
77
136
  }
78
137
 
79
138
  // Whether FROM resolved to our own direct table scan
@@ -118,7 +177,7 @@ function planSelect({ select, ctePlans, tables }) {
118
177
  /** @type {Map<string, ExprNode>} */
119
178
  const aliases = new Map()
120
179
  for (const col of select.columns) {
121
- if (col.kind === 'derived' && col.alias) {
180
+ if (col.type === 'derived' && col.alias) {
122
181
  aliases.set(col.alias, col.expr)
123
182
  }
124
183
  }
@@ -133,9 +192,16 @@ function planSelect({ select, ctePlans, tables }) {
133
192
  // So the order is: Sort -> Project -> Distinct -> Limit
134
193
 
135
194
  // Fast path for SELECT *
136
- const isPassthrough = select.columns.length === 1 && select.columns[0].kind === 'star'
195
+ const isPassthrough = select.columns.length === 1 && select.columns[0].type === 'star'
137
196
  if (!isPassthrough) {
138
- plan = { type: 'Project', columns: select.columns, child: plan }
197
+ // When parent only needs specific columns, drop unneeded projections
198
+ let projectColumns = select.columns
199
+ if (parentColumns) {
200
+ projectColumns = select.columns.filter(col =>
201
+ col.type === 'star' || parentColumns.includes(col.alias ?? derivedAlias(col.expr))
202
+ )
203
+ }
204
+ plan = { type: 'Project', columns: projectColumns, child: plan }
139
205
  }
140
206
 
141
207
  if (select.distinct) {
@@ -153,24 +219,35 @@ function planSelect({ select, ctePlans, tables }) {
153
219
  /**
154
220
  * @param {object} options
155
221
  * @param {SelectStatement} options.select
156
- * @param {Map<string, QueryPlan>} options.ctePlans
222
+ * @param {Map<string, QueryPlan>} [options.ctePlans]
223
+ * @param {Map<string, string[]>} [options.cteColumns]
157
224
  * @param {ScanOptions} options.hints
158
225
  * @param {Record<string, AsyncDataSource>} [options.tables]
159
226
  * @returns {QueryPlan}
160
227
  */
161
- function planFrom({ select, ctePlans, hints, tables }) {
162
- if (select.from.kind === 'table') {
163
- const ctePlan = ctePlans.get(select.from.table.toLowerCase())
228
+ function planFrom({ select, ctePlans, cteColumns, hints, tables }) {
229
+ if (select.from.type === 'table') {
230
+ const ctePlan = ctePlans?.get(select.from.table.toLowerCase())
164
231
  if (ctePlan) {
165
232
  return ctePlan
166
233
  }
167
234
  validateScan({ ...select.from, hints, tables })
168
235
  return { type: 'Scan', table: select.from.table, hints }
169
236
  } else {
170
- if (select.from.query.with) {
171
- throw new Error('WITH clause is not supported inside subqueries')
237
+ const subPlan = planStatement({ stmt: select.from.query, ctePlans, cteColumns, tables, parentColumns: hints.columns })
238
+ // Validate that requested columns exist in subquery output
239
+ const availableColumns = inferStatementColumns({ stmt: select.from.query, cteColumns, tables })
240
+ if (hints.columns && availableColumns.length) {
241
+ const missingColumn = hints.columns.find(col => !availableColumns.includes(col))
242
+ if (missingColumn) {
243
+ throw new ColumnNotFoundError({
244
+ columnName: missingColumn,
245
+ availableColumns,
246
+ ...select.from,
247
+ })
248
+ }
172
249
  }
173
- return planSelect({ select: select.from.query, ctePlans, tables })
250
+ return subPlan
174
251
  }
175
252
  }
176
253
 
@@ -179,24 +256,28 @@ function planFrom({ select, ctePlans, hints, tables }) {
179
256
  * @param {QueryPlan} options.left - the left side of the join (FROM or previous joins)
180
257
  * @param {JoinClause[]} options.joins - array of join clauses
181
258
  * @param {string} options.leftTable - name/alias of the left table
182
- * @param {Map<string, QueryPlan>} options.ctePlans
259
+ * @param {Map<string, QueryPlan>} [options.ctePlans]
260
+ * @param {Map<string, string[]>} [options.cteColumns]
183
261
  * @param {Map<string, string[] | undefined>} options.perTableColumns
184
262
  * @param {Record<string, AsyncDataSource>} [options.tables]
185
263
  * @returns {QueryPlan}
186
264
  */
187
- function planJoin({ left, joins, leftTable, ctePlans, perTableColumns, tables }) {
265
+ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumns, tables }) {
188
266
  let plan = left
189
267
  let currentLeftTable = leftTable
190
268
 
191
269
  for (const join of joins) {
192
270
  const rightTable = join.alias ?? join.table
193
271
 
194
- const ctePlan = ctePlans.get(join.table.toLowerCase())
272
+ const ctePlan = ctePlans?.get(join.table.toLowerCase())
195
273
  /** @type {ScanOptions} */
196
274
  const rightHints = {}
197
275
  if (!ctePlan) {
198
276
  rightHints.columns = perTableColumns.get(rightTable)
199
277
  validateScan({ ...join, hints: rightHints, tables })
278
+ } else {
279
+ // For CTE joins, use CTE column metadata for hints
280
+ rightHints.columns = perTableColumns.get(rightTable) ?? cteColumns?.get(join.table.toLowerCase())
200
281
  }
201
282
  /** @type {QueryPlan} */
202
283
  const rightScan = ctePlan ?? { type: 'Scan', table: join.table, hints: rightHints }
@@ -300,13 +381,9 @@ function resolveAliases(node, aliases) {
300
381
  * @returns {{ leftKey: ExprNode, rightKey: ExprNode } | undefined}
301
382
  */
302
383
  function extractSimpleJoinKeys({ condition, leftTable, rightTable }) {
303
- if (condition.type !== 'binary' || condition.op !== '=') {
304
- return undefined
305
- }
384
+ if (condition.type !== 'binary' || condition.op !== '=') return
306
385
  const { left, right } = condition
307
- if (left.type !== 'identifier' || right.type !== 'identifier') {
308
- return undefined
309
- }
386
+ if (left.type !== 'identifier' || right.type !== 'identifier') return
310
387
 
311
388
  // Check if keys are in swapped order (right table ref on left side)
312
389
  const leftRefsRight = left.name.startsWith(`${rightTable}.`)
@@ -333,11 +410,11 @@ function validateScan({ table, hints, tables, positionStart, positionEnd }) {
333
410
  if (!tables) return
334
411
  const resolved = tables[table]
335
412
  if (!resolved) {
336
- throw tableNotFoundError({ table, tables, positionStart, positionEnd })
413
+ throw new TableNotFoundError({ table, tables, positionStart, positionEnd })
337
414
  }
338
415
  const missingColumn = hints.columns?.find(col => !resolved.columns.includes(col))
339
416
  if (missingColumn) {
340
- throw columnNotFoundError({
417
+ throw new ColumnNotFoundError({
341
418
  columnName: missingColumn,
342
419
  availableColumns: resolved.columns,
343
420
  positionStart,
@@ -355,7 +432,7 @@ function validateScan({ table, hints, tables, positionStart, positionEnd }) {
355
432
  function isAllCountStar(columns) {
356
433
  if (columns.length === 0) return false
357
434
  return columns.every(col =>
358
- col.kind === 'derived' &&
435
+ col.type === 'derived' &&
359
436
  col.expr.type === 'function' &&
360
437
  col.expr.funcName.toUpperCase() === 'COUNT' &&
361
438
  col.expr.args.length === 1 &&
@@ -1,4 +1,4 @@
1
- import { DerivedColumn, ExprNode, JoinType, OrderByItem, ScanOptions, SelectColumn } from '../types.js'
1
+ import { DerivedColumn, ExprNode, JoinType, OrderByItem, ScanOptions, SelectColumn, SetOperator } from '../types.js'
2
2
 
3
3
  export type QueryPlan =
4
4
  | ScanNode
@@ -13,6 +13,7 @@ export type QueryPlan =
13
13
  | HashJoinNode
14
14
  | NestedLoopJoinNode
15
15
  | PositionalJoinNode
16
+ | SetOperationNode
16
17
 
17
18
  // Scan node
18
19
  export interface ScanNode {
@@ -104,3 +105,12 @@ export interface PositionalJoinNode {
104
105
  left: QueryPlan
105
106
  right: QueryPlan
106
107
  }
108
+
109
+ // Set operation node (UNION, INTERSECT, EXCEPT)
110
+ export interface SetOperationNode {
111
+ type: 'SetOperation'
112
+ operator: SetOperator
113
+ all: boolean
114
+ left: QueryPlan
115
+ right: QueryPlan
116
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @import { BBox, SimpleGeometry } from './geometry.js'
2
+ * @import { BoundingBox, SimpleGeometry } from './geometry.js'
3
3
  */
4
4
 
5
5
  export const EPSILON = 1e-10
6
6
  export const EPSILON_SQ = EPSILON * EPSILON
7
7
 
8
- /** @type {WeakMap<SimpleGeometry, BBox>} */
8
+ /** @type {WeakMap<SimpleGeometry, BoundingBox>} */
9
9
  const bboxCache = new WeakMap()
10
10
 
11
11
  /**
@@ -26,7 +26,7 @@ export function bboxOverlap(a, b) {
26
26
  * Results are cached per geometry object.
27
27
  *
28
28
  * @param {SimpleGeometry} geom
29
- * @returns {BBox}
29
+ * @returns {BoundingBox}
30
30
  */
31
31
  export function bbox(geom) {
32
32
  let b = bboxCache.get(geom)
@@ -15,7 +15,7 @@ export type Geometry =
15
15
  */
16
16
  export type SimpleGeometry = Point | LineString | Polygon
17
17
 
18
- export interface BBox {
18
+ export interface BoundingBox {
19
19
  minX: number
20
20
  minY: number
21
21
  maxX: number
@@ -0,0 +1,6 @@
1
+ import type { BoundingBox, Geometry, SimpleGeometry } from './geometry.js'
2
+
3
+ export function decompose(geom: Geometry): SimpleGeometry[]
4
+ export function bbox(geom: SimpleGeometry): BoundingBox
5
+ export function bboxOverlap(a: SimpleGeometry, b: SimpleGeometry): boolean
6
+ export function parseWkt(wkt: string): Geometry | null
@@ -0,0 +1,3 @@
1
+ export { decompose } from './spatial.js'
2
+ export { bbox, bboxOverlap } from './bbox.js'
3
+ export { parseWkt } from './wkt.js'
@@ -68,6 +68,25 @@ export function evaluateSpatialFunc({ funcName, args }) {
68
68
  }
69
69
  }
70
70
 
71
+ /**
72
+ * Decompose Multi* and GeometryCollection into simple geometries.
73
+ *
74
+ * @param {Geometry} geom
75
+ * @returns {SimpleGeometry[]}
76
+ */
77
+ export function decompose(geom) {
78
+ if (geom.type === 'MultiPoint') {
79
+ return geom.coordinates.map(c => ({ type: 'Point', coordinates: c }))
80
+ } else if (geom.type === 'MultiLineString') {
81
+ return geom.coordinates.map(c => ({ type: 'LineString', coordinates: c }))
82
+ } else if (geom.type === 'MultiPolygon') {
83
+ return geom.coordinates.map(c => ({ type: 'Polygon', coordinates: c }))
84
+ } else if (geom.type === 'GeometryCollection') {
85
+ return geom.geometries.flatMap(decompose)
86
+ }
87
+ return [geom]
88
+ }
89
+
71
90
  /**
72
91
  * Normalize a geometry value. Accepts GeoJSON objects.
73
92
  * Returns null if the value is not a valid geometry.
@@ -90,10 +109,6 @@ function toGeometry(val) {
90
109
  return null
91
110
  }
92
111
 
93
- // ============================================================================
94
- // Minimum distance between geometries
95
- // ============================================================================
96
-
97
112
  /**
98
113
  * Get all line segments from a geometry.
99
114
  *
@@ -165,35 +180,6 @@ function stDWithin(a, b, distance) {
165
180
  return false
166
181
  }
167
182
 
168
- // ============================================================================
169
- // Spatial predicate dispatch - decompose to primitive type pairs
170
- // ============================================================================
171
-
172
- /**
173
- * Decompose Multi* and GeometryCollection into simple geometries.
174
- *
175
- * @param {Geometry} geom
176
- * @returns {SimpleGeometry[]}
177
- */
178
- function decompose(geom) {
179
- switch (geom.type) {
180
- case 'MultiPoint':
181
- return geom.coordinates.map(c => ({ type: 'Point', coordinates: c }))
182
- case 'MultiLineString':
183
- return geom.coordinates.map(c => ({ type: 'LineString', coordinates: c }))
184
- case 'MultiPolygon':
185
- return geom.coordinates.map(c => ({ type: 'Polygon', coordinates: c }))
186
- case 'GeometryCollection':
187
- return geom.geometries.flatMap(decompose)
188
- default:
189
- return [geom]
190
- }
191
- }
192
-
193
- // ============================================================================
194
- // ST_Contains
195
- // ============================================================================
196
-
197
183
  /**
198
184
  * @param {SimpleGeometry[]} a
199
185
  * @param {SimpleGeometry[]} b
@@ -204,10 +190,6 @@ function stContains(a, b) {
204
190
  return b.every(pb => a.some(pa => pairContainment(pa, pb) !== 'OUTSIDE'))
205
191
  }
206
192
 
207
- // ============================================================================
208
- // ST_ContainsProperly
209
- // ============================================================================
210
-
211
193
  /**
212
194
  * @param {SimpleGeometry[]} a
213
195
  * @param {SimpleGeometry[]} b
@@ -218,10 +200,6 @@ function stContainsProperly(a, b) {
218
200
  return b.every(pb => a.some(pa => pairContainment(pa, pb) === 'INSIDE'))
219
201
  }
220
202
 
221
- // ============================================================================
222
- // ST_Touches
223
- // ============================================================================
224
-
225
203
  /**
226
204
  * @param {SimpleGeometry[]} a
227
205
  * @param {SimpleGeometry[]} b
@@ -239,10 +217,6 @@ function stTouches(a, b) {
239
217
  return intersects
240
218
  }
241
219
 
242
- // ============================================================================
243
- // ST_Overlaps
244
- // ============================================================================
245
-
246
220
  /**
247
221
  * @param {SimpleGeometry[]} a
248
222
  * @param {SimpleGeometry[]} b
@@ -281,10 +255,6 @@ function geometryDimension(parts) {
281
255
  return max
282
256
  }
283
257
 
284
- // ============================================================================
285
- // ST_Equals
286
- // ============================================================================
287
-
288
258
  /**
289
259
  * @param {SimpleGeometry[]} a
290
260
  * @param {SimpleGeometry[]} b
@@ -310,10 +280,6 @@ function stEquals(a, b) {
310
280
  return true
311
281
  }
312
282
 
313
- // ============================================================================
314
- // ST_Crosses
315
- // ============================================================================
316
-
317
283
  /**
318
284
  * @param {SimpleGeometry[]} a
319
285
  * @param {SimpleGeometry[]} b
package/src/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExprNode, SelectStatement, SqlPrimitive } from './ast.js'
1
+ import type { ExprNode, SelectStatement, SqlPrimitive, Statement } from './ast.js'
2
2
 
3
3
  export * from './ast.js'
4
4
  export { ParserState, Token, TokenType } from './parse/types.js'
@@ -13,14 +13,14 @@ export interface ParseSqlOptions {
13
13
  // executeSql(options)
14
14
  export interface ExecuteSqlOptions {
15
15
  tables: Record<string, Row | AsyncDataSource>
16
- query: string | SelectStatement
16
+ query: string | Statement
17
17
  functions?: Record<string, UserDefinedFunction>
18
18
  signal?: AbortSignal
19
19
  }
20
20
 
21
21
  // planSql(options)
22
22
  export interface PlanSqlOptions {
23
- query: string | SelectStatement
23
+ query: string | Statement
24
24
  functions?: Record<string, UserDefinedFunction>
25
25
  tables?: Record<string, AsyncDataSource>
26
26
  }
@@ -49,6 +49,8 @@ export interface AsyncDataSource {
49
49
  numRows?: number
50
50
  columns: string[]
51
51
  scan(options: ScanOptions): ScanResults
52
+ // Optional method for fast column scans
53
+ scanColumn?(options: ScanColumnOptions): AsyncIterable<ArrayLike<SqlPrimitive>>
52
54
  }
53
55
 
54
56
  /**
@@ -77,6 +79,14 @@ export interface ScanOptions {
77
79
  signal?: AbortSignal
78
80
  }
79
81
 
82
+ /**
83
+ * Options for scanning a single column.
84
+ */
85
+ export interface ScanColumnOptions {
86
+ column: string
87
+ signal?: AbortSignal
88
+ }
89
+
80
90
  export interface FunctionSignature {
81
91
  min: number
82
92
  max?: number
@@ -88,9 +98,9 @@ export interface UserDefinedFunction {
88
98
  arguments: FunctionSignature
89
99
  }
90
100
 
91
- export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP'
101
+ export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE'
92
102
 
93
- export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_REPLACE'
103
+ export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE'
94
104
 
95
105
  export type MathFunc =
96
106
  | 'FLOOR'
@@ -131,6 +141,8 @@ export type StringFunc =
131
141
  | 'LEFT'
132
142
  | 'RIGHT'
133
143
  | 'INSTR'
144
+ | 'POSITION'
145
+ | 'STRPOS'
134
146
 
135
147
  export type SpatialFunc =
136
148
  | 'ST_INTERSECTS'
@@ -1,3 +1,5 @@
1
+ import { FUNCTION_SIGNATURES } from './functions.js'
2
+
1
3
  /**
2
4
  * Structured execution error with position range and optional row number.
3
5
  */
@@ -12,7 +14,7 @@ export class ExecutionError extends Error {
12
14
  constructor({ message, positionStart, positionEnd, rowIndex }) {
13
15
  const rowSuffix = rowIndex != null ? ` (row ${rowIndex})` : ''
14
16
  super(message + rowSuffix)
15
- this.name = 'ExecutionError'
17
+ this.name = this.constructor.name
16
18
  this.positionStart = positionStart
17
19
  this.positionEnd = positionEnd
18
20
  this.rowIndex = rowIndex
@@ -20,16 +22,22 @@ export class ExecutionError extends Error {
20
22
  }
21
23
 
22
24
  /**
23
- * Error for invalid context (e.g., INTERVAL without date arithmetic).
24
- *
25
- * @param {Object} options
26
- * @param {string} options.item - What was used incorrectly
27
- * @param {string} options.validContext - Where it can be used
28
- * @param {number} options.positionStart
29
- * @param {number} options.positionEnd
30
- * @param {number} options.rowIndex - 1-based row number where error occurred
31
- * @returns {ExecutionError}
25
+ * Error for invalid argument type or value.
32
26
  */
33
- export function invalidContextError({ item, validContext, positionStart, positionEnd, rowIndex }) {
34
- return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowIndex })
27
+ export class ArgValueError extends ExecutionError {
28
+ /**
29
+ * @param {Object} options
30
+ * @param {string} options.funcName - The function name
31
+ * @param {string} options.message - Specific error message
32
+ * @param {number} options.positionStart
33
+ * @param {number} options.positionEnd
34
+ * @param {string} [options.hint] - Recovery hint
35
+ * @param {number} [options.rowIndex] - 1-based row number where error occurred
36
+ */
37
+ constructor({ funcName, message, positionStart, positionEnd, hint, rowIndex }) {
38
+ const funcNameUpper = funcName.toUpperCase()
39
+ const signature = FUNCTION_SIGNATURES[funcNameUpper]?.signature ?? ''
40
+ const suffix = hint ? `. ${hint}` : ''
41
+ super({ message: `${funcName}(${signature}): ${message}${suffix}`, positionStart, positionEnd, rowIndex })
42
+ }
35
43
  }