squirreling 0.12.26 → 0.13.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
@@ -4,7 +4,7 @@ import { findAggregate } from '../validation/aggregates.js'
4
4
  import { ParseError } from '../validation/parseErrors.js'
5
5
  import { ColumnNotFoundError, TableNotFoundError } from '../validation/tables.js'
6
6
  import { validateNoIdentifiers, validateScan, validateTableRefs } from '../validation/tables.js'
7
- import { collectScopeColumns, extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, statementScope, tableFunctionColumnNames } from './columns.js'
7
+ import { collectColumnsFromExpr, collectScopeColumns, extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, statementScope, tableFunctionColumnNames } from './columns.js'
8
8
 
9
9
  /**
10
10
  * @import { AsyncDataSource, DerivedColumn, ExprNode, FromFunction, IdentifierNode, JoinClause, OrderByItem, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement, WindowFunctionNode } from '../types.js'
@@ -208,6 +208,10 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
208
208
  const hints = {}
209
209
  const perTableColumns = extractColumns({ select: originalSelect, parentColumns })
210
210
  if (sourceAlias !== undefined) hints.columns = perTableColumns.get(sourceAlias)
211
+ // Capture what the parent reads from a FROM subquery before the reset
212
+ // below, so aggregate outputs it never reads can still be pruned when the
213
+ // parent reads nothing at all (an empty array here means exactly that).
214
+ const subqueryNeeds = select.from?.type === 'subquery' ? hints.columns : undefined
211
215
  // Empty columns array means no columns were referenced, but a FROM subquery
212
216
  // still needs its own columns (e.g. for DISTINCT). Treat empty as unrestricted.
213
217
  if (hints.columns?.length === 0 && select.from?.type === 'subquery') {
@@ -224,6 +228,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
224
228
  // Start with the data source (FROM clause)
225
229
  /** @type {QueryPlan} */
226
230
  let plan = planFrom({ select, ctePlans, cteColumns, hints, tables, outerScope })
231
+ pruneAggregateColumns(plan, subqueryNeeds)
227
232
 
228
233
  // Add JOINs
229
234
  if (select.joins.length) {
@@ -349,6 +354,38 @@ function pushLimitIntoSort(plan, limit, offset) {
349
354
  }
350
355
  }
351
356
 
357
+ /**
358
+ * Drops derived columns from a subquery's aggregate node when the parent
359
+ * query never reads them. The buffered path defers aggregate cells so unread
360
+ * outputs are never evaluated, but the streaming path accumulates every
361
+ * planned aggregate eagerly, so unread aggregates must be removed at plan
362
+ * time to preserve that lazy behavior. Columns referenced by HAVING or the
363
+ * aggregate's ORDER BY are kept: both evaluate against the group context
364
+ * row, which exposes output aliases. Descends only through Subquery and
365
+ * Limit wrappers; Sort and Distinct depend on the full column set, so
366
+ * anything below them is left untouched.
367
+ *
368
+ * @param {QueryPlan} plan
369
+ * @param {string[] | undefined} needed - output column names the parent reads
370
+ */
371
+ function pruneAggregateColumns(plan, needed) {
372
+ if (!needed) return
373
+ let node = plan
374
+ while (node.type === 'Subquery' || node.type === 'Limit') node = node.child
375
+ if (node.type !== 'HashAggregate' && node.type !== 'ScalarAggregate') return
376
+ /** @type {IdentifierNode[]} */
377
+ const identifiers = []
378
+ collectColumnsFromExpr(node.having, identifiers)
379
+ if (node.type === 'HashAggregate' && node.orderBy) {
380
+ for (const term of node.orderBy) collectColumnsFromExpr(term.expr, identifiers)
381
+ }
382
+ const keep = new Set(needed)
383
+ for (const { name } of identifiers) keep.add(name)
384
+ node.columns = node.columns.filter(col =>
385
+ col.type === 'star' || keep.has(col.alias ?? derivedAlias(col.expr))
386
+ )
387
+ }
388
+
352
389
  /**
353
390
  * @param {object} options
354
391
  * @param {SelectStatement} options.select
@@ -487,6 +524,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
487
524
  outerScope,
488
525
  parentColumns: subColumns?.map(name => ({ type: 'identifier', name, positionStart: 0, positionEnd: 0 })),
489
526
  })
527
+ pruneAggregateColumns(subPlan, perTableColumns.get(rightTable))
490
528
  const availableColumns = inferStatementColumns({ stmt: join.subquery.query, cteColumns, tables })
491
529
  if (subColumns && availableColumns.length) {
492
530
  const missingColumn = subColumns.find(col => !availableColumns.includes(col))