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.
- package/package.json +2 -2
- package/src/backend/batch.js +236 -0
- package/src/backend/batchAdapters.js +178 -0
- package/src/backend/dataSource.js +17 -1
- package/src/execute/aggregates.js +2 -1
- package/src/execute/batchResults.js +28 -0
- package/src/execute/batches.js +274 -0
- package/src/execute/execute.js +440 -24
- package/src/execute/streamingAggregate.js +151 -1
- package/src/execute/utils.js +37 -0
- package/src/expression/batch.js +659 -0
- package/src/expression/binary.js +17 -4
- package/src/expression/evaluate.js +39 -109
- package/src/expression/scalar.js +105 -0
- package/src/index.d.ts +44 -1
- package/src/index.js +9 -0
- package/src/internalTypes.d.ts +28 -0
- package/src/plan/columns.js +105 -59
- package/src/plan/plan.js +12 -3
- package/src/plan/types.d.ts +3 -0
- package/src/types.d.ts +156 -4
- package/src/validation/tables.js +4 -2
package/src/execute/execute.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { selectedRowCount } from '../backend/batch.js'
|
|
2
|
+
import { batchesToRows } from '../backend/batchAdapters.js'
|
|
3
|
+
import { dataSourceColumns, memorySource } from '../backend/dataSource.js'
|
|
2
4
|
import { derivedAlias } from '../expression/alias.js'
|
|
5
|
+
import { compileBatchExpression } from '../expression/batch.js'
|
|
3
6
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
4
7
|
import { parseSql } from '../parse/parse.js'
|
|
5
8
|
import { planSql, planStatement } from '../plan/plan.js'
|
|
6
|
-
import { statementScope } from '../plan/columns.js'
|
|
9
|
+
import { collectColumnsFromExpr, statementScope } from '../plan/columns.js'
|
|
7
10
|
import { validateScan, validateTable } from '../validation/tables.js'
|
|
8
11
|
import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
|
|
12
|
+
import { batchResult } from './batchResults.js'
|
|
13
|
+
import { distinctBatches, filterBatches, limitBatches, projectExpressionBatches } from './batches.js'
|
|
9
14
|
import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
|
|
10
15
|
import { normalizeScanColumnResult } from './scanColumn.js'
|
|
11
16
|
import { executeSort } from './sort.js'
|
|
@@ -14,7 +19,8 @@ import { executeWindow } from './window.js'
|
|
|
14
19
|
import { yieldToEventLoop } from './yield.js'
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
|
-
* @import {
|
|
22
|
+
* @import { BatchProjection, CompiledBatchExpression } from '../internalTypes.js'
|
|
23
|
+
* @import { AsyncBatch, AsyncCells, AsyncDataSource, AsyncRow, ColumnDemand, ColumnVector, DerivedColumn, ExecuteContext, ExecuteSqlOptions, ExprNode, IdentifierNode, PreparedScan, QueryResults, RelationSchema, ScanRequest, SelectColumn, SqlPrimitive, Statement } from '../types.js'
|
|
18
24
|
* @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode, TableFunctionNode } from '../plan/types.js'
|
|
19
25
|
*/
|
|
20
26
|
|
|
@@ -278,6 +284,12 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
278
284
|
const table = validateTable({ ...plan, tables })
|
|
279
285
|
validateScan({ ...plan, tables })
|
|
280
286
|
const hasLimitOffset = plan.hints.limit !== undefined || plan.hints.offset // 0 offset is noop
|
|
287
|
+
const scanContext = { ...context, scope: [plan.alias ?? plan.table] }
|
|
288
|
+
|
|
289
|
+
if (!existingColumnResult && table.prepareScan && table.schema) {
|
|
290
|
+
const prepared = table.prepareScan(scanRequest(plan, table.schema, scanContext))
|
|
291
|
+
return executePreparedScan({ plan, prepared, context: scanContext, table })
|
|
292
|
+
}
|
|
281
293
|
|
|
282
294
|
// Fast path: single column scan. As with scan(), hints the source did not
|
|
283
295
|
// apply are handled by the engine over the returned column values.
|
|
@@ -294,9 +306,43 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
294
306
|
if (columnResult && scanColumnOptions) {
|
|
295
307
|
const column = plan.hints.columns[0]
|
|
296
308
|
const scanRows = computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
|
|
309
|
+
const columns = [column]
|
|
310
|
+
const appliedLimitOffset = plan.hints.where
|
|
311
|
+
? false
|
|
312
|
+
: columnResult.appliedLimitOffset
|
|
313
|
+
const residualFilter = plan.hints.where && !columnResult.appliedWhere
|
|
314
|
+
? compileBatchExpression(plan.hints.where, columns)
|
|
315
|
+
: undefined
|
|
316
|
+
|
|
317
|
+
/** @returns {AsyncIterable<AsyncBatch>} */
|
|
318
|
+
function makeBatches() {
|
|
319
|
+
/** @type {AsyncIterable<AsyncBatch>} */
|
|
320
|
+
let batches = columnBatches(columnResult.chunks(), signal)
|
|
321
|
+
if (residualFilter) {
|
|
322
|
+
const targetRows = plan.hints.limit === undefined
|
|
323
|
+
? undefined
|
|
324
|
+
: plan.hints.limit + (plan.hints.offset ?? 0)
|
|
325
|
+
batches = filterBatches(batches, residualFilter, signal, targetRows)
|
|
326
|
+
}
|
|
327
|
+
if (!appliedLimitOffset && hasLimitOffset) {
|
|
328
|
+
batches = limitBatches(batches, plan.hints.limit, plan.hints.offset, signal)
|
|
329
|
+
}
|
|
330
|
+
return batches
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const canUseBatches = !plan.hints.where || columnResult.appliedWhere || residualFilter !== undefined
|
|
334
|
+
if (canUseBatches) {
|
|
335
|
+
return batchResult({
|
|
336
|
+
columns,
|
|
337
|
+
numRows: plan.hints.where ? undefined : scanRows,
|
|
338
|
+
maxRows: scanRows,
|
|
339
|
+
batches: makeBatches,
|
|
340
|
+
signal,
|
|
341
|
+
})
|
|
342
|
+
}
|
|
297
343
|
return {
|
|
298
|
-
columns
|
|
299
|
-
numRows:
|
|
344
|
+
columns,
|
|
345
|
+
numRows: undefined,
|
|
300
346
|
maxRows: scanRows,
|
|
301
347
|
async *rows() {
|
|
302
348
|
const columns = [column]
|
|
@@ -315,13 +361,8 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
315
361
|
}
|
|
316
362
|
})()
|
|
317
363
|
|
|
318
|
-
|
|
319
|
-
result = filterRows(result, plan.hints.where, context, plan.hints.limit)
|
|
320
|
-
}
|
|
364
|
+
result = filterRows(result, plan.hints.where, scanContext, plan.hints.limit)
|
|
321
365
|
// Filtered LIMIT/OFFSET was intentionally not passed to scanColumn.
|
|
322
|
-
const appliedLimitOffset = plan.hints.where
|
|
323
|
-
? false
|
|
324
|
-
: columnResult.appliedLimitOffset
|
|
325
366
|
if (!appliedLimitOffset && hasLimitOffset) {
|
|
326
367
|
result = limitRows(result, plan.hints.limit, plan.hints.offset, signal)
|
|
327
368
|
}
|
|
@@ -334,6 +375,10 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
334
375
|
}
|
|
335
376
|
}
|
|
336
377
|
|
|
378
|
+
if (!table.scan) {
|
|
379
|
+
throw new Error(`Data source "${plan.table}" does not implement scan()`)
|
|
380
|
+
}
|
|
381
|
+
|
|
337
382
|
// do the scan
|
|
338
383
|
const scanResult = table.scan({ ...plan.hints, signal })
|
|
339
384
|
const { appliedWhere, appliedLimitOffset } = scanResult
|
|
@@ -345,7 +390,7 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
345
390
|
|
|
346
391
|
const scanRows = computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
|
|
347
392
|
return {
|
|
348
|
-
columns: plan.hints.columns ?? table
|
|
393
|
+
columns: plan.hints.columns ?? dataSourceColumns(table),
|
|
349
394
|
numRows: !plan.hints.where ? scanRows : undefined,
|
|
350
395
|
maxRows: scanRows,
|
|
351
396
|
async *rows() {
|
|
@@ -353,7 +398,7 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
353
398
|
|
|
354
399
|
// Apply WHERE if data source did not
|
|
355
400
|
if (!appliedWhere && plan.hints.where) {
|
|
356
|
-
result = filterRows(result, plan.hints.where,
|
|
401
|
+
result = filterRows(result, plan.hints.where, scanContext, plan.hints.limit)
|
|
357
402
|
}
|
|
358
403
|
|
|
359
404
|
// Apply LIMIT/OFFSET if data source did not
|
|
@@ -370,6 +415,137 @@ export function executeScan(plan, context, existingColumnResult) {
|
|
|
370
415
|
}
|
|
371
416
|
}
|
|
372
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Executes a prepared native-batch scan and applies only the residual work
|
|
420
|
+
* reported by the source.
|
|
421
|
+
*
|
|
422
|
+
* @param {Object} options
|
|
423
|
+
* @param {ScanNode} options.plan
|
|
424
|
+
* @param {PreparedScan} options.prepared
|
|
425
|
+
* @param {ExecuteContext} options.context
|
|
426
|
+
* @param {AsyncDataSource} options.table
|
|
427
|
+
* @returns {QueryResults}
|
|
428
|
+
*/
|
|
429
|
+
function executePreparedScan({ plan, prepared, context, table }) {
|
|
430
|
+
const { signal } = context
|
|
431
|
+
const { residual, properties, schema } = prepared
|
|
432
|
+
const hasRequestedRange = plan.hints.limit !== undefined || Boolean(plan.hints.offset)
|
|
433
|
+
if (residual.filter && hasRequestedRange && (
|
|
434
|
+
residual.limit !== plan.hints.limit || (residual.offset ?? 0) !== (plan.hints.offset ?? 0)
|
|
435
|
+
)) {
|
|
436
|
+
throw new Error(`Data source "${plan.table}" applied limit/offset without applying where`)
|
|
437
|
+
}
|
|
438
|
+
const columns = schema.fields.map(function fieldName(field) { return field.name })
|
|
439
|
+
const residualFilter = residual.filter
|
|
440
|
+
? compileUnscopedBatchExpression(residual.filter, columns, context)
|
|
441
|
+
: undefined
|
|
442
|
+
const canUseBatches = !residual.filter || residualFilter !== undefined
|
|
443
|
+
|
|
444
|
+
/** @returns {AsyncIterable<AsyncBatch>} */
|
|
445
|
+
function makeBatches() {
|
|
446
|
+
/** @type {AsyncIterable<AsyncBatch>} */
|
|
447
|
+
let batches = prepared.batches({ signal })
|
|
448
|
+
if (residualFilter) {
|
|
449
|
+
const targetRows = residual.limit === undefined
|
|
450
|
+
? undefined
|
|
451
|
+
: residual.limit + (residual.offset ?? 0)
|
|
452
|
+
batches = filterBatches(batches, residualFilter, signal, targetRows)
|
|
453
|
+
}
|
|
454
|
+
if (residual.limit !== undefined || residual.offset) {
|
|
455
|
+
batches = limitBatches(batches, residual.limit, residual.offset, signal)
|
|
456
|
+
}
|
|
457
|
+
return batches
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const exactRows = properties.exactRows === undefined
|
|
461
|
+
? computeScanRows(plan.hints.where ? undefined : table.numRows, plan.hints.limit, plan.hints.offset)
|
|
462
|
+
: computeScanRows(properties.exactRows, residual.limit, residual.offset)
|
|
463
|
+
const preparedMaxRows = properties.maxRows ?? properties.exactRows
|
|
464
|
+
const maxRows = preparedMaxRows === undefined
|
|
465
|
+
? computeScanRows(table.numRows, plan.hints.limit, plan.hints.offset)
|
|
466
|
+
: computeScanRows(preparedMaxRows, residual.limit, residual.offset)
|
|
467
|
+
const metadata = {
|
|
468
|
+
columns,
|
|
469
|
+
numRows: residual.filter ? undefined : exactRows,
|
|
470
|
+
maxRows,
|
|
471
|
+
}
|
|
472
|
+
if (canUseBatches) {
|
|
473
|
+
return batchResult({ ...metadata, batches: makeBatches, signal })
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
...metadata,
|
|
477
|
+
async *rows() {
|
|
478
|
+
let result = batchesToRows(prepared.batches({ signal }), columns, signal)
|
|
479
|
+
if (residual.filter) result = filterRows(result, residual.filter, context, residual.limit)
|
|
480
|
+
if (residual.limit !== undefined || residual.offset) {
|
|
481
|
+
result = limitRows(result, residual.limit, residual.offset, signal)
|
|
482
|
+
}
|
|
483
|
+
yield* result
|
|
484
|
+
signal?.throwIfAborted()
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Builds a generic demand schedule from the scan's logical columns. Predicate
|
|
491
|
+
* fields are required in phase zero; remaining output fields stay deferred.
|
|
492
|
+
*
|
|
493
|
+
* @param {ScanNode} plan
|
|
494
|
+
* @param {RelationSchema} schema
|
|
495
|
+
* @param {ExecuteContext} context
|
|
496
|
+
* @returns {ScanRequest}
|
|
497
|
+
*/
|
|
498
|
+
function scanRequest(plan, schema, context) {
|
|
499
|
+
const currentScope = context.scope ?? [plan.table]
|
|
500
|
+
/** @type {IdentifierNode[]} */
|
|
501
|
+
const predicateIdentifiers = []
|
|
502
|
+
collectColumnsFromExpr(plan.hints.where, predicateIdentifiers, undefined, {
|
|
503
|
+
cteColumns: context.cteColumns,
|
|
504
|
+
tables: context.tables,
|
|
505
|
+
outerAliases: new Set([...currentScope, ...context.outerAliases ?? []]),
|
|
506
|
+
})
|
|
507
|
+
const predicateNames = new Set()
|
|
508
|
+
for (const identifier of predicateIdentifiers) {
|
|
509
|
+
if (!identifier.prefix) {
|
|
510
|
+
predicateNames.add(identifier.name)
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
if (currentScope.includes(identifier.prefix)) {
|
|
514
|
+
predicateNames.add(identifier.name)
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
if (context.outerAliases?.has(identifier.prefix)) continue
|
|
518
|
+
const baseField = schema.fields.some(function fieldOwnsPrefix(field) {
|
|
519
|
+
return field.name === identifier.prefix
|
|
520
|
+
})
|
|
521
|
+
if (baseField) predicateNames.add(identifier.prefix)
|
|
522
|
+
}
|
|
523
|
+
const requestedNames = plan.hints.columns
|
|
524
|
+
? [...plan.hints.columns]
|
|
525
|
+
: schema.fields.map(function fieldName(field) { return field.name })
|
|
526
|
+
for (const name of predicateNames) {
|
|
527
|
+
if (!requestedNames.includes(name)) requestedNames.push(name)
|
|
528
|
+
}
|
|
529
|
+
/** @type {ColumnDemand[]} */
|
|
530
|
+
const columns = requestedNames.map(function columnDemand(name) {
|
|
531
|
+
const field = schema.fields.find(function fieldName(candidate) { return candidate.name === name })
|
|
532
|
+
if (!field) throw new Error(`Prepared source schema does not contain column "${name}"`)
|
|
533
|
+
const predicate = predicateNames.has(name)
|
|
534
|
+
return {
|
|
535
|
+
field: field.id,
|
|
536
|
+
phase: predicate ? 0 : 1,
|
|
537
|
+
purpose: predicate ? 'filter' : 'output',
|
|
538
|
+
mode: predicate ? 'required' : 'deferred',
|
|
539
|
+
}
|
|
540
|
+
})
|
|
541
|
+
return {
|
|
542
|
+
columns,
|
|
543
|
+
filter: plan.hints.where,
|
|
544
|
+
limit: plan.hints.limit,
|
|
545
|
+
offset: plan.hints.offset,
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
373
549
|
/**
|
|
374
550
|
* Executes a Count node using numRows when available, falling back to scan
|
|
375
551
|
*
|
|
@@ -392,7 +568,24 @@ function executeCount(plan, context) {
|
|
|
392
568
|
// Use source numRows if available
|
|
393
569
|
if (table.numRows !== undefined) return table.numRows
|
|
394
570
|
|
|
571
|
+
if (table.prepareScan && table.schema) {
|
|
572
|
+
const prepared = table.prepareScan({ columns: [] })
|
|
573
|
+
if (prepared.properties.exactRows !== undefined) {
|
|
574
|
+
return prepared.properties.exactRows
|
|
575
|
+
}
|
|
576
|
+
let count = 0
|
|
577
|
+
for await (const batch of prepared.batches({ signal })) {
|
|
578
|
+
signal?.throwIfAborted()
|
|
579
|
+
count += selectedRowCount(batch.selection)
|
|
580
|
+
}
|
|
581
|
+
signal?.throwIfAborted()
|
|
582
|
+
return count
|
|
583
|
+
}
|
|
584
|
+
|
|
395
585
|
// Fall back to counting rows via scan
|
|
586
|
+
if (!table.scan) {
|
|
587
|
+
throw new Error(`Data source "${plan.table}" does not implement scan()`)
|
|
588
|
+
}
|
|
396
589
|
let count = 0
|
|
397
590
|
const { rows } = table.scan({ signal })
|
|
398
591
|
// eslint-disable-next-line no-unused-vars
|
|
@@ -512,6 +705,41 @@ async function* limitRows(rows, limit = Infinity, offset = 0, signal) {
|
|
|
512
705
|
}
|
|
513
706
|
}
|
|
514
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Compiles a batch expression only when qualified identifiers do not depend
|
|
710
|
+
* on current or outer row scope.
|
|
711
|
+
*
|
|
712
|
+
* @param {ExprNode} expression
|
|
713
|
+
* @param {string[]} columns
|
|
714
|
+
* @param {ExecuteContext} context
|
|
715
|
+
* @returns {CompiledBatchExpression | undefined}
|
|
716
|
+
*/
|
|
717
|
+
function compileUnscopedBatchExpression(expression, columns, context) {
|
|
718
|
+
return referencesRowScope(expression, columns, context)
|
|
719
|
+
? undefined
|
|
720
|
+
: compileBatchExpression(expression, columns)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Returns whether an expression reads a qualified identifier from row scope.
|
|
725
|
+
*
|
|
726
|
+
* @param {ExprNode} expression
|
|
727
|
+
* @param {string[]} columns
|
|
728
|
+
* @param {ExecuteContext} context
|
|
729
|
+
* @returns {boolean}
|
|
730
|
+
*/
|
|
731
|
+
function referencesRowScope(expression, columns, context) {
|
|
732
|
+
/** @type {IdentifierNode[]} */
|
|
733
|
+
const identifiers = []
|
|
734
|
+
collectColumnsFromExpr(expression, identifiers)
|
|
735
|
+
return identifiers.some(function scopedIdentifier(identifier) {
|
|
736
|
+
return Boolean(identifier.prefix && (
|
|
737
|
+
context.outerAliases?.has(identifier.prefix) ||
|
|
738
|
+
context.scope?.includes(identifier.prefix) && columns.includes(identifier.prefix)
|
|
739
|
+
))
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
|
|
515
743
|
/**
|
|
516
744
|
* Executes a filter operation (WHERE clause)
|
|
517
745
|
*
|
|
@@ -521,6 +749,22 @@ async function* limitRows(rows, limit = Infinity, offset = 0, signal) {
|
|
|
521
749
|
*/
|
|
522
750
|
function executeFilter(plan, context) {
|
|
523
751
|
const child = executePlan({ plan: plan.child, context })
|
|
752
|
+
const expression = child.batches
|
|
753
|
+
? compileUnscopedBatchExpression(plan.condition, child.columns, context)
|
|
754
|
+
: undefined
|
|
755
|
+
if (expression && child.batches) {
|
|
756
|
+
const readChildBatches = child.batches
|
|
757
|
+
/** @returns {AsyncIterable<AsyncBatch>} */
|
|
758
|
+
function makeBatches() {
|
|
759
|
+
return filterBatches(readChildBatches(), expression, context.signal)
|
|
760
|
+
}
|
|
761
|
+
return batchResult({
|
|
762
|
+
columns: child.columns,
|
|
763
|
+
maxRows: child.maxRows,
|
|
764
|
+
batches: makeBatches,
|
|
765
|
+
signal: context.signal,
|
|
766
|
+
})
|
|
767
|
+
}
|
|
524
768
|
return {
|
|
525
769
|
columns: child.columns,
|
|
526
770
|
maxRows: child.maxRows,
|
|
@@ -539,9 +783,32 @@ function executeProject(plan, context) {
|
|
|
539
783
|
const child = executePlan({ plan: plan.child, context })
|
|
540
784
|
const columns = selectColumnNames(plan.columns, child.columns)
|
|
541
785
|
|
|
542
|
-
const resolveable = plan.columns.every(
|
|
543
|
-
|
|
544
|
-
|
|
786
|
+
const resolveable = plan.columns.every(function resolvesDirectly(column) {
|
|
787
|
+
if (column.type === 'star') return true
|
|
788
|
+
if (column.expr.type !== 'identifier') return false
|
|
789
|
+
const sourceName = column.expr.prefix
|
|
790
|
+
? `${column.expr.prefix}.${column.expr.name}`
|
|
791
|
+
: column.expr.name
|
|
792
|
+
return child.columns.includes(sourceName)
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
const projection = child.batches
|
|
796
|
+
? batchProjection(plan.columns, columns, child.columns, context)
|
|
797
|
+
: undefined
|
|
798
|
+
if (projection && child.batches) {
|
|
799
|
+
const readChildBatches = child.batches
|
|
800
|
+
/** @returns {AsyncIterable<AsyncBatch>} */
|
|
801
|
+
function makeBatches() {
|
|
802
|
+
return projectExpressionBatches(readChildBatches(), projection)
|
|
803
|
+
}
|
|
804
|
+
return batchResult({
|
|
805
|
+
columns,
|
|
806
|
+
numRows: child.numRows,
|
|
807
|
+
maxRows: child.maxRows,
|
|
808
|
+
batches: makeBatches,
|
|
809
|
+
signal: context.signal,
|
|
810
|
+
})
|
|
811
|
+
}
|
|
545
812
|
|
|
546
813
|
return {
|
|
547
814
|
columns,
|
|
@@ -631,6 +898,19 @@ function executeProject(plan, context) {
|
|
|
631
898
|
*/
|
|
632
899
|
function executeDistinct(plan, context) {
|
|
633
900
|
const child = executePlan({ plan: plan.child, context })
|
|
901
|
+
if (child.batches) {
|
|
902
|
+
const readChildBatches = child.batches
|
|
903
|
+
/** @returns {AsyncIterable<AsyncBatch>} */
|
|
904
|
+
function makeBatches() {
|
|
905
|
+
return distinctBatches(readChildBatches(), context.signal)
|
|
906
|
+
}
|
|
907
|
+
return batchResult({
|
|
908
|
+
columns: child.columns,
|
|
909
|
+
maxRows: child.maxRows,
|
|
910
|
+
batches: makeBatches,
|
|
911
|
+
signal: context.signal,
|
|
912
|
+
})
|
|
913
|
+
}
|
|
634
914
|
return {
|
|
635
915
|
columns: child.columns,
|
|
636
916
|
maxRows: child.maxRows,
|
|
@@ -689,6 +969,20 @@ function executeDistinct(plan, context) {
|
|
|
689
969
|
*/
|
|
690
970
|
function executeLimit(plan, context) {
|
|
691
971
|
const child = executePlan({ plan: plan.child, context })
|
|
972
|
+
if (child.batches) {
|
|
973
|
+
const readChildBatches = child.batches
|
|
974
|
+
/** @returns {AsyncIterable<AsyncBatch>} */
|
|
975
|
+
function makeBatches() {
|
|
976
|
+
return limitBatches(readChildBatches(), plan.limit, plan.offset, context.signal)
|
|
977
|
+
}
|
|
978
|
+
return batchResult({
|
|
979
|
+
columns: child.columns,
|
|
980
|
+
numRows: computeScanRows(child.numRows, plan.limit, plan.offset),
|
|
981
|
+
maxRows: computeScanRows(child.maxRows, plan.limit, plan.offset),
|
|
982
|
+
batches: makeBatches,
|
|
983
|
+
signal: context.signal,
|
|
984
|
+
})
|
|
985
|
+
}
|
|
692
986
|
return {
|
|
693
987
|
columns: child.columns,
|
|
694
988
|
numRows: computeScanRows(child.numRows, plan.limit, plan.offset),
|
|
@@ -697,6 +991,132 @@ function executeLimit(plan, context) {
|
|
|
697
991
|
}
|
|
698
992
|
}
|
|
699
993
|
|
|
994
|
+
/**
|
|
995
|
+
* Wraps one-column source chunks as loaded batches without copying their
|
|
996
|
+
* arrays. Each source chunk remains the async scheduling unit.
|
|
997
|
+
*
|
|
998
|
+
* @param {AsyncIterable<ArrayLike<SqlPrimitive>>} chunks
|
|
999
|
+
* @param {AbortSignal} [signal]
|
|
1000
|
+
* @yields {AsyncBatch}
|
|
1001
|
+
*/
|
|
1002
|
+
async function* columnBatches(chunks, signal) {
|
|
1003
|
+
for await (const chunk of chunks) {
|
|
1004
|
+
signal?.throwIfAborted()
|
|
1005
|
+
const vector = vectorFromChunk(chunk)
|
|
1006
|
+
/** @type {AsyncBatch} */
|
|
1007
|
+
const batch = {
|
|
1008
|
+
selection: { type: 'all', length: vector.length },
|
|
1009
|
+
columns: [vector],
|
|
1010
|
+
}
|
|
1011
|
+
yield batch
|
|
1012
|
+
}
|
|
1013
|
+
signal?.throwIfAborted()
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* @param {ArrayLike<SqlPrimitive>} chunk
|
|
1018
|
+
* @returns {ColumnVector}
|
|
1019
|
+
*/
|
|
1020
|
+
function vectorFromChunk(chunk) {
|
|
1021
|
+
if (Array.isArray(chunk)) {
|
|
1022
|
+
return { type: 'values', values: chunk, length: chunk.length }
|
|
1023
|
+
}
|
|
1024
|
+
if (isNumericArray(chunk)) {
|
|
1025
|
+
return {
|
|
1026
|
+
type: 'typed',
|
|
1027
|
+
values: chunk,
|
|
1028
|
+
length: chunk.length,
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return { type: 'values', values: Array.from(chunk), length: chunk.length }
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* @param {ArrayLike<SqlPrimitive>} values
|
|
1036
|
+
* @returns {values is import('../types.js').NumericArray}
|
|
1037
|
+
*/
|
|
1038
|
+
function isNumericArray(values) {
|
|
1039
|
+
return values instanceof Int8Array
|
|
1040
|
+
|| values instanceof Uint8Array
|
|
1041
|
+
|| values instanceof Uint8ClampedArray
|
|
1042
|
+
|| values instanceof Int16Array
|
|
1043
|
+
|| values instanceof Uint16Array
|
|
1044
|
+
|| values instanceof Int32Array
|
|
1045
|
+
|| values instanceof Uint32Array
|
|
1046
|
+
|| values instanceof Float32Array
|
|
1047
|
+
|| values instanceof Float64Array
|
|
1048
|
+
|| values instanceof BigInt64Array
|
|
1049
|
+
|| values instanceof BigUint64Array
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Compiles a projection to direct, constant, or computed batch columns.
|
|
1054
|
+
*
|
|
1055
|
+
* @param {SelectColumn[]} planColumns
|
|
1056
|
+
* @param {string[]} outputColumns
|
|
1057
|
+
* @param {string[]} childColumns
|
|
1058
|
+
* @param {ExecuteContext} context
|
|
1059
|
+
* @returns {BatchProjection[] | undefined}
|
|
1060
|
+
*/
|
|
1061
|
+
function batchProjection(planColumns, outputColumns, childColumns, context) {
|
|
1062
|
+
/** @type {BatchProjection[]} */
|
|
1063
|
+
const projections = []
|
|
1064
|
+
for (const column of planColumns) {
|
|
1065
|
+
if (column.type === 'star') {
|
|
1066
|
+
const prefix = column.table ? `${column.table}.` : undefined
|
|
1067
|
+
for (let i = 0; i < childColumns.length; i++) {
|
|
1068
|
+
if (!prefix || childColumns[i].startsWith(prefix)) {
|
|
1069
|
+
projections.push({ type: 'column', columnIndex: i })
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
continue
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (referencesRowScope(column.expr, childColumns, context)) return undefined
|
|
1076
|
+
|
|
1077
|
+
if (column.expr.type === 'literal') {
|
|
1078
|
+
projections.push({ type: 'constant', value: column.expr.value })
|
|
1079
|
+
continue
|
|
1080
|
+
}
|
|
1081
|
+
if (column.expr.type === 'identifier') {
|
|
1082
|
+
const index = identifierColumnIndex(column.expr, childColumns)
|
|
1083
|
+
if (index !== undefined) {
|
|
1084
|
+
projections.push({ type: 'column', columnIndex: index })
|
|
1085
|
+
continue
|
|
1086
|
+
}
|
|
1087
|
+
const expression = compileBatchExpression(column.expr, childColumns)
|
|
1088
|
+
if (!expression) return undefined
|
|
1089
|
+
projections.push({ type: 'expression', expression })
|
|
1090
|
+
continue
|
|
1091
|
+
}
|
|
1092
|
+
const expression = compileBatchExpression(column.expr, childColumns)
|
|
1093
|
+
if (!expression) return undefined
|
|
1094
|
+
projections.push({ type: 'expression', expression })
|
|
1095
|
+
}
|
|
1096
|
+
if (projections.length !== outputColumns.length) return undefined
|
|
1097
|
+
|
|
1098
|
+
return projections
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* @param {IdentifierNode} identifier
|
|
1103
|
+
* @param {string[]} childColumns
|
|
1104
|
+
* @returns {number | undefined}
|
|
1105
|
+
*/
|
|
1106
|
+
function identifierColumnIndex(identifier, childColumns) {
|
|
1107
|
+
const sourceName = identifier.prefix
|
|
1108
|
+
? `${identifier.prefix}.${identifier.name}`
|
|
1109
|
+
: identifier.name
|
|
1110
|
+
const index = childColumns.lastIndexOf(sourceName)
|
|
1111
|
+
if (index >= 0) return index
|
|
1112
|
+
|
|
1113
|
+
const suffix = `.${identifier.name}`
|
|
1114
|
+
const matches = childColumns
|
|
1115
|
+
.map(function childColumn(name, childIndex) { return { name, childIndex } })
|
|
1116
|
+
.filter(function suffixMatch(candidate) { return candidate.name.endsWith(suffix) })
|
|
1117
|
+
return matches.length === 1 ? matches[0].childIndex : undefined
|
|
1118
|
+
}
|
|
1119
|
+
|
|
700
1120
|
/**
|
|
701
1121
|
* Executes a set operation (UNION, INTERSECT, EXCEPT)
|
|
702
1122
|
*
|
|
@@ -706,11 +1126,13 @@ function executeLimit(plan, context) {
|
|
|
706
1126
|
*/
|
|
707
1127
|
function executeSetOperation(plan, context) {
|
|
708
1128
|
const { signal } = context
|
|
1129
|
+
const leftContext = plan.leftScope === undefined ? context : { ...context, scope: plan.leftScope }
|
|
1130
|
+
const rightContext = plan.rightScope === undefined ? context : { ...context, scope: plan.rightScope }
|
|
1131
|
+
const left = executePlan({ plan: plan.left, context: leftContext })
|
|
1132
|
+
const right = executePlan({ plan: plan.right, context: rightContext })
|
|
709
1133
|
|
|
710
1134
|
if (plan.operator === 'UNION') {
|
|
711
1135
|
if (plan.all) {
|
|
712
|
-
const left = executePlan({ plan: plan.left, context })
|
|
713
|
-
const right = executePlan({ plan: plan.right, context })
|
|
714
1136
|
return {
|
|
715
1137
|
columns: left.columns,
|
|
716
1138
|
numRows: addBounds(left.numRows, right.numRows),
|
|
@@ -722,8 +1144,6 @@ function executeSetOperation(plan, context) {
|
|
|
722
1144
|
},
|
|
723
1145
|
}
|
|
724
1146
|
} else {
|
|
725
|
-
const left = executePlan({ plan: plan.left, context })
|
|
726
|
-
const right = executePlan({ plan: plan.right, context })
|
|
727
1147
|
return {
|
|
728
1148
|
columns: left.columns,
|
|
729
1149
|
maxRows: addBounds(left.maxRows, right.maxRows),
|
|
@@ -759,8 +1179,6 @@ function executeSetOperation(plan, context) {
|
|
|
759
1179
|
}
|
|
760
1180
|
}
|
|
761
1181
|
} else if (plan.operator === 'INTERSECT') {
|
|
762
|
-
const left = executePlan({ plan: plan.left, context })
|
|
763
|
-
const right = executePlan({ plan: plan.right, context })
|
|
764
1182
|
return {
|
|
765
1183
|
columns: left.columns,
|
|
766
1184
|
maxRows: minBounds(left.maxRows, right.maxRows),
|
|
@@ -814,8 +1232,6 @@ function executeSetOperation(plan, context) {
|
|
|
814
1232
|
}
|
|
815
1233
|
} else {
|
|
816
1234
|
// EXCEPT
|
|
817
|
-
const left = executePlan({ plan: plan.left, context })
|
|
818
|
-
const right = executePlan({ plan: plan.right, context })
|
|
819
1235
|
return {
|
|
820
1236
|
columns: left.columns,
|
|
821
1237
|
maxRows: left.maxRows,
|