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.
- package/README.md +2 -2
- package/package.json +3 -3
- package/src/backend/dataSource.js +1 -1
- package/src/execute/aggregates.js +2 -23
- package/src/execute/execute.js +92 -174
- package/src/execute/expression.js +21 -41
- package/src/execute/having.js +4 -34
- package/src/execute/join.js +357 -0
- package/src/execute/utils.js +70 -1
- package/src/index.d.ts +1 -0
- package/src/parse/expression.js +49 -20
- package/src/parse/parse.js +59 -16
- package/src/types.d.ts +11 -12
- package/src/validation.js +1 -1
package/src/parse/parse.js
CHANGED
|
@@ -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
|
|
239
|
+
const toType = expectIdentifier(state).value
|
|
223
240
|
expect(state, 'paren', ')')
|
|
224
241
|
arg = {
|
|
225
242
|
kind: 'expression',
|
|
226
|
-
expr: { type: 'cast', expr, toType
|
|
243
|
+
expr: { type: 'cast', expr, toType },
|
|
227
244
|
}
|
|
228
245
|
} else {
|
|
229
|
-
|
|
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
|
|
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
|
|
327
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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'
|
|
94
|
+
type: 'in'
|
|
96
95
|
expr: ExprNode
|
|
97
96
|
subquery: SelectStatement
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
export interface InValuesNode {
|
|
101
|
-
type: '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
|
-
|
|
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
|
}
|