squirreling 0.15.2 → 0.16.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,10 +1,19 @@
1
- import { tableFunctionDefaultColumns } from '../parse/parse.js'
1
+ import { dataSourceColumns } from '../backend/dataSource.js'
2
2
  import { derivedAlias } from '../expression/alias.js'
3
+ import { tableFunctionDefaultColumns } from '../parse/parse.js'
3
4
 
4
5
  /**
5
6
  * @import { AsyncDataSource, ExprNode, FromFunction, FromSubquery, FromTable, IdentifierNode, SelectStatement, Statement } from '../types.js'
6
7
  */
7
8
 
9
+ /**
10
+ * @typedef {{
11
+ * cteColumns?: Map<string, string[]>,
12
+ * tables?: Record<string, AsyncDataSource>,
13
+ * outerAliases?: Set<string>,
14
+ * }} ColumnCollectionContext
15
+ */
16
+
8
17
  /**
9
18
  * @param {FromTable | FromSubquery | FromFunction | undefined} from
10
19
  * @returns {string | undefined}
@@ -60,9 +69,13 @@ export function tableFunctionColumnNames(from) {
60
69
  * @param {object} options
61
70
  * @param {SelectStatement} options.select
62
71
  * @param {IdentifierNode[]} [options.parentColumns] - columns needed by the parent query
72
+ * @param {Set<string>} [options.scopeColumns] - bare column names available in the current scope
73
+ * @param {Map<string, string[]>} [options.cteColumns]
74
+ * @param {Record<string, AsyncDataSource>} [options.tables]
75
+ * @param {string[]} [options.outerScope]
63
76
  * @returns {Map<string, string[] | undefined>}
64
77
  */
