squirreling 0.12.4 → 0.12.6

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 { derivedAlias } from '../expression/alias.js'
1
2
  import { expectNoAggregate, findAggregate } from '../validation/aggregates.js'
3
+ import { isTableFunction, validateFunctionArgs } from '../validation/functions.js'
2
4
  import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE } from '../validation/keywords.js'
3
5
  import { ParseError } from '../validation/parseErrors.js'
4
6
  import { parseExpression } from './expression.js'
@@ -7,7 +9,7 @@ import { consume, current, expect, match, parseError, peekToken } from './state.
7
9
  import { tokenizeSql } from './tokenize.js'
8
10
 
9
11
  /**
10
- * @import { CTEDefinition, ExprNode, FromSubquery, FromTable, OrderByItem, ParseSqlOptions, ParserState, SelectColumn, SelectStatement, SetOperationStatement, SetOperator, Statement } from '../types.js'
12
+ * @import { CTEDefinition, ExprNode, FromFunction, FromSubquery, FromTable, OrderByItem, ParseSqlOptions, ParserState, SelectColumn, SelectStatement, SetOperationStatement, SetOperator, Statement } from '../types.js'
11
13
  */
12
14
 
13
15
  /**
@@ -183,8 +185,8 @@ function parseSelect(state) {
183
185
  expect(state, 'keyword', 'FROM')
184
186
  }
185
187
 
186
- // Check if it's a subquery or table name
187
- /** @type {FromTable | FromSubquery} */
188
+ // Check if it's a subquery, table function, or table name
189
+ /** @type {FromTable | FromSubquery | FromFunction} */
188
190
  let from
189
191
  const fromTok = current(state)
