squirreling 0.12.3 → 0.12.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -43,7 +43,7 @@
43
43
  "@vitest/coverage-v8": "4.1.4",
44
44
  "eslint": "9.39.2",
45
45
  "eslint-plugin-jsdoc": "62.9.0",
46
- "typescript": "6.0.2",
46
+ "typescript": "6.0.3",
47
47
  "vitest": "4.1.4"
48
48
  }
49
49
  }
@@ -1,3 +1,4 @@
1
+ import { derivedAlias } from '../expression/alias.js'
1
2
  import { expectNoAggregate, findAggregate } from '../validation/aggregates.js'
2
3
  import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE } from '../validation/keywords.js'
3
4
  import { ParseError } from '../validation/parseErrors.js'
@@ -237,7 +238,7 @@ function parseSelect(state) {
237
238
  if (match(state, 'keyword', 'GROUP')) {
238
239
  expect(state, 'keyword', 'BY')
239
240
  while (true) {
240
- const expr = parseExpression(state)
241
+ const expr = resolvePositionalRef(parseExpression(state), columns, 'GROUP BY')
241
242
  expectNoAggregate(expr, 'GROUP BY')
242
243
  groupBy.push(expr)
243
244
  if (!match(state, 'comma')) break
@@ -255,7 +256,7 @@ function parseSelect(state) {
255
256
  if (match(state, 'keyword', 'ORDER')) {
256
257
  expect(state, 'keyword', 'BY')
257
258
  while (true) {
258
- const expr = parseExpression(state)
259
+ const expr = resolvePositionalRef(parseExpression(state), columns, 'ORDER BY', hasAggregate)
259
260
  if (!hasAggregate) {
260
261
  expectNoAggregate(expr, 'ORDER BY')
261
262
  }
@@ -333,6 +334,53 @@ function parseSelect(state) {
333
334
  }
334
335
  }
335
336
 
337
+ /**
338
+ * Resolves a positive integer literal in GROUP BY or ORDER BY as a positional
339
+ * reference to the Nth SELECT column. Non-integer and non-literal expressions
340
+ * pass through unchanged.
341
+ *
342
+ * In post-aggregation contexts (ORDER BY when the query aggregates), the
343
+ * reference resolves to an identifier on the output column name, so it
344
+ * matches columns produced by the aggregate projection. In pre-projection
345
+ * contexts (GROUP BY, non-aggregated ORDER BY), the target column's full
346
+ * expression is substituted, since the output name won't exist yet.
347
+ *
348
+ * @param {ExprNode} expr
349
+ * @param {SelectColumn[]} columns
350
+ * @param {string} clauseName
351
+ * @param {boolean} [postAgg]
352
+ * @returns {ExprNode}
353
+ */
354
+ function resolvePositionalRef(expr, columns, clauseName, postAgg) {
355
+ if (expr.type !== 'literal') return expr
356
+ const n = expr.value
357
+ if (typeof n !== 'number' || !Number.isInteger(n) || n <= 0) return expr
358
+ if (n > columns.length) {
359
+ throw new ParseError({
360
+ message: `${clauseName} position ${n} is out of range (expected 1..${columns.length}) at position ${expr.positionStart}`,
361
+ positionStart: expr.positionStart,
362
+ positionEnd: expr.positionEnd,
363
+ })
364
+ }
365
+ const col = columns[n - 1]
366
+ if (col.type === 'star') {
367
+ throw new ParseError({
368
+ message: `${clauseName} position ${n} refers to * which is not supported at position ${expr.positionStart}`,
369
+ positionStart: expr.positionStart,
370
+ positionEnd: expr.positionEnd,
371
+ })
372
+ }
373
+ if (postAgg) {
374
+ return {
375
+ type: 'identifier',
376
+ name: col.alias ?? derivedAlias(col.expr),
377
+ positionStart: expr.positionStart,
378
+ positionEnd: expr.positionEnd,
379
+ }
380
+ }
381
+ return col.expr
382
+ }
383
+
336
384
  /**
337
385
  * @param {ParserState} state
338
386
  * @returns {SelectColumn[]}
@@ -54,8 +54,19 @@ export function extractColumns({ select, parentColumns }) {
54
54
  // directly. For non-star queries, parent names may be aliases and are
55
55
  // handled below by filtering derived columns and collecting from expressions.
56
56
  const hasStar = select.columns.some(col => col.type === 'star' && !col.table)
57
+ // Exclude parent names that match a derived alias in this SELECT — those are
58
+ // produced by projection (e.g. `SELECT *, a+b AS c`), not by the source.
59
+ /** @type {Set<string>} */
60
+ const derivedAliases = new Set()
61
+ for (const col of select.columns) {
62
+ if (col.type === 'derived') {
63
+ derivedAliases.add(col.alias ?? derivedAlias(col.expr))
64
+ }
65
+ }
57
66
  /** @type {IdentifierNode[]} */
58
- const identifiers = hasStar && parentColumns ? [...parentColumns] : []
67
+ const identifiers = hasStar && parentColumns
68
+ ? parentColumns.filter(id => !derivedAliases.has(id.name))
69
+ : []
59
70
 
60
71
  // Collect ORDER BY identifiers, excluding SELECT aliases (their underlying
61
72
  // columns are already collected from select.columns expressions above)
package/src/plan/plan.js CHANGED
@@ -48,7 +48,7 @@ export function planStatement({ stmt, ctePlans, cteColumns, tables, parentColumn
48
48
  return planStatement({ stmt: stmt.query, ctePlans, cteColumns, tables, parentColumns, outerScope })
49
49
  }
50
50
  if (stmt.type === 'compound') {
51
- return planSetOperation({ compound: stmt, ctePlans, cteColumns, tables })
51
+ return planSetOperation({ compound: stmt, ctePlans, cteColumns, tables, parentColumns })
52
52
  }
53
53
  return planSelect({ select: stmt, ctePlans, cteColumns, tables, parentColumns, outerScope })
54
54
  }
@@ -61,11 +61,12 @@ export function planStatement({ stmt, ctePlans, cteColumns, tables, parentColumn
61
61
  * @param {Map<string, QueryPlan>} [options.ctePlans]
62
62
  * @param {Map<string, string[]>} [options.cteColumns]
63
63
  * @param {Record<string, AsyncDataSource>} [options.tables]
64
+ * @param {IdentifierNode[]} [options.parentColumns] - columns needed by the parent query
64
65
  * @returns {QueryPlan}
65
66
  */
66
- function planSetOperation({ compound, ctePlans, cteColumns, tables }) {
67
- const left = planStatement({ stmt: compound.left, ctePlans, cteColumns, tables })
68
- const right = planStatement({ stmt: compound.right, ctePlans, cteColumns, tables })
67
+ function planSetOperation({ compound, ctePlans, cteColumns, tables, parentColumns }) {
68
+ const left = planStatement({ stmt: compound.left, ctePlans, cteColumns, tables, parentColumns })
69
+ const right = planStatement({ stmt: compound.right, ctePlans, cteColumns, tables, parentColumns })
69
70
  const leftColumns = inferStatementColumns({ stmt: compound.left, cteColumns, tables })
70
71
  const rightColumns = inferStatementColumns({ stmt: compound.right, cteColumns, tables })
71
72