65
- export function extractColumns({ select, parentColumns }) {
78
+ export function extractColumns({ select, parentColumns, scopeColumns, cteColumns, tables, outerScope }) {
66
79
  /** @type {Map<string, string[] | undefined>} */
67
80
  const result = new Map()
68
81
 
@@ -74,6 +87,11 @@ export function extractColumns({ select, parentColumns }) {
74
87
  for (const join of select.joins) {
75
88
  aliases.push(join.alias ?? join.table)
76
89
  }
90
+ const collectionContext = {
91
+ cteColumns,
92
+ tables,
93
+ outerAliases: new Set([...aliases, ...outerScope ?? []]),
94
+ }
77
95
 
78
96
  // If any unqualified SELECT * exists, all tables need all columns
79
97
  if (select.columns.some(col => col.type === 'star' && !col.table)) {
@@ -133,26 +151,26 @@ export function extractColumns({ select, parentColumns }) {
133
151
  if (!parentColumns.some(id => id.name === outputName)) continue
134
152
  }
135
153
  // Exclude earlier SELECT aliases so they aren't treated as source columns
136
- collectColumnsFromExpr(col.expr, identifiers, selectAliases)
154
+ collectColumnsFromExpr(col.expr, identifiers, selectAliases, collectionContext)
137
155
  if (col.alias) {
138
156
  selectAliases.add(col.alias)
139
157
  }
140
158
  }
141
159
  }
142
- collectColumnsFromExpr(select.where, identifiers)
160
+ collectColumnsFromExpr(select.where, identifiers, undefined, collectionContext)
143
161
 
144
162
  for (const item of select.orderBy) {
145
- collectColumnsFromExpr(item.expr, identifiers, selectAliases)
163
+ collectColumnsFromExpr(item.expr, identifiers, selectAliases, collectionContext)
146
164
  }
147
165
  for (const expr of select.groupBy) {
148
- collectColumnsFromExpr(expr, identifiers, selectAliases)
166
+ collectColumnsFromExpr(expr, identifiers, selectAliases, collectionContext)
149
167
  }
150
- collectColumnsFromExpr(select.having, identifiers, selectAliases)
168
+ collectColumnsFromExpr(select.having, identifiers, selectAliases, collectionContext)
151
169
  /** @type {string[]} */
152
170
  const visibleLateralAliases = []
153
171
  if (sourceAlias !== undefined) visibleLateralAliases.push(sourceAlias)
154
172
  for (const join of select.joins) {
155
- collectColumnsFromExpr(join.on, identifiers)
173
+ collectColumnsFromExpr(join.on, identifiers, undefined, collectionContext)
156
174
  // USING columns are equi-join keys on both sides; keep them in every
157
175
  // table's needed set so projection pushdown can't prune the join key.
158
176
  if (join.using) {
@@ -165,7 +183,7 @@ export function extractColumns({ select, parentColumns }) {
165
183
  /** @type {IdentifierNode[]} */
166
184
  const lateralArgIdentifiers = []
167
185
  for (const arg of join.fromFunction.args) {
168
- collectColumnsFromExpr(arg, lateralArgIdentifiers)
186
+ collectColumnsFromExpr(arg, lateralArgIdentifiers, undefined, collectionContext)
169
187
  }
170
188
  lateralArgGroups.push({ identifiers: lateralArgIdentifiers, visibleAliases: [...visibleLateralAliases] })
171
189
  }
@@ -175,9 +193,18 @@ export function extractColumns({ select, parentColumns }) {
175
193
  // Partition identifiers by table prefix
176
194
  for (const { prefix, name } of identifiers) {
177
195
  if (prefix) {
178
- // Qualified: add to matching table only
179
- const set = perTable.get(prefix)
180
- if (set) set.add(name)
196
+ if (perTable.has(prefix)) {
197
+ // Table-qualified: add to the matching table only
198
+ perTable.get(prefix)?.add(name)
199
+ } else if (scopeColumns?.has(prefix)) {
200
+ // Struct-field access: the prefix is the source column being read.
201
+ if (aliases.length === 1) {
202
+ perTable.get(aliases[0])?.add(prefix)
203
+ } else {
204
+ // As with an unqualified column in a join, its owner is ambiguous.
205
+ for (const alias of aliases) perTable.set(alias, undefined)
206
+ }
207
+ }
181
208
  } else if (aliases.length > 1) {
182
209
  // Unqualified in a JOIN: can't disambiguate, request all columns from all tables
183
210
  for (const alias of aliases) {
@@ -225,49 +252,50 @@ export function extractColumns({ select, parentColumns }) {
225
252
  * @param {ExprNode} expr
226
253
  * @param {IdentifierNode[]} columns
227
254
  * @param {Set<string>} [aliases] - aliases to exclude from columns
255
+ * @param {ColumnCollectionContext} [context]
228
256
  */
229
- export function collectColumnsFromExpr(expr, columns, aliases) {
257
+ export function collectColumnsFromExpr(expr, columns, aliases, context) {
230
258
  if (!expr) return
231
259
  if (expr.type === 'identifier') {
232
260
  if (expr.prefix || !aliases?.has(expr.name)) {
233
261
  columns.push(expr)
234
262
  }
235
263
  } else if (expr.type === 'binary') {
236
- collectColumnsFromExpr(expr.left, columns, aliases)
237
- collectColumnsFromExpr(expr.right, columns, aliases)
264
+ collectColumnsFromExpr(expr.left, columns, aliases, context)
265
+ collectColumnsFromExpr(expr.right, columns, aliases, context)
238
266
  } else if (expr.type === 'unary') {
239
- collectColumnsFromExpr(expr.argument, columns, aliases)
267
+ collectColumnsFromExpr(expr.argument, columns, aliases, context)
240
268
  } else if (expr.type === 'function') {
241
269
  for (const arg of expr.args) {
242
- collectColumnsFromExpr(arg, columns, aliases)
270
+ collectColumnsFromExpr(arg, columns, aliases, context)
243
271
  }
244
- collectColumnsFromExpr(expr.filter, columns, aliases)
272
+ collectColumnsFromExpr(expr.filter, columns, aliases, context)
245
273
  } else if (expr.type === 'window') {
246
- for (const arg of expr.args) collectColumnsFromExpr(arg, columns, aliases)
247
- for (const p of expr.partitionBy) collectColumnsFromExpr(p, columns, aliases)
248
- for (const o of expr.orderBy) collectColumnsFromExpr(o.expr, columns, aliases)
274
+ for (const arg of expr.args) collectColumnsFromExpr(arg, columns, aliases, context)
275
+ for (const p of expr.partitionBy) collectColumnsFromExpr(p, columns, aliases, context)
276
+ for (const o of expr.orderBy) collectColumnsFromExpr(o.expr, columns, aliases, context)
249
277
  } else if (expr.type === 'cast') {
250
- collectColumnsFromExpr(expr.expr, columns, aliases)
278
+ collectColumnsFromExpr(expr.expr, columns, aliases, context)
251
279
  } else if (expr.type === 'in valuelist') {
252
- collectColumnsFromExpr(expr.expr, columns, aliases)
280
+ collectColumnsFromExpr(expr.expr, columns, aliases, context)
253
281
  for (const val of expr.values) {
254
- collectColumnsFromExpr(val, columns, aliases)
282
+ collectColumnsFromExpr(val, columns, aliases, context)
255
283
  }
256
284
  } else if (expr.type === 'in') {
257
- collectColumnsFromExpr(expr.expr, columns, aliases)
285
+ collectColumnsFromExpr(expr.expr, columns, aliases, context)
258
286
  } else if (expr.type === 'subscript') {
259
- collectColumnsFromExpr(expr.expr, columns, aliases)
260
- collectColumnsFromExpr(expr.index, columns, aliases)
287
+ collectColumnsFromExpr(expr.expr, columns, aliases, context)
288
+ collectColumnsFromExpr(expr.index, columns, aliases, context)
261
289
  } else if (expr.type === 'case') {
262
290
  if (expr.caseExpr) {
263
- collectColumnsFromExpr(expr.caseExpr, columns, aliases)
291
+ collectColumnsFromExpr(expr.caseExpr, columns, aliases, context)
264
292
  }
265
293
  for (const when of expr.whenClauses) {
266
- collectColumnsFromExpr(when.condition, columns, aliases)
267
- collectColumnsFromExpr(when.result, columns, aliases)
294
+ collectColumnsFromExpr(when.condition, columns, aliases, context)
295
+ collectColumnsFromExpr(when.result, columns, aliases, context)
268
296
  }
269
297
  if (expr.elseResult) {
270
- collectColumnsFromExpr(expr.elseResult, columns, aliases)
298
+ collectColumnsFromExpr(expr.elseResult, columns, aliases, context)
271
299
  }
272
300
  }
273
301
  // Subqueries: collect prefixed identifiers for correlated column detection.
@@ -276,60 +304,76 @@ export function collectColumnsFromExpr(expr, columns, aliases) {
276
304
  // from the inner query would incorrectly be attributed to the outer table.
277
305
  if (expr.type === 'subquery' || expr.type === 'in' || expr.type === 'exists' || expr.type === 'not exists') {
278
306
  if (expr.type === 'in') {
279
- collectColumnsFromExpr(expr.expr, columns, aliases)
307
+ collectColumnsFromExpr(expr.expr, columns, aliases, context)
280
308
  }
281
309
  const sub = expr.subquery
282
310
  if (sub) {
283
- /** @type {IdentifierNode[]} */
284
- const inner = []
285
- collectColumnsFromStatement(sub, inner)
286
- for (const id of inner) {
287
- if (id.prefix) columns.push(id)
288
- }
311
+ collectCorrelatedColumnsFromStatement(sub, columns, context)
289
312
  // FROM-function args (e.g. UNNEST in the subquery's FROM) are evaluated
290
313
  // against the outer scope — the table function is itself the FROM, so
291
314
  // any identifier inside its args must be correlated. Push them even if
292
315
  // unprefixed so the outer scan reads the columns they reference.
293
- collectFromFunctionArgs(sub, columns)
316
+ collectFromFunctionArgs(sub, columns, context)
294
317
  }
295
318
  }
296
319
  // No columns: count(*), literal, interval
297
320
  }
298
321
 
299
322
  /**
300
- * Collects identifiers from a subquery statement for correlated column detection.
323
+ * Collects qualified identifiers that escape a subquery statement's scope.
324
+ * Each SELECT filters its own aliases before identifiers propagate outward,
325
+ * preserving ownership through nested and compound subqueries.
301
326
  *
302
327
  * @param {Statement} stmt
303
328
  * @param {IdentifierNode[]} columns
329
+ * @param {ColumnCollectionContext} [context]
304
330
  */
305
- function collectColumnsFromStatement(stmt, columns) {
331
+ function collectCorrelatedColumnsFromStatement(stmt, columns, context) {
306
332
  if (stmt.type === 'compound') {
307
- collectColumnsFromStatement(stmt.left, columns)
308
- collectColumnsFromStatement(stmt.right, columns)
333
+ collectCorrelatedColumnsFromStatement(stmt.left, columns, context)
334
+ collectCorrelatedColumnsFromStatement(stmt.right, columns, context)
309
335
  return
310
336
  }
311
337
  if (stmt.type === 'with') {
312
- collectColumnsFromStatement(stmt.query, columns)
338
+ collectCorrelatedColumnsFromStatement(stmt.query, columns, context)
313
339
  return
314
340
  }
341
+ const scope = statementScope(stmt) ?? []
342
+ const nestedContext = context && {
343
+ ...context,
344
+ outerAliases: new Set([...context.outerAliases ?? [], ...scope]),
345
+ }
346
+ /** @type {IdentifierNode[]} */
347
+ const identifiers = []
315
348
  for (const col of stmt.columns) {
316
- if (col.type === 'derived') collectColumnsFromExpr(col.expr, columns)
349
+ if (col.type === 'derived') collectColumnsFromExpr(col.expr, identifiers, undefined, nestedContext)
317
350
  }
318
- collectColumnsFromExpr(stmt.where, columns)
351
+ collectColumnsFromExpr(stmt.where, identifiers, undefined, nestedContext)
319
352
  if (stmt.from && stmt.from.type === 'subquery') {
320
- collectColumnsFromStatement(stmt.from.query, columns)
353
+ collectCorrelatedColumnsFromStatement(stmt.from.query, identifiers, nestedContext)
321
354
  }
322
355
  for (const join of stmt.joins) {
323
- collectColumnsFromExpr(join.on, columns)
356
+ collectColumnsFromExpr(join.on, identifiers, undefined, nestedContext)
324
357
  if (join.fromFunction) {
325
358
  for (const arg of join.fromFunction.args) {
326
- collectColumnsFromExpr(arg, columns)
359
+ collectColumnsFromExpr(arg, identifiers, undefined, nestedContext)
327
360
  }
328
361
  }
329
362
  }
330
- for (const expr of stmt.groupBy) collectColumnsFromExpr(expr, columns)
331
- collectColumnsFromExpr(stmt.having, columns)
332
- for (const item of stmt.orderBy) collectColumnsFromExpr(item.expr, columns)
363
+ for (const expr of stmt.groupBy) collectColumnsFromExpr(expr, identifiers, undefined, nestedContext)
364
+ collectColumnsFromExpr(stmt.having, identifiers, undefined, nestedContext)
365
+ for (const item of stmt.orderBy) collectColumnsFromExpr(item.expr, identifiers, undefined, nestedContext)
366
+
367
+ const localColumns = context
368
+ ? collectScopeColumns({ select: stmt, cteColumns: context.cteColumns, tables: context.tables })
369
+ : new Set()
370
+ for (const identifier of identifiers) {
371
+ if (identifier.prefix &&
372
+ !scope.includes(identifier.prefix) &&
373
+ (context?.outerAliases?.has(identifier.prefix) || !localColumns.has(identifier.prefix))) {
374
+ columns.push(identifier)
375
+ }
376
+ }
333
377
  }
334
378
 
335
379
  /**
@@ -339,23 +383,24 @@ function collectColumnsFromStatement(stmt, columns) {
339
383
  *
340
384
  * @param {Statement} stmt
341
385
  * @param {IdentifierNode[]} columns
386
+ * @param {ColumnCollectionContext} [context]
342
387
  */
343
- function collectFromFunctionArgs(stmt, columns) {
388
+ function collectFromFunctionArgs(stmt, columns, context) {
344
389
  if (stmt.type === 'compound') {
345
- collectFromFunctionArgs(stmt.left, columns)
346
- collectFromFunctionArgs(stmt.right, columns)
390
+ collectFromFunctionArgs(stmt.left, columns, context)
391
+ collectFromFunctionArgs(stmt.right, columns, context)
347
392
  return
348
393
  }
349
394
  if (stmt.type === 'with') {
350
- collectFromFunctionArgs(stmt.query, columns)
395
+ collectFromFunctionArgs(stmt.query, columns, context)
351
396
  return
352
397
  }
353
398
  if (stmt.from?.type === 'function') {
354
399
  for (const arg of stmt.from.args) {
355
- collectColumnsFromExpr(arg, columns)
400
+ collectColumnsFromExpr(arg, columns, undefined, context)
356
401
  }
357
402
  } else if (stmt.from?.type === 'subquery') {
358
- collectFromFunctionArgs(stmt.from.query, columns)
403
+ collectFromFunctionArgs(stmt.from.query, columns, context)
359
404
  }
360
405
  }
361
406
 
@@ -473,7 +518,8 @@ export function inferSelectSourceColumns({ select, cteColumns, tables }) {
473
518
  * @returns {string[]}
474
519
  */
475
520
  function lookupTableColumns(table, cteColumns, tables) {
476
- return cteColumns?.get(table.toLowerCase()) ?? tables?.[table]?.columns ?? []
521
+ const source = tables?.[table]
522
+ return cteColumns?.get(table.toLowerCase()) ?? (source ? dataSourceColumns(source) : [])
477
523
  }
478
524
 
479
525
  /**
package/src/plan/plan.js CHANGED
@@ -82,6 +82,8 @@ function planSetOperation({ compound, ctePlans, cteColumns, tables, parentColumn
82
82
  all: compound.all,
83
83
  left,
84
84
  right,
85
+ leftScope: statementScope(compound.left),
86
+ rightScope: statementScope(compound.right),
85
87
  }
86
88
 
87
89
  if (compound.orderBy.length) {
@@ -206,7 +208,14 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
206
208
  // included so they are only applied to fresh scans, not CTE/subquery plans)
207
209
  /** @type {ScanOptions} */
208
210
  const hints = {}
209
- const perTableColumns = extractColumns({ select: originalSelect, parentColumns })
211
+ const perTableColumns = extractColumns({
212
+ select: originalSelect,
213
+ parentColumns,
214
+ scopeColumns,
215
+ cteColumns,
216
+ tables,
217
+ outerScope,
218
+ })
210
219
  if (sourceAlias !== undefined) hints.columns = perTableColumns.get(sourceAlias)
211
220
  // Capture what the parent reads from a FROM subquery before the reset
212
221
  // below, so aggregate outputs it never reads can still be pruned when the
@@ -406,7 +415,7 @@ function planFrom({ select, ctePlans, cteColumns, hints, tables, outerScope }) {
406
415
  return ctePlan
407
416
  }
408
417
  validateScan({ ...select.from, hints, tables })
409
- return { type: 'Scan', table: select.from.table, hints }
418
+ return { type: 'Scan', table: select.from.table, alias: select.from.alias, hints }
410
419
  } else if (select.from.type === 'function') {
411
420
  for (const arg of select.from.args) {
412
421
  validateNoIdentifiers(arg, select.from.funcName, outerScope)
@@ -545,7 +554,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
545
554
  // For CTE joins, use CTE column metadata for hints
546
555
  rightHints.columns = perTableColumns.get(rightTable) ?? cteColumns?.get(join.table.toLowerCase())
547
556
  }
548
- rightScan = ctePlan ?? { type: 'Scan', table: join.table, hints: rightHints }
557
+ rightScan = ctePlan ?? { type: 'Scan', table: join.table, alias: join.alias, hints: rightHints }
549
558
  }
550
559
 
551
560
  if (join.joinType === 'POSITIONAL') {
@@ -23,6 +23,7 @@ export type QueryPlan =
23
23
  export interface ScanNode {
24
24
  type: 'Scan'
25
25
  table: string
26
+ alias?: string
26
27
  hints: ScanOptions
27
28
  }
28
29
 
@@ -130,6 +131,8 @@ export interface SetOperationNode {
130
131
  all: boolean
131
132
  left: QueryPlan
132
133
  right: QueryPlan
134
+ leftScope?: string[]
135
+ rightScope?: string[]
133
136
  }
134
137
 
135
138
  // Wraps a derived-table or CTE subplan with the lexical alias scope of its
package/src/types.d.ts CHANGED
@@ -11,10 +11,148 @@ export { QueryPlan } from './plan/types.js'
11
11
  export interface QueryResults {
12
12
  columns: string[]
13
13
  rows(): AsyncGenerator<AsyncRow>
14
+ batches?(): AsyncIterable<AsyncBatch>
14
15
  numRows?: number
15
16
  maxRows?: number
16
17
  }
17
18
 
19
+ export type SqlType =
20
+ | { type: 'unknown' }
21
+ | { type: 'string' }
22
+ | { type: 'number' }
23
+ | { type: 'bigint' }
24
+ | { type: 'boolean' }
25
+ | { type: 'date' }
26
+ | { type: 'array', items: SqlType }
27
+ | { type: 'struct', fields: readonly Field[] }
28
+
29
+ export interface Field {
30
+ id: number
31
+ name: string
32
+ dataType: SqlType
33
+ nullable: boolean
34
+ }
35
+
36
+ export interface RelationSchema {
37
+ fields: readonly Field[]
38
+ }
39
+
40
+ /**
41
+ * A selection over a base domain of `length` rows.
42
+ */
43
+ export type RowSelection =
44
+ | { type: 'all', length: number }
45
+ | { type: 'range', start: number, end: number, length: number }
46
+ | { type: 'indices', indices: Uint32Array, length: number }
47
+
48
+ export type NumericArray =
49
+ | Int8Array
50
+ | Uint8Array
51
+ | Uint8ClampedArray
52
+ | Int16Array
53
+ | Uint16Array
54
+ | Int32Array
55
+ | Uint32Array
56
+ | Float32Array
57
+ | Float64Array
58
+ | BigInt64Array
59
+ | BigUint64Array
60
+
61
+ export type ColumnVector =
62
+ | {
63
+ type: 'values'
64
+ values: readonly SqlPrimitive[]
65
+ length: number
66
+ }
67
+ | {
68
+ type: 'typed'
69
+ values: NumericArray
70
+ validity?: Uint8Array
71
+ length: number
72
+ }
73
+ | {
74
+ type: 'constant'
75
+ value: SqlPrimitive
76
+ length: number
77
+ }
78
+ | {
79
+ type: 'selected'
80
+ source: ColumnVector
81
+ selection: RowSelection
82
+ length: number
83
+ }
84
+
85
+ export interface ColumnReadRequest {
86
+ batch: AsyncBatch
87
+ selection: RowSelection
88
+ signal?: AbortSignal
89
+ rowOffset?: number
90
+ rowOrdinals?: ColumnVector
91
+ }
92
+
93
+ export type ColumnResult = ColumnVector | Promise<ColumnVector>
94
+ export type ReadColumn = (request: ColumnReadRequest) => ColumnResult
95
+
96
+ export type BatchColumn =
97
+ | ColumnVector
98
+ | {
99
+ read: ReadColumn
100
+ input?: AsyncBatch
101
+ rowOffset?: number
102
+ rowOrdinals?: ColumnVector
103
+ }
104
+
105
+ export interface AsyncBatch {
106
+ selection: RowSelection
107
+ columns: readonly BatchColumn[]
108
+ }
109
+
110
+ export interface ReadBatchColumnOptions {
111
+ batch: AsyncBatch
112
+ columnIndex: number
113
+ selection?: RowSelection
114
+ signal?: AbortSignal
115
+ }
116
+
117
+ export interface RowsToBatchesOptions {
118
+ batchRows?: number
119
+ signal?: AbortSignal
120
+ }
121
+
122
+ export interface ColumnDemand {
123
+ field: number
124
+ phase: number
125
+ purpose: 'filter' | 'output'
126
+ mode: 'required' | 'deferred'
127
+ }
128
+
129
+ export interface ScanRequest {
130
+ columns: readonly ColumnDemand[]
131
+ filter?: ExprNode
132
+ limit?: number
133
+ offset?: number
134
+ }
135
+
136
+ export interface ScanProperties {
137
+ exactRows?: number
138
+ maxRows?: number
139
+ }
140
+
141
+ export interface ScanResidual {
142
+ filter?: ExprNode
143
+ limit?: number
144
+ offset?: number
145
+ }
146
+
147
+ export interface PreparedScan {
148
+ schema: RelationSchema
149
+ residual: ScanResidual
150
+ properties: ScanProperties
151
+ batches(options?: { signal?: AbortSignal }): AsyncIterable<AsyncBatch>
152
+ }
153
+
154
+ export type PrepareScan = (request: ScanRequest) => PreparedScan
155
+
18
156
  // parseSql(options)
19
157
  export interface ParseSqlOptions {
20
158
  query: string
@@ -72,16 +210,30 @@ export type AsyncCell = () => Promise<SqlPrimitive>
72
210
  export type Row = Record<string, SqlPrimitive>[]
73
211
 
74
212
  /**
75
- * Async data source for streaming SQL execution.
213
+ * Async data source for streaming SQL execution. A source must implement
214
+ * either scan() or prepareScan().
76
215
  */
77
- export interface AsyncDataSource {
216
+ interface AsyncDataSourceBase {
78
217
  numRows?: number
79
- columns: string[]
80
- scan(options: ScanOptions): ScanResults
81
218
  // Optional method for fast column scans
82
219
  scanColumn?(options: ScanColumnOptions): AsyncIterable<ArrayLike<SqlPrimitive>> | ScanColumnResults
83
220
  }
84
221
 
222
+ export type AsyncDataSource = AsyncDataSourceBase & (
223
+ | {
224
+ columns: string[]
225
+ scan(options: ScanOptions): ScanResults
226
+ schema?: RelationSchema
227
+ prepareScan?: PrepareScan
228
+ }
229
+ | {
230
+ columns?: string[]
231
+ scan?(options: ScanOptions): ScanResults
232
+ schema: RelationSchema
233
+ prepareScan: PrepareScan
234
+ }
235
+ )
236
+
85
237
  /**
86
238
  * Result of a scan: streaming rows and flags indicating which hints were
87
239
  * applied by the data source.
@@ -1,3 +1,4 @@
1
+ import { dataSourceColumns } from '../backend/dataSource.js'
1
2
  import { ExecutionError } from './executionErrors.js'
2
3
 
3
4
  /**
@@ -34,11 +35,12 @@ export function validateTable({ table, qualified, tables, positionStart, positio
34
35
  export function validateScan({ table, hints, tables, positionStart, positionEnd }) {
35
36
  if (!tables) return
36
37
  const resolved = validateTable({ table, tables, positionStart, positionEnd })
37
- const missingColumn = hints.columns?.find(col => !resolved.columns.includes(col))
38
+ const columns = dataSourceColumns(resolved)
39
+ const missingColumn = hints.columns?.find(col => !columns.includes(col))
38
40
  if (missingColumn) {
39
41
  throw new ColumnNotFoundError({
40
42
  missingColumn,
41
- availableColumns: resolved.columns,
43
+ availableColumns: columns,
42
44
  positionStart,
43
45
  positionEnd,
44
46
  })