190
192
  if (fromTok.type === 'paren' && fromTok.value === '(') {
@@ -200,6 +202,9 @@ function parseSelect(state) {
200
202
  positionStart: fromTok.positionStart,
201
203
  positionEnd: state.lastPos,
202
204
  }
205
+ } else if (isTableFunctionStart(state)) {
206
+ // Table function: SELECT * FROM UNNEST(expr) [AS alias[(col_alias)]]
207
+ from = parseFromFunction(state)
203
208
  } else {
204
209
  // Simple table name: SELECT * FROM users
205
210
  expect(state, 'identifier')
@@ -237,7 +242,7 @@ function parseSelect(state) {
237
242
  if (match(state, 'keyword', 'GROUP')) {
238
243
  expect(state, 'keyword', 'BY')
239
244
  while (true) {
240
- const expr = parseExpression(state)
245
+ const expr = resolvePositionalRef(parseExpression(state), columns, 'GROUP BY')
241
246
  expectNoAggregate(expr, 'GROUP BY')
242
247
  groupBy.push(expr)
243
248
  if (!match(state, 'comma')) break
@@ -255,7 +260,7 @@ function parseSelect(state) {
255
260
  if (match(state, 'keyword', 'ORDER')) {
256
261
  expect(state, 'keyword', 'BY')
257
262
  while (true) {
258
- const expr = parseExpression(state)
263
+ const expr = resolvePositionalRef(parseExpression(state), columns, 'ORDER BY', hasAggregate)
259
264
  if (!hasAggregate) {
260
265
  expectNoAggregate(expr, 'ORDER BY')
261
266
  }
@@ -333,6 +338,53 @@ function parseSelect(state) {
333
338
  }
334
339
  }
335
340
 
341
+ /**
342
+ * Resolves a positive integer literal in GROUP BY or ORDER BY as a positional
343
+ * reference to the Nth SELECT column. Non-integer and non-literal expressions
344
+ * pass through unchanged.
345
+ *
346
+ * In post-aggregation contexts (ORDER BY when the query aggregates), the
347
+ * reference resolves to an identifier on the output column name, so it
348
+ * matches columns produced by the aggregate projection. In pre-projection
349
+ * contexts (GROUP BY, non-aggregated ORDER BY), the target column's full
350
+ * expression is substituted, since the output name won't exist yet.
351
+ *
352
+ * @param {ExprNode} expr
353
+ * @param {SelectColumn[]} columns
354
+ * @param {string} clauseName
355
+ * @param {boolean} [postAgg]
356
+ * @returns {ExprNode}
357
+ */
358
+ function resolvePositionalRef(expr, columns, clauseName, postAgg) {
359
+ if (expr.type !== 'literal') return expr
360
+ const n = expr.value
361
+ if (typeof n !== 'number' || !Number.isInteger(n) || n <= 0) return expr
362
+ if (n > columns.length) {
363
+ throw new ParseError({
364
+ message: `${clauseName} position ${n} is out of range (expected 1..${columns.length}) at position ${expr.positionStart}`,
365
+ positionStart: expr.positionStart,
366
+ positionEnd: expr.positionEnd,
367
+ })
368
+ }
369
+ const col = columns[n - 1]
370
+ if (col.type === 'star') {
371
+ throw new ParseError({
372
+ message: `${clauseName} position ${n} refers to * which is not supported at position ${expr.positionStart}`,
373
+ positionStart: expr.positionStart,
374
+ positionEnd: expr.positionEnd,
375
+ })
376
+ }
377
+ if (postAgg) {
378
+ return {
379
+ type: 'identifier',
380
+ name: col.alias ?? derivedAlias(col.expr),
381
+ positionStart: expr.positionStart,
382
+ positionEnd: expr.positionEnd,
383
+ }
384
+ }
385
+ return col.expr
386
+ }
387
+
336
388
  /**
337
389
  * @param {ParserState} state
338
390
  * @returns {SelectColumn[]}
@@ -376,6 +428,73 @@ function parseSelectList(state) {
376
428
  return cols
377
429
  }
378
430
 
431
+ /**
432
+ * Peeks whether the current token starts a table-valued function call
433
+ * like `UNNEST(...)`.
434
+ *
435
+ * @param {ParserState} state
436
+ * @returns {boolean}
437
+ */
438
+ export function isTableFunctionStart(state) {
439
+ const tok = current(state)
440
+ if (tok.type !== 'identifier') return false
441
+ if (!isTableFunction(tok.value.toUpperCase())) return false
442
+ const next = peekToken(state, 1)
443
+ return next.type === 'paren' && next.value === '('
444
+ }
445
+
446
+ /**
447
+ * Parses a table function source: UNNEST(args...) [AS alias[(col_alias)]]
448
+ *
449
+ * @param {ParserState} state
450
+ * @returns {FromFunction}
451
+ */
452
+ export function parseFromFunction(state) {
453
+ const funcTok = consume(state)
454
+ const funcName = funcTok.value.toUpperCase()
455
+ const { positionStart } = funcTok
456
+
457
+ expect(state, 'paren', '(')
458
+ /** @type {ExprNode[]} */
459
+ const args = []
460
+ if (!match(state, 'paren', ')')) {
461
+ while (true) {
462
+ args.push(parseExpression(state))
463
+ if (!match(state, 'comma')) break
464
+ }
465
+ expect(state, 'paren', ')')
466
+ }
467
+
468
+ validateFunctionArgs(funcName, args.length, positionStart, state.lastPos, state.functions)
469
+
470
+ const alias = parseTableAlias(state)
471
+ /** @type {string | undefined} */
472
+ let columnAlias
473
+ if (alias && match(state, 'paren', '(')) {
474
+ const colStart = state.lastPos
475
+ const colTok = expect(state, 'identifier')
476
+ columnAlias = colTok.value
477
+ if (match(state, 'comma')) {
478
+ throw new ParseError({
479
+ message: `${funcName} produces a single column; only one column alias is allowed`,
480
+ positionStart: colStart,
481
+ positionEnd: state.lastPos,
482
+ })
483
+ }
484
+ expect(state, 'paren', ')')
485
+ }
486
+
487
+ return {
488
+ type: 'function',
489
+ funcName,
490
+ args,
491
+ alias,
492
+ columnAlias,
493
+ positionStart,
494
+ positionEnd: state.lastPos,
495
+ }
496
+ }
497
+
379
498
  /**
380
499
  * Parses an optional table alias (e.g., "FROM users u" or "FROM users AS u")
381
500
  * @param {ParserState} state
@@ -1,4 +1,4 @@
1
- import { isCastType, isExtractField, isIntervalUnit, isKnownFunction, niladicFuncs } from '../validation/functions.js'
1
+ import { isCastType, isExtractField, isIntervalUnit, isKnownFunction, isTableFunction, niladicFuncs } from '../validation/functions.js'
2
2
  import { InvalidLiteralError, ParseError, SyntaxError, UnknownFunctionError } from '../validation/parseErrors.js'
3
3
  import { RESERVED_KEYWORDS } from '../validation/keywords.js'
4
4
  import { parseExpression } from './expression.js'
@@ -7,7 +7,7 @@ import { parseStatement } from './parse.js'
7
7
  import { consume, current, expect, match, parseError, peekToken } from './state.js'
8
8
 
9
9
  /**
10
- * @import { ExprNode, IntervalNode, ParserState, WhenClause } from '../types.js'
10
+ * @import { ExprNode, IntervalNode, ParserState, SqlPrimitive, WhenClause } from '../types.js'
11
11
  */
12
12
 
13
13
  /**
@@ -40,6 +40,34 @@ export function parsePrimary(state) {
40
40
  return expr
41
41
  }
42
42
 
43
+ // Array literal: [elem, elem, ...] — elements must be literals
44
+ if (match(state, 'bracket', '[')) {
45
+ /** @type {SqlPrimitive[]} */
46
+ const values = []
47
+ if (!match(state, 'bracket', ']')) {
48
+ while (true) {
49
+ const elemStart = current(state).positionStart
50
+ const elem = parseExpression(state)
51
+ if (elem.type !== 'literal') {
52
+ throw new ParseError({
53
+ message: 'Array literal elements must be constant literals',
54
+ positionStart: elemStart,
55
+ positionEnd: state.lastPos,
56
+ })
57
+ }
58
+ values.push(elem.value)
59
+ if (!match(state, 'comma')) break
60
+ }
61
+ expect(state, 'bracket', ']')
62
+ }
63
+ return {
64
+ type: 'literal',
65
+ value: values,
66
+ positionStart,
67
+ positionEnd: state.lastPos,
68
+ }
69
+ }
70
+
43
71
  if (tok.type === 'identifier') {
44
72
  const next = peekToken(state, 1)
45
73
  const funcNameUpper = tok.value.toUpperCase()
@@ -109,6 +137,14 @@ export function parsePrimary(state) {
109
137
  })
110
138
  }
111
139
 
140
+ if (isTableFunction(funcNameUpper)) {
141
+ throw new ParseError({
142
+ message: `${funcNameUpper} is a table function and can only be used in FROM clauses at position ${positionStart}`,
143
+ positionStart,
144
+ positionEnd: tok.positionEnd,
145
+ })
146
+ }
147
+
112
148
  return parseFunctionCall(state, positionStart)
113
149
  }
114
150
 
@@ -222,6 +222,17 @@ export function tokenizeSql(query) {
222
222
  continue
223
223
  }
224
224
 
225
+ if (ch === '[' || ch === ']') {
226
+ i++
227
+ tokens.push({
228
+ type: 'bracket',
229
+ value: ch,
230
+ positionStart,
231
+ positionEnd: i,
232
+ })
233
+ continue
234
+ }
235
+
225
236
  if (ch === ';') {
226
237
  i++
227
238
  tokens.push({
@@ -17,6 +17,7 @@ export type TokenType =
17
17
  | 'comma'
18
18
  | 'dot'
19
19
  | 'paren'
20
+ | 'bracket'
20
21
  | 'semicolon'
21
22
  | 'eof'
22
23
 
@@ -1,15 +1,30 @@
1
1
  import { derivedAlias } from '../expression/alias.js'
2
2
 
3
3
  /**
4
- * @import { AsyncDataSource, ExprNode, FromSubquery, FromTable, IdentifierNode, SelectStatement, Statement } from '../types.js'
4
+ * @import { AsyncDataSource, ExprNode, FromFunction, FromSubquery, FromTable, IdentifierNode, SelectStatement, Statement } from '../types.js'
5
5
  */
6
6
 
7
7
  /**
8
- * @param {FromTable | FromSubquery} from
8
+ * @param {FromTable | FromSubquery | FromFunction} from
9
9
  * @returns {string}
10
10
  */
11
11
  export function fromAlias(from) {
12
- return from.alias ?? (from.type === 'table' ? from.table : 'table')
12
+ if (from.alias) return from.alias
13
+ if (from.type === 'table') return from.table
14
+ if (from.type === 'function') return from.funcName.toLowerCase()
15
+ // Unaliased subquery: no natural name. Callers that need a stable alias
16
+ // should require one at parse time.
17
+ return 'table'
18
+ }
19
+
20
+ /**
21
+ * Returns the single output column name for a FROM table function.
22
+ *
23
+ * @param {FromFunction} from
24
+ * @returns {string}
25
+ */
26
+ export function tableFunctionColumnName(from) {
27
+ return from.columnAlias ?? from.funcName.toLowerCase()
13
28
  }
14
29
 
15
30
  /**
@@ -54,8 +69,24 @@ export function extractColumns({ select, parentColumns }) {
54
69
  // directly. For non-star queries, parent names may be aliases and are
55
70
  // handled below by filtering derived columns and collecting from expressions.
56
71
  const hasStar = select.columns.some(col => col.type === 'star' && !col.table)
72
+ // Exclude parent names that match a derived alias in this SELECT — those are
73
+ // produced by projection (e.g. `SELECT *, a+b AS c`), not by the source.
74
+ /** @type {Set<string>} */
75
+ const derivedAliases = new Set()
76
+ for (const col of select.columns) {
77
+ if (col.type === 'derived') {
78
+ derivedAliases.add(col.alias ?? derivedAlias(col.expr))
79
+ }
80
+ }
57
81
  /** @type {IdentifierNode[]} */
58
- const identifiers = hasStar && parentColumns ? [...parentColumns] : []
82
+ const identifiers = hasStar && parentColumns
83
+ ? parentColumns.filter(id => !derivedAliases.has(id.name))
84
+ : []
85
+ // Identifiers collected from lateral table function arguments, grouped with
86
+ // the aliases visible to that argument. Earlier lateral outputs are visible;
87
+ // the current function alias and later joins are not.
88
+ /** @type {{ identifiers: IdentifierNode[], visibleAliases: string[] }[]} */
89
+ const lateralArgGroups = []
59
90
 
60
91
  // Collect ORDER BY identifiers, excluding SELECT aliases (their underlying
61
92
  // columns are already collected from select.columns expressions above)
@@ -88,8 +119,19 @@ export function extractColumns({ select, parentColumns }) {
88
119
  collectColumnsFromExpr(expr, identifiers, selectAliases)
89
120
  }
90
121
  collectColumnsFromExpr(select.having, identifiers, selectAliases)
122
+ const visibleLateralAliases = [fromAlias(select.from)]
91
123
  for (const join of select.joins) {
92
124
  collectColumnsFromExpr(join.on, identifiers)
125
+ const joinAlias = join.alias ?? join.table
126
+ if (join.fromFunction) {
127
+ /** @type {IdentifierNode[]} */
128
+ const lateralArgIdentifiers = []
129
+ for (const arg of join.fromFunction.args) {
130
+ collectColumnsFromExpr(arg, lateralArgIdentifiers)
131
+ }
132
+ lateralArgGroups.push({ identifiers: lateralArgIdentifiers, visibleAliases: [...visibleLateralAliases] })
133
+ }
134
+ visibleLateralAliases.push(joinAlias)
93
135
  }
94
136
 
95
137
  // Partition identifiers by table prefix
@@ -111,6 +153,26 @@ export function extractColumns({ select, parentColumns }) {
111
153
  }
112
154
  }
113
155
 
156
+ // Partition identifiers from lateral UNNEST args using only the left-side
157
+ // aliases that are in scope for that specific join.
158
+ for (const { identifiers, visibleAliases } of lateralArgGroups) {
159
+ for (const { prefix, name } of identifiers) {
160
+ if (prefix) {
161
+ const set = perTable.get(prefix)
162
+ if (set) set.add(name)
163
+ } else {
164
+ if (visibleAliases.length > 1) {
165
+ for (const alias of visibleAliases) {
166
+ perTable.set(alias, undefined)
167
+ }
168
+ } else if (visibleAliases.length === 1) {
169
+ const set = perTable.get(visibleAliases[0])
170
+ if (set) set.add(name)
171
+ }
172
+ }
173
+ }
174
+ }
175
+
114
176
  // Build result map: convert Sets to arrays, undefined for all-columns tables
115
177
  for (const alias of aliases) {
116
178
  const set = perTable.get(alias)
@@ -207,7 +269,14 @@ function collectColumnsFromStatement(stmt, columns) {
207
269
  if (stmt.from?.type === 'subquery') {
208
270
  collectColumnsFromStatement(stmt.from.query, columns)
209
271
  }
210
- for (const join of stmt.joins) collectColumnsFromExpr(join.on, columns)
272
+ for (const join of stmt.joins) {
273
+ collectColumnsFromExpr(join.on, columns)
274
+ if (join.fromFunction) {
275
+ for (const arg of join.fromFunction.args) {
276
+ collectColumnsFromExpr(arg, columns)
277
+ }
278
+ }
279
+ }
211
280
  for (const expr of stmt.groupBy) collectColumnsFromExpr(expr, columns)
212
281
  collectColumnsFromExpr(stmt.having, columns)
213
282
  for (const item of stmt.orderBy) collectColumnsFromExpr(item.expr, columns)
@@ -255,11 +324,33 @@ export function inferStatementColumns({ stmt, cteColumns, tables }) {
255
324
  * @param {Record<string, AsyncDataSource>} [options.tables]
256
325
  * @returns {string[]}
257
326
  */
258
- function inferSelectSourceColumns({ select, cteColumns, tables }) {
327
+ export function inferSelectSourceColumns({ select, cteColumns, tables }) {
259
328
  if (select.from.type === 'subquery') {
260
329
  return inferStatementColumns({ stmt: select.from.query, cteColumns, tables })
261
330
  }
262
331
 
332
+ if (select.from.type === 'function') {
333
+ // Table functions currently produce a single column
334
+ if (!select.joins.length) {
335
+ return [tableFunctionColumnName(select.from)]
336
+ }
337
+ /** @type {string[]} */
338
+ const result = []
339
+ const alias = fromAlias(select.from)
340
+ result.push(`${alias}.${tableFunctionColumnName(select.from)}`)
341
+ for (const join of select.joins) {
342
+ const joinAlias = join.alias ?? join.table
343
+ if (join.fromFunction) {
344
+ result.push(`${joinAlias}.${tableFunctionColumnName(join.fromFunction)}`)
345
+ } else {
346
+ for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
347
+ result.push(`${joinAlias}.${col}`)
348
+ }
349
+ }
350
+ }
351
+ return result
352
+ }
353
+
263
354
  if (!select.joins.length) {
264
355
  return lookupTableColumns(select.from.table, cteColumns, tables)
265
356
  }
@@ -267,14 +358,18 @@ function inferSelectSourceColumns({ select, cteColumns, tables }) {
267
358
  // Collect all sources, then prefix each table's columns
268
359
  /** @type {string[]} */
269
360
  const result = []
270
- const fromAlias = select.from.alias ?? select.from.table
361
+ const sourceAlias = select.from.alias ?? select.from.table
271
362
  for (const col of lookupTableColumns(select.from.table, cteColumns, tables)) {
272
- result.push(`${fromAlias}.${col}`)
363
+ result.push(`${sourceAlias}.${col}`)
273
364
  }
274
365
  for (const join of select.joins) {
275
366
  const joinAlias = join.alias ?? join.table
276
- for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
277
- result.push(`${joinAlias}.${col}`)
367
+ if (join.fromFunction) {
368
+ result.push(`${joinAlias}.${tableFunctionColumnName(join.fromFunction)}`)
369
+ } else {
370
+ for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
371
+ result.push(`${joinAlias}.${col}`)
372
+ }
278
373
  }
279
374
  }
280
375
  return result