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,4 +1,6 @@
1
+ import { selectedRowCount, valueAt } from '../backend/batch.js'
1
2
  import { derivedAlias } from '../expression/alias.js'
3
+ import { compileBatchExpression } from '../expression/batch.js'
2
4
  import { evaluateAll, evaluateExpr } from '../expression/evaluate.js'
3
5
  import { collectColumnsFromExpr } from '../plan/columns.js'
4
6
  import { isAggregateFunc } from '../validation/functions.js'
@@ -8,7 +10,8 @@ import { keyify } from './utils.js'
8
10
  import { yieldToEventLoop } from './yield.js'
9
11
 
10
12
  /**
11
- * @import { AsyncCells, AsyncRow, ExecuteContext, ExprNode, FunctionNode, IdentifierNode, QueryResults, SelectColumn, SqlPrimitive } from '../types.js'
13
+ * @import { BatchAggregateInputs, CompiledBatchExpression } from '../internalTypes.js'
14
+ * @import { AsyncBatch, AsyncCells, AsyncRow, ColumnVector, ExecuteContext, ExprNode, FunctionNode, IdentifierNode, QueryResults, SelectColumn, SqlPrimitive } from '../types.js'
12
15
  * @import { HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
13
16
  * @import { Accumulator } from './accumulator.js'
14
17
  */
@@ -419,6 +422,141 @@ async function accumulateChunk({ chunk, groupBy, specs, groups, needsRow, contex
419
422
  }
420
423
  }
421
424
 
