squirreling 0.4.0 → 0.4.2

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.
@@ -3,7 +3,7 @@ import { parseExpression } from './expression.js'
3
3
  import { isAggregateFunc } from '../validation.js'
4
4
 
5
5
  /**
6
- * @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, Token, TokenType } from '../types.js'
6
+ * @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, FromTable, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, Token, TokenType } from '../types.js'
7
7
  */
8
8
 
9
9
  // Keywords that cannot be used as implicit aliases after a column
@@ -17,6 +17,23 @@ const RESERVED_AFTER_COLUMN = new Set([
17
17
  'OFFSET',
18
18
  ])
19
19
 
20
+ // Keywords that cannot be used as table aliases
21
+ const RESERVED_AFTER_TABLE = new Set([
22
+ 'WHERE',
23
+ 'GROUP',
24
+ 'HAVING',
25
+ 'ORDER',
26
+ 'LIMIT',
27
+ 'OFFSET',
28
+ 'JOIN',
29
+ 'INNER',
30
+ 'LEFT',
31
+ 'RIGHT',
32
+ 'FULL',
33
+ 'CROSS',
34
+ 'ON',
35
+ ])
36
+
20
37
  /**
21
38
  * @param {string} query
22
39
  * @returns {SelectStatement}
@@ -219,17 +236,24 @@ function parseAggregateItem(state, func) {
219
236
  const cursor = createExprCursor(state)
220
237
  const expr = parseExpression(cursor)
221
238
  expect(state, 'keyword', 'AS')
222
- const typeTok = expectIdentifier(state)
239
+ const toType = expectIdentifier(state).value
223
240
  expect(state, 'paren', ')')
224
241
  arg = {
225
242
  kind: 'expression',
226
- expr: { type: 'cast', expr, toType: typeTok.value },
243
+ expr: { type: 'cast', expr, toType },
227
244
  }
228
245
  } else {
229
- const colTok = expectIdentifier(state)
246
+ // column name
247
+ let name = expectIdentifier(state).value
248
+ // Handle qualified column names like orders.amount
249
+ if (current(state).type === 'dot') {
250
+ consume(state) // consume dot
251
+ const qualifiedPart = expectIdentifier(state)
252
+ name = `${name}.${qualifiedPart.value}`
253
+ }
230
254
  arg = {
231
255
  kind: 'expression',
232
- expr: { type: 'identifier', name: colTok.value },
256
+ expr: { type: 'identifier', name },
233
257
  }
234
258
  }
235
259
 
@@ -240,6 +264,25 @@ function parseAggregateItem(state, func) {
240
264
  return { kind: 'aggregate', func, arg, alias }
241
265
  }
242
266
 
267
+ /**
268
+ * Parses an optional table alias (e.g., "FROM users u" or "FROM users AS u")
269
+ * @param {ParserState} state
270
+ * @returns {string | undefined}
271
+ */
272
+ function parseTableAlias(state) {
273
+ // Check for explicit AS keyword
274
+ if (match(state, 'keyword', 'AS')) {
275
+ const aliasTok = expectIdentifier(state)
276
+ return aliasTok.value
277
+ }
278
+ // Check for implicit alias (identifier not in reserved list)
279
+ const maybeAlias = current(state)
280
+ if (maybeAlias.type === 'identifier' && !RESERVED_AFTER_TABLE.has(maybeAlias.value.toUpperCase())) {
281
+ consume(state)
282
+ return maybeAlias.value
283
+ }
284
+ }
285
+
243
286
  /**
244
287
  * @param {ParserState} state
245
288
  * @returns {string | undefined}
@@ -322,9 +365,9 @@ function parseJoins(state) {
322
365
  break
323
366
  }
324
367
 
325
- // Parse table name
326
- const tableTok = expectIdentifier(state)
327
- const tableName = tableTok.value
368
+ // Parse table name and optional alias
369
+ const tableName = expectIdentifier(state).value
370
+ const tableAlias = parseTableAlias(state)
328
371
 
329
372
  // Parse ON condition
330
373
  expect(state, 'keyword', 'ON')
@@ -332,8 +375,9 @@ function parseJoins(state) {
332
375
  const condition = parseExpression(cursor)
333
376
 
334
377
  joins.push({
335
- type: joinType,
378
+ joinType,
336
379
  table: tableName,
380
+ alias: tableAlias,
337
381
  on: condition,
338
382
  })
339
383
  }
@@ -351,12 +395,8 @@ function parseSubquery(state) {
351
395
  const query = parseSelectInternal(state)
352
396
  expect(state, 'paren', ')')
353
397
  expect(state, 'keyword', 'AS')
354
- const aliasTok = expectIdentifier(state)
355
- return {
356
- kind: 'subquery',
357
- query,
358
- alias: aliasTok.value,
359
- }
398
+ const alias = expectIdentifier(state).value
399
+ return { kind: 'subquery', query, alias }
360
400
  }
361
401
 
362
402
  /**
@@ -376,6 +416,7 @@ function parseSelectInternal(state) {
376
416
  expect(state, 'keyword', 'FROM')
377
417
 
378
418
  // Check if it's a subquery or table name
419
+ /** @type {FromTable | FromSubquery} */
379
420
  let from
