squirreling 0.12.4 → 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 +2 -2
- package/src/parse/parse.js +50 -2
- package/src/plan/columns.js +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.12.
|
|
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.
|
|
46
|
+
"typescript": "6.0.3",
|
|
47
47
|
"vitest": "4.1.4"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/parse/parse.js
CHANGED
|
@@ -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[]}
|
package/src/plan/columns.js
CHANGED
|
@@ -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
|
|
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)
|