425
+ /**
426
+ * Compiles the expressions an aggregate reads from each input batch. A
427
+ * filtered non-star aggregate retains the row path because its argument must
428
+ * only be evaluated for passing rows.
429
+ *
430
+ * @param {ExprNode[]} groupBy
431
+ * @param {StreamingAggSpec[]} specs
432
+ * @param {readonly string[]} columns
433
+ * @param {ExecuteContext} context
434
+ * @returns {BatchAggregateInputs | undefined}
435
+ */
436
+ function compileBatchAggregateInputs(groupBy, specs, columns, context) {
437
+ /** @type {CompiledBatchExpression[]} */
438
+ const keys = []
439
+ for (const expression of groupBy) {
440
+ if (referencesRowScope(expression, context)) return undefined
441
+ const key = compileBatchExpression(expression, columns)
442
+ if (!key) return undefined
443
+ keys.push(key)
444
+ }
445
+
446
+ /** @type {(CompiledBatchExpression | undefined)[]} */
447
+ const filters = []
448
+ /** @type {(CompiledBatchExpression | undefined)[]} */
449
+ const args = []
450
+ for (const spec of specs) {
451
+ if (spec.node.filter && !spec.star) return undefined
452
+ if (spec.node.filter && referencesRowScope(spec.node.filter, context)) return undefined
453
+ if (!spec.star && referencesRowScope(spec.node.args[0], context)) return undefined
454
+ const filter = spec.node.filter
455
+ ? compileBatchExpression(spec.node.filter, columns)
456
+ : undefined
457
+ const argument = spec.star
458
+ ? undefined
459
+ : compileBatchExpression(spec.node.args[0], columns)
460
+ if (spec.node.filter && !filter || !spec.star && !argument) return undefined
461
+ filters.push(filter)
462
+ args.push(argument)
463
+ }
464
+ return {
465
+ keys,
466
+ filters,
467
+ args,
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Returns whether an expression reads a qualified identifier whose table
473
+ * scope the batch compiler cannot distinguish from struct-field access.
474
+ *
475
+ * @param {ExprNode} expression
476
+ * @param {ExecuteContext} context
477
+ * @returns {boolean}
478
+ */
479
+ function referencesRowScope(expression, context) {
480
+ /** @type {IdentifierNode[]} */
481
+ const identifiers = []
482
+ collectColumnsFromExpr(expression, identifiers)
483
+ return identifiers.some(function isScopedReference(identifier) {
484
+ return Boolean(identifier.prefix && (
485
+ context.scope?.includes(identifier.prefix) || context.outerAliases?.has(identifier.prefix)
486
+ ))
487
+ })
488
+ }
489
+
490
+ /**
491
+ * Resolves one set of compiled expressions against a batch.
492
+ *
493
+ * @param {(CompiledBatchExpression | undefined)[]} expressions
494
+ * @param {AsyncBatch} batch
495
+ * @param {ExecuteContext} context
496
+ * @param {number} rowOffset
497
+ * @returns {Promise<(ColumnVector | undefined)[]>}
498
+ */
499
+ function evaluateBatchInputs(expressions, batch, context, rowOffset) {
500
+ return Promise.all(expressions.map(function evaluateInput(expression) {
501
+ return expression?.evaluate({ batch, selection: batch.selection, signal: context.signal, rowOffset })
502
+ }))
503
+ }
504
+
505
+ /**
506
+ * Folds a native batch directly into aggregate state without constructing
507
+ * rows, cells, or per-value promises.
508
+ *
509
+ * @param {object} options
510
+ * @param {AsyncBatch} options.batch
511
+ * @param {BatchAggregateInputs} options.inputs
512
+ * @param {StreamingAggSpec[]} options.specs
513
+ * @param {Map<unknown, StreamingGroup>} options.groups
514
+ * @param {ExecuteContext} options.context
515
+ * @param {number} options.rowOffset
516
+ * @returns {Promise<void>}
517
+ */
518
+ async function accumulateBatch({ batch, inputs, specs, groups, context, rowOffset }) {
519
+ const [keys, filters, args] = await Promise.all([
520
+ evaluateBatchInputs(inputs.keys, batch, context, rowOffset),
521
+ evaluateBatchInputs(inputs.filters, batch, context, rowOffset),
522
+ evaluateBatchInputs(inputs.args, batch, context, rowOffset),
523
+ ])
524
+ const rowCount = selectedRowCount(batch.selection)
525
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
526
+ if (rowIndex > 0 && rowIndex % CHUNK_SIZE === 0) {
527
+ await yieldToEventLoop()
528
+ context.signal?.throwIfAborted()
529
+ }
530
+ const keyValues = keys.map(function keyValue(vector) {
531
+ if (!vector) throw new Error('Missing compiled group key')
532
+ return valueAt(vector, rowIndex)
533
+ })
534
+ const key = keyValues.length === 0
535
+ ? true
536
+ : keyValues.length === 1 ? keyify(keyValues[0]) : keyify(...keyValues)
537
+ let group = groups.get(key)
538
+ if (!group) {
539
+ group = {
540
+ firstRow: undefined,
541
+ keyValues,
542
+ accumulators: specs.map(spec => newAccumulator(spec.funcName, spec.node.distinct)),
543
+ }
544
+ groups.set(key, group)
545
+ }
546
+ for (let specIndex = 0; specIndex < specs.length; specIndex++) {
547
+ const filter = filters[specIndex]
548
+ if (filter && !valueAt(filter, rowIndex)) continue
549
+ const spec = specs[specIndex]
550
+ if (spec.star && spec.funcName === 'COUNT') {
551
+ group.accumulators[specIndex].count++
552
+ } else {
553
+ const argument = args[specIndex]
554
+ updateAccumulator(spec.funcName, group.accumulators[specIndex], argument ? valueAt(argument, rowIndex) : null)
555
+ }
556
+ }
557
+ }
558
+ }
559
+
422
560
  /**
423
561
  * Consumes the child rows into per-group accumulators, holding at most one
424
562
  * chunk of rows at a time. Throws when aborted so partial accumulators are
@@ -435,6 +573,18 @@ async function accumulateChunk({ chunk, groupBy, specs, groups, needsRow, contex
435
573
  async function accumulateGroups({ child, groupBy, specs, needsRow, context }) {
436
574
  /** @type {Map<unknown, StreamingGroup>} */
437
575
  const groups = new Map()
576
+ const batchInputs = child.batches && !needsRow
577
+ ? compileBatchAggregateInputs(groupBy, specs, child.columns, context)
578
+ : undefined
579
+ if (batchInputs && child.batches) {
580
+ let rowOffset = 0
581
+ for await (const batch of child.batches()) {
582
+ await accumulateBatch({ batch, inputs: batchInputs, specs, groups, context, rowOffset })
583
+ rowOffset += selectedRowCount(batch.selection)
584
+ context.signal?.throwIfAborted()
585
+ }
586
+ return groups
587
+ }
438
588
  /** @type {AsyncRow[]} */
439
589
  let chunk = []
440
590
  for await (const row of child.rows()) {
@@ -1,9 +1,42 @@
1
+ import { collectBatches } from '../backend/batchAdapters.js'
2
+
1
3
  /**
2
4
  * @import { AsyncRow, OrderByItem, QueryResults, SqlPrimitive } from '../types.js'
3
5
  */
4
6
 
5
7
  const primitiveTypes = new Set(['number', 'bigint', 'boolean', 'string'])
6
8
 
9
+ /** @type {WeakMap<QueryResults, AbortSignal>} */
10
+ const querySignals = new WeakMap()
11
+
12
+ /**
13
+ * Associates an execution signal with results without expanding the public
14
+ * result shape solely for the collection adapter.
15
+ *
16
+ * @param {QueryResults} results
17
+ * @param {AbortSignal} [signal]
18
+ * @returns {QueryResults}
19
+ */
20
+ export function bindQuerySignal(results, signal) {
21
+ if (!signal || querySignals.get(results) === signal) return results
22
+
23
+ const { batches, rows } = results
24
+ results.rows = async function* boundRows() {
25
+ signal.throwIfAborted()
26
+ yield* rows.call(results)
27
+ signal.throwIfAborted()
28
+ }
29
+ if (batches) {
30
+ results.batches = async function* boundBatches() {
31
+ signal.throwIfAborted()
32
+ yield* batches.call(results)
33
+ signal.throwIfAborted()
34
+ }
35
+ }
36
+ querySignals.set(results, signal)
37
+ return results
38
+ }
39
+
7
40
  /**
8
41
  * Compares two values for a single ORDER BY term, handling nulls and direction
9
42
  *
@@ -49,6 +82,10 @@ export function compareForTerm(a, b, term) {
49
82
  * @returns {Promise<Record<string, SqlPrimitive>[]>} array of all yielded values
50
83
  */
51
84
  export async function collect(results) {
85
+ if (results.batches) {
86
+ return await collectBatches(results.batches(), results.columns, querySignals.get(results))
87
+ }
88
+
52
89
  // Collect all rows first, then materialize cells concurrently
53
90
  // This enables dataloader-style batching of cell accessors
54
91
  /** @type {AsyncRow[]} */