380
421
  const tok = current(state)
381
422
  if (tok.type === 'paren' && tok.value === '(') {
@@ -383,7 +424,9 @@ function parseSelectInternal(state) {
383
424
  from = parseSubquery(state)
384
425
  } else {
385
426
  // Simple table name: SELECT * FROM users
386
- from = expectIdentifier(state).value
427
+ const table = expectIdentifier(state).value
428
+ const alias = parseTableAlias(state)
429
+ from = { kind: 'table', table, alias }
387
430
  }
388
431
 
389
432
  // Parse JOIN clauses
package/src/types.d.ts CHANGED
@@ -21,7 +21,7 @@ export type SqlPrimitive = string | number | bigint | boolean | null
21
21
  export interface SelectStatement {
22
22
  distinct: boolean
23
23
  columns: SelectColumn[]
24
- from: string | FromSubquery
24
+ from: FromTable | FromSubquery
25
25
  joins: JoinClause[]
26
26
  where?: ExprNode
27
27
  groupBy: ExprNode[]
@@ -31,6 +31,12 @@ export interface SelectStatement {
31
31
  offset?: number
32
32
  }
33
33
 
34
+ export interface FromTable {
35
+ kind: 'table'
36
+ table: string
37
+ alias?: string
38
+ }
39
+
34
40
  export interface FromSubquery {
35
41
  kind: 'subquery'
36
42
  query: SelectStatement
@@ -84,21 +90,14 @@ export interface CastNode {
84
90
  toType: string
85
91
  }
86
92
 
87
- export interface BetweenNode {
88
- type: 'between' | 'not between'
89
- expr: ExprNode
90
- lower: ExprNode
91
- upper: ExprNode
92
- }
93
-
94
93
  export interface InSubqueryNode {
95
- type: 'in' | 'not in'
94
+ type: 'in'
96
95
  expr: ExprNode
97
96
  subquery: SelectStatement
98
97
  }
99
98
 
100
99
  export interface InValuesNode {
101
- type: 'in valuelist' | 'not in valuelist'
100
+ type: 'in valuelist'
102
101
  expr: ExprNode
103
102
  values: ExprNode[]
104
103
  }
@@ -132,7 +131,6 @@ export type ExprNode =
132
131
  | BinaryNode
133
132
  | FunctionNode
134
133
  | CastNode
135
- | BetweenNode
136
134
  | InSubqueryNode
137
135
  | InValuesNode
138
136
  | ExistsNode
@@ -184,8 +182,9 @@ export interface OrderByItem {
184
182
  export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS'
185
183
 
186
184
  export interface JoinClause {
187
- type: JoinType
185
+ joinType: JoinType
188
186
  table: string
187
+ alias?: string
189
188
  on?: ExprNode
190
189
  }
191
190
 
package/src/validation.js CHANGED
@@ -13,5 +13,5 @@ export function isAggregateFunc(name) {
13
13
  * @returns {name is StringFunc}
14
14
  */
15
15
  export function isStringFunc(name) {
16
- return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE'].includes(name)
16
+ return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE', 'RANDOM', 'RAND'].includes(name)
17
17
  }