squirreling 0.2.3 → 0.2.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 +1 -1
- package/src/execute/execute.js +49 -49
- package/src/execute/expression.js +22 -4
- package/src/parse/expression.js +91 -20
- package/src/parse/parse.js +41 -75
- package/src/parse/tokenize.js +1 -0
- package/src/types.d.ts +36 -39
- package/src/validation.js +1 -1
package/package.json
CHANGED
package/src/execute/execute.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { DataSource, ExecuteSqlOptions, FunctionColumn, FunctionNode, OrderByItem, RowSource, SelectStatement, SqlPrimitive } from '../types.js'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
6
2
|
import { evaluateExpr } from './expression.js'
|
|
7
3
|
import { evaluateHavingExpr } from './having.js'
|
|
8
4
|
import { parseSql } from '../parse/parse.js'
|
|
9
5
|
import { createMemorySource, createRowAccessor } from '../backend/memory.js'
|
|
10
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @import { DataSource, ExecuteSqlOptions, ExprNode, OrderByItem, RowSource, SelectStatement, SqlPrimitive } from '../types.js'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
11
|
/**
|
|
12
12
|
* Executes a SQL SELECT query against a data source
|
|
13
13
|
*
|
|
@@ -21,21 +21,31 @@ export function executeSql({ source, query }) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Generates a default alias
|
|
24
|
+
* Generates a default alias for a derived column expression
|
|
25
25
|
*
|
|
26
|
-
* @param {
|
|
27
|
-
* @returns {string} the generated alias
|
|
26
|
+
* @param {ExprNode} expr - the expression node
|
|
27
|
+
* @returns {string} the generated alias
|
|
28
28
|
*/
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const columnNames = col.args
|
|
33
|
-
.filter(arg => arg.type === 'identifier')
|
|
34
|
-
.map(arg => arg.name)
|
|
35
|
-
if (columnNames.length > 0) {
|
|
36
|
-
return base + '_' + columnNames.join('_')
|
|
29
|
+
function defaultDerivedAlias(expr) {
|
|
30
|
+
if (expr.type === 'identifier') {
|
|
31
|
+
return expr.name
|
|
37
32
|
}
|
|
38
|
-
|
|
33
|
+
if (expr.type === 'function') {
|
|
34
|
+
const base = expr.name.toLowerCase()
|
|
35
|
+
// Try to extract column names from identifier arguments
|
|
36
|
+
const columnNames = expr.args
|
|
37
|
+
.filter(arg => arg.type === 'identifier')
|
|
38
|
+
.map(arg => arg.name)
|
|
39
|
+
if (columnNames.length > 0) {
|
|
40
|
+
return base + '_' + columnNames.join('_')
|
|
41
|
+
}
|
|
42
|
+
return base
|
|
43
|
+
}
|
|
44
|
+
if (expr.type === 'cast') return 'cast_expr'
|
|
45
|
+
if (expr.type === 'unary' && expr.argument.type === 'identifier') {
|
|
46
|
+
return expr.op === '-' ? 'neg_' + expr.argument.name : 'expr'
|
|
47
|
+
}
|
|
48
|
+
return 'expr'
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/**
|
|
@@ -118,6 +128,24 @@ function applyOrderBy(rows, orderBy) {
|
|
|
118
128
|
const dir = term.direction
|
|
119
129
|
const av = evaluateExpr(term.expr, createRowAccessor(a))
|
|
120
130
|
const bv = evaluateExpr(term.expr, createRowAccessor(b))
|
|
131
|
+
|
|
132
|
+
// Handle NULLS FIRST / NULLS LAST
|
|
133
|
+
const aIsNull = av == null
|
|
134
|
+
const bIsNull = bv == null
|
|
135
|
+
|
|
136
|
+
if (aIsNull || bIsNull) {
|
|
137
|
+
if (aIsNull && bIsNull) continue // both null, try next sort term
|
|
138
|
+
|
|
139
|
+
// Determine null ordering
|
|
140
|
+
const nullsFirst = term.nulls === 'LAST' ? false : true // default is NULLS FIRST
|
|
141
|
+
|
|
142
|
+
if (aIsNull) {
|
|
143
|
+
return nullsFirst ? -1 : 1
|
|
144
|
+
} else {
|
|
145
|
+
return nullsFirst ? 1 : -1
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
const cmp = compareValues(av, bv)
|
|
122
150
|
if (cmp !== 0) {
|
|
123
151
|
return dir === 'DESC' ? -cmp : cmp
|
|
@@ -214,20 +242,9 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
214
242
|
continue
|
|
215
243
|
}
|
|
216
244
|
|
|
217
|
-
if (col.kind === '
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
// Evaluate on first row of group (all rows have same value for GROUP BY columns)
|
|
221
|
-
resultRow[alias] = group[0]?.getCell(name)
|
|
222
|
-
continue
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (col.kind === 'function') {
|
|
226
|
-
// Evaluate function on the first row of the group
|
|
227
|
-
/** @type {FunctionNode} */
|
|
228
|
-
const funcNode = { type: 'function', name: col.func, args: col.args }
|
|
229
|
-
const alias = col.alias ?? defaultFunctionAlias(col)
|
|
230
|
-
const value = group.length > 0 ? evaluateExpr(funcNode, group[0]) : undefined
|
|
245
|
+
if (col.kind === 'derived') {
|
|
246
|
+
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
247
|
+
const value = group.length > 0 ? evaluateExpr(col.expr, group[0]) : undefined
|
|
231
248
|
resultRow[alias] = value
|
|
232
249
|
continue
|
|
233
250
|
}
|
|
@@ -238,13 +255,6 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
238
255
|
resultRow[alias] = value
|
|
239
256
|
continue
|
|
240
257
|
}
|
|
241
|
-
|
|
242
|
-
if (col.kind === 'operation') {
|
|
243
|
-
const alias = col.alias ?? 'expr'
|
|
244
|
-
const value = group.length > 0 ? evaluateExpr(col.expr, group[0]) : undefined
|
|
245
|
-
resultRow[alias] = value
|
|
246
|
-
continue
|
|
247
|
-
}
|
|
248
258
|
}
|
|
249
259
|
|
|
250
260
|
// Apply HAVING filter before adding to projected results
|
|
@@ -269,18 +279,8 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
269
279
|
for (const key of keys) {
|
|
270
280
|
outRow[key] = row.getCell(key)
|
|
271
281
|
}
|
|
272
|
-
} else if (col.kind === '
|
|
273
|
-
const
|
|
274
|
-
const alias = col.alias ?? name
|
|
275
|
-
outRow[alias] = row.getCell(name)
|
|
276
|
-
} else if (col.kind === 'function') {
|
|
277
|
-
/** @type {FunctionNode} */
|
|
278
|
-
const funcNode = { type: 'function', name: col.func, args: col.args }
|
|
279
|
-
const value = evaluateExpr(funcNode, row)
|
|
280
|
-
const alias = col.alias ?? defaultFunctionAlias(col)
|
|
281
|
-
outRow[alias] = value
|
|
282
|
-
} else if (col.kind === 'operation') {
|
|
283
|
-
const alias = col.alias ?? 'expr'
|
|
282
|
+
} else if (col.kind === 'derived') {
|
|
283
|
+
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
284
284
|
const value = evaluateExpr(col.expr, row)
|
|
285
285
|
outRow[alias] = value
|
|
286
286
|
} else if (col.kind === 'aggregate') {
|
|
@@ -131,23 +131,23 @@ export function evaluateExpr(node, row) {
|
|
|
131
131
|
return String(val).length
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
if (funcName === 'SUBSTRING') {
|
|
134
|
+
if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
|
|
135
135
|
if (args.length < 2 || args.length > 3) {
|
|
136
|
-
throw new Error(
|
|
136
|
+
throw new Error(`${funcName} requires 2 or 3 arguments`)
|
|
137
137
|
}
|
|
138
138
|
const str = args[0]
|
|
139
139
|
if (str == null) return null
|
|
140
140
|
const strVal = String(str)
|
|
141
141
|
const start = Number(args[1])
|
|
142
142
|
if (!Number.isInteger(start) || start < 1) {
|
|
143
|
-
throw new Error(
|
|
143
|
+
throw new Error(`${funcName} start position must be a positive integer`)
|
|
144
144
|
}
|
|
145
145
|
// SQL uses 1-based indexing
|
|
146
146
|
const startIdx = start - 1
|
|
147
147
|
if (args.length === 3) {
|
|
148
148
|
const len = Number(args[2])
|
|
149
149
|
if (!Number.isInteger(len) || len < 0) {
|
|
150
|
-
throw new Error(
|
|
150
|
+
throw new Error(`${funcName} length must be a non-negative integer`)
|
|
151
151
|
}
|
|
152
152
|
return strVal.substring(startIdx, startIdx + len)
|
|
153
153
|
}
|
|
@@ -190,6 +190,24 @@ export function evaluateExpr(node, row) {
|
|
|
190
190
|
throw new Error('Unsupported CAST to type ' + node.toType)
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// IN and NOT IN with value lists
|
|
194
|
+
if (node.type === 'in valuelist') {
|
|
195
|
+
const exprVal = evaluateExpr(node.expr, row)
|
|
196
|
+
for (const valueNode of node.values) {
|
|
197
|
+
const val = evaluateExpr(valueNode, row)
|
|
198
|
+
if (exprVal === val) return true
|
|
199
|
+
}
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
if (node.type === 'not in valuelist') {
|
|
203
|
+
const exprVal = evaluateExpr(node.expr, row)
|
|
204
|
+
for (const valueNode of node.values) {
|
|
205
|
+
const val = evaluateExpr(valueNode, row)
|
|
206
|
+
if (exprVal === val) return false
|
|
207
|
+
}
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
210
|
+
|
|
193
211
|
// IN and NOT IN with subqueries
|
|
194
212
|
if (node.type === 'in') {
|
|
195
213
|
throw new Error('WHERE IN with subqueries is not yet supported.')
|
package/src/parse/expression.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { isAggregateFunc, isStringFunc } from '../validation.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* @import { ExprCursor, ExprNode, BinaryOp
|
|
4
|
+
* @import { ExprCursor, ExprNode, BinaryOp } from '../types.js'
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -11,13 +13,10 @@ export function parseExpression(c) {
|
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
|
-
* Exposed so SELECT list parsing can reuse the same notion of "primary"
|
|
15
|
-
* for function arguments, etc.
|
|
16
|
-
*
|
|
17
16
|
* @param {ExprCursor} c
|
|
18
17
|
* @returns {ExprNode}
|
|
19
18
|
*/
|
|
20
|
-
|
|
19
|
+
function parsePrimary(c) {
|
|
21
20
|
const tok = c.current()
|
|
22
21
|
|
|
23
22
|
if (tok.type === 'paren' && tok.value === '(') {
|
|
@@ -30,10 +29,30 @@ export function parsePrimary(c) {
|
|
|
30
29
|
if (tok.type === 'identifier') {
|
|
31
30
|
const next = c.peek(1)
|
|
32
31
|
|
|
32
|
+
// CAST expression
|
|
33
|
+
if (tok.value === 'CAST' && next.type === 'paren' && next.value === '(') {
|
|
34
|
+
c.consume() // CAST
|
|
35
|
+
c.consume() // '('
|
|
36
|
+
const expr = parseExpression(c)
|
|
37
|
+
c.expect('keyword', 'AS')
|
|
38
|
+
const typeTok = c.expectIdentifier()
|
|
39
|
+
c.expect('paren', ')')
|
|
40
|
+
return {
|
|
41
|
+
type: 'cast',
|
|
42
|
+
expr,
|
|
43
|
+
toType: typeTok.value,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
// function call
|
|
34
48
|
if (next.type === 'paren' && next.value === '(') {
|
|
35
49
|
const funcName = tok.value
|
|
36
|
-
|
|
50
|
+
|
|
51
|
+
// validate function names
|
|
52
|
+
if (!isStringFunc(funcName) && !isAggregateFunc(funcName)) {
|
|
53
|
+
throw new Error(`Unknown function "${funcName}" at position ${tok.position}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
c.consume() // function name
|
|
38
57
|
c.consume() // '('
|
|
39
58
|
|
|
@@ -281,28 +300,80 @@ function parseComparison(c) {
|
|
|
281
300
|
if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
|
|
282
301
|
c.consume() // NOT
|
|
283
302
|
c.consume() // IN
|
|
284
|
-
|
|
285
|
-
|
|
303
|
+
|
|
304
|
+
// Check if it's a subquery or a list of values by peeking ahead
|
|
305
|
+
// parseSubquery expects to consume the opening paren itself
|
|
306
|
+
const parenTok = c.current()
|
|
307
|
+
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
308
|
+
throw new Error('Expected ( after IN')
|
|
286
309
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
const peekTok = c.peek(1)
|
|
311
|
+
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
312
|
+
// Subquery - let parseSubquery handle the parens
|
|
313
|
+
if (!c.parseSubquery) {
|
|
314
|
+
throw new Error('Subquery parsing not available in this context')
|
|
315
|
+
}
|
|
316
|
+
const subquery = c.parseSubquery()
|
|
317
|
+
return {
|
|
318
|
+
type: 'not in',
|
|
319
|
+
expr: left,
|
|
320
|
+
subquery,
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Parse list of values - we handle the parens
|
|
324
|
+
c.consume() // '('
|
|
325
|
+
/** @type {ExprNode[]} */
|
|
326
|
+
const values = []
|
|
327
|
+
while (true) {
|
|
328
|
+
values.push(parseExpression(c))
|
|
329
|
+
if (!c.match('comma')) break
|
|
330
|
+
}
|
|
331
|
+
c.expect('paren', ')')
|
|
332
|
+
return {
|
|
333
|
+
type: 'not in valuelist',
|
|
334
|
+
expr: left,
|
|
335
|
+
values,
|
|
336
|
+
}
|
|
292
337
|
}
|
|
293
338
|
}
|
|
294
339
|
}
|
|
295
340
|
|
|
296
341
|
if (tok.type === 'keyword' && tok.value === 'IN') {
|
|
297
342
|
c.consume() // IN
|
|
298
|
-
|
|
299
|
-
|
|
343
|
+
|
|
344
|
+
// Check if it's a subquery or a list of values by peeking ahead
|
|
345
|
+
// parseSubquery expects to consume the opening paren itself
|
|
346
|
+
const parenTok = c.current()
|
|
347
|
+
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
348
|
+
throw new Error('Expected ( after IN')
|
|
300
349
|
}
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
350
|
+
const peekTok = c.peek(1)
|
|
351
|
+
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
352
|
+
// Subquery - let parseSubquery handle the parens
|
|
353
|
+
if (!c.parseSubquery) {
|
|
354
|
+
throw new Error('Subquery parsing not available in this context')
|
|
355
|
+
}
|
|
356
|
+
const subquery = c.parseSubquery()
|
|
357
|
+
return {
|
|
358
|
+
type: 'in',
|
|
359
|
+
expr: left,
|
|
360
|
+
subquery,
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
// Parse list of values - we handle the parens
|
|
364
|
+
c.consume() // '('
|
|
365
|
+
/** @type {ExprNode[]} */
|
|
366
|
+
const values = []
|
|
367
|
+
while (true) {
|
|
368
|
+
values.push(parseExpression(c))
|
|
369
|
+
if (!c.match('comma')) break
|
|
370
|
+
}
|
|
371
|
+
c.expect('paren', ')')
|
|
372
|
+
return {
|
|
373
|
+
type: 'in valuelist',
|
|
374
|
+
expr: left,
|
|
375
|
+
values,
|
|
376
|
+
}
|
|
306
377
|
}
|
|
307
378
|
}
|
|
308
379
|
|
package/src/parse/parse.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { tokenize } from './tokenize.js'
|
|
2
|
+
import { parseExpression } from './expression.js'
|
|
3
|
+
import { isAggregateFunc } from '../validation.js'
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
* @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn,
|
|
6
|
+
* @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, Token, TokenType } from '../types.js'
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import { tokenize } from './tokenize.js'
|
|
6
|
-
import { parseExpression, parsePrimary } from './expression.js'
|
|
7
|
-
import { isAggregateFunc, isStringFunc } from '../validation.js'
|
|
8
|
-
|
|
9
9
|
// Keywords that cannot be used as implicit aliases after a column
|
|
10
10
|
const RESERVED_AFTER_COLUMN = new Set([
|
|
11
11
|
'FROM',
|
|
@@ -112,6 +112,7 @@ function expectIdentifier(state) {
|
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Creates an ExprCursor adapter for the ParserState.
|
|
115
|
+
*
|
|
115
116
|
* @param {ParserState} state
|
|
116
117
|
* @returns {ExprCursor}
|
|
117
118
|
*/
|
|
@@ -141,6 +142,20 @@ function parseSelectList(state) {
|
|
|
141
142
|
const cols = []
|
|
142
143
|
const tok = current(state)
|
|
143
144
|
|
|
145
|
+
// Check for qualified asterisk (table.*)
|
|
146
|
+
if (tok.type === 'identifier') {
|
|
147
|
+
const next = peekToken(state, 1)
|
|
148
|
+
const nextNext = peekToken(state, 2)
|
|
149
|
+
if (next.type === 'dot' && nextNext.type === 'operator' && nextNext.value === '*') {
|
|
150
|
+
const tableTok = consume(state) // consume table name
|
|
151
|
+
consume(state) // consume dot
|
|
152
|
+
consume(state) // consume asterisk
|
|
153
|
+
cols.push({ kind: 'star', table: tableTok.value })
|
|
154
|
+
return cols
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check for unqualified asterisk (*)
|
|
144
159
|
if (tok.type === 'operator' && tok.value === '*') {
|
|
145
160
|
consume(state)
|
|
146
161
|
cols.push({ kind: 'star' })
|
|
@@ -166,56 +181,20 @@ function parseSelectItem(state) {
|
|
|
166
181
|
throw parseError(state, 'column name or expression')
|
|
167
182
|
}
|
|
168
183
|
|
|
169
|
-
if (tok.type === 'identifier' && tok.value === 'CAST') {
|
|
170
|
-
expectIdentifier(state) // consume CAST
|
|
171
|
-
expect(state, 'paren', '(')
|
|
172
|
-
const cursor = createExprCursor(state)
|
|
173
|
-
const expr = parseExpression(cursor)
|
|
174
|
-
expect(state, 'keyword', 'AS')
|
|
175
|
-
const typeTok = expectIdentifier(state)
|
|
176
|
-
expect(state, 'paren', ')')
|
|
177
|
-
const alias = parseAs(state)
|
|
178
|
-
return {
|
|
179
|
-
kind: 'operation',
|
|
180
|
-
expr: { type: 'cast', expr, toType: typeTok.value },
|
|
181
|
-
alias,
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (tok.type === 'operator') {
|
|
186
|
-
// Handle SELECT expression AS alias
|
|
187
|
-
const cursor = createExprCursor(state)
|
|
188
|
-
const expr = parseExpression(cursor)
|
|
189
|
-
const alias = parseAs(state)
|
|
190
|
-
return { kind: 'operation', expr, alias }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
184
|
const next = peekToken(state, 1)
|
|
194
|
-
const upper = tok.value.toUpperCase()
|
|
195
|
-
|
|
196
185
|
if (next.type === 'paren' && next.value === '(') {
|
|
197
|
-
|
|
186
|
+
const upper = tok.value.toUpperCase()
|
|
198
187
|
if (isAggregateFunc(upper)) {
|
|
188
|
+
expectIdentifier(state) // consume function name
|
|
199
189
|
return parseAggregateItem(state, upper)
|
|
200
190
|
}
|
|
201
|
-
if (isStringFunc(upper)) {
|
|
202
|
-
return parseStringFunctionItem(state, upper)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
consume(state)
|
|
207
|
-
let column = tok.value
|
|
208
|
-
|
|
209
|
-
// Handle dot notation (table.column)
|
|
210
|
-
if (current(state).type === 'dot') {
|
|
211
|
-
consume(state) // consume the dot
|
|
212
|
-
const columnTok = expectIdentifier(state)
|
|
213
|
-
column += '.' + columnTok.value
|
|
214
191
|
}
|
|
215
192
|
|
|
193
|
+
// Delegate to expression parser
|
|
194
|
+
const cursor = createExprCursor(state)
|
|
195
|
+
const expr = parseExpression(cursor)
|
|
216
196
|
const alias = parseAs(state)
|
|
217
|
-
|
|
218
|
-
return { kind: 'column', column, alias }
|
|
197
|
+
return { kind: 'derived', expr, alias }
|
|
219
198
|
}
|
|
220
199
|
|
|
221
200
|
/**
|
|
@@ -261,34 +240,6 @@ function parseAggregateItem(state, func) {
|
|
|
261
240
|
return { kind: 'aggregate', func, arg, alias }
|
|
262
241
|
}
|
|
263
242
|
|
|
264
|
-
/**
|
|
265
|
-
* @param {ParserState} state
|
|
266
|
-
* @param {StringFunc} func
|
|
267
|
-
* @returns {SelectColumn}
|
|
268
|
-
*/
|
|
269
|
-
function parseStringFunctionItem(state, func) {
|
|
270
|
-
expect(state, 'paren', '(')
|
|
271
|
-
|
|
272
|
-
/** @type {ExprNode[]} */
|
|
273
|
-
const args = []
|
|
274
|
-
|
|
275
|
-
// Parse comma-separated arguments
|
|
276
|
-
if (current(state).type !== 'paren' || current(state).value !== ')') {
|
|
277
|
-
const cursor = createExprCursor(state)
|
|
278
|
-
while (true) {
|
|
279
|
-
const arg = parsePrimary(cursor)
|
|
280
|
-
args.push(arg)
|
|
281
|
-
if (!match(state, 'comma')) break
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
expect(state, 'paren', ')')
|
|
286
|
-
|
|
287
|
-
const alias = parseAs(state)
|
|
288
|
-
|
|
289
|
-
return { kind: 'function', func, args, alias }
|
|
290
|
-
}
|
|
291
|
-
|
|
292
243
|
/**
|
|
293
244
|
* @param {ParserState} state
|
|
294
245
|
* @returns {string | undefined}
|
|
@@ -481,9 +432,24 @@ function parseSelectInternal(state) {
|
|
|
481
432
|
} else if (match(state, 'keyword', 'DESC')) {
|
|
482
433
|
direction = 'DESC'
|
|
483
434
|
}
|
|
435
|
+
/** @type {'FIRST' | 'LAST' | undefined} */
|
|
436
|
+
let nulls
|
|
437
|
+
if (match(state, 'keyword', 'NULLS')) {
|
|
438
|
+
const tok = current(state)
|
|
439
|
+
if (tok.type === 'identifier' && tok.value.toUpperCase() === 'FIRST') {
|
|
440
|
+
consume(state)
|
|
441
|
+
nulls = 'FIRST'
|
|
442
|
+
} else if (tok.type === 'identifier' && tok.value.toUpperCase() === 'LAST') {
|
|
443
|
+
consume(state)
|
|
444
|
+
nulls = 'LAST'
|
|
445
|
+
} else {
|
|
446
|
+
throw parseError(state, 'FIRST or LAST after NULLS')
|
|
447
|
+
}
|
|
448
|
+
}
|
|
484
449
|
orderBy.push({
|
|
485
450
|
expr,
|
|
486
451
|
direction,
|
|
452
|
+
nulls,
|
|
487
453
|
})
|
|
488
454
|
if (!match(state, 'comma')) break
|
|
489
455
|
}
|
package/src/parse/tokenize.js
CHANGED
package/src/types.d.ts
CHANGED
|
@@ -34,26 +34,6 @@ export interface SelectStatement {
|
|
|
34
34
|
offset?: number
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export type TokenType =
|
|
38
|
-
| 'keyword'
|
|
39
|
-
| 'identifier'
|
|
40
|
-
| 'number'
|
|
41
|
-
| 'string'
|
|
42
|
-
| 'operator'
|
|
43
|
-
| 'comma'
|
|
44
|
-
| 'dot'
|
|
45
|
-
| 'paren'
|
|
46
|
-
| 'semicolon'
|
|
47
|
-
| 'eof'
|
|
48
|
-
|
|
49
|
-
export interface Token {
|
|
50
|
-
type: TokenType
|
|
51
|
-
value: string
|
|
52
|
-
position: number
|
|
53
|
-
numericValue?: number
|
|
54
|
-
originalValue?: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
37
|
export type BinaryOp =
|
|
58
38
|
| 'AND'
|
|
59
39
|
| 'OR'
|
|
@@ -108,12 +88,18 @@ export interface BetweenNode {
|
|
|
108
88
|
upper: ExprNode
|
|
109
89
|
}
|
|
110
90
|
|
|
111
|
-
export interface
|
|
91
|
+
export interface InSubqueryNode {
|
|
112
92
|
type: 'in' | 'not in'
|
|
113
93
|
expr: ExprNode
|
|
114
94
|
subquery: SelectStatement
|
|
115
95
|
}
|
|
116
96
|
|
|
97
|
+
export interface InValuesNode {
|
|
98
|
+
type: 'in valuelist' | 'not in valuelist'
|
|
99
|
+
expr: ExprNode
|
|
100
|
+
values: ExprNode[]
|
|
101
|
+
}
|
|
102
|
+
|
|
117
103
|
export interface ExistsNode {
|
|
118
104
|
type: 'exists' | 'not exists'
|
|
119
105
|
subquery: SelectStatement
|
|
@@ -127,23 +113,19 @@ export type ExprNode =
|
|
|
127
113
|
| FunctionNode
|
|
128
114
|
| CastNode
|
|
129
115
|
| BetweenNode
|
|
130
|
-
|
|
|
116
|
+
| InSubqueryNode
|
|
117
|
+
| InValuesNode
|
|
131
118
|
| ExistsNode
|
|
132
119
|
|
|
133
120
|
export interface StarColumn {
|
|
134
121
|
kind: 'star'
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface SimpleColumn {
|
|
139
|
-
kind: 'column'
|
|
140
|
-
column: string
|
|
122
|
+
table?: string
|
|
141
123
|
alias?: string
|
|
142
124
|
}
|
|
143
125
|
|
|
144
126
|
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
|
|
145
127
|
|
|
146
|
-
export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'TRIM'
|
|
128
|
+
export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM'
|
|
147
129
|
|
|
148
130
|
export interface AggregateArgStar {
|
|
149
131
|
kind: 'star'
|
|
@@ -163,24 +145,18 @@ export interface AggregateColumn {
|
|
|
163
145
|
alias?: string
|
|
164
146
|
}
|
|
165
147
|
|
|
166
|
-
export interface
|
|
167
|
-
kind: '
|
|
168
|
-
func: StringFunc
|
|
169
|
-
args: ExprNode[]
|
|
170
|
-
alias?: string
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export interface OperationColumn {
|
|
174
|
-
kind: 'operation'
|
|
148
|
+
export interface DerivedColumn {
|
|
149
|
+
kind: 'derived'
|
|
175
150
|
expr: ExprNode
|
|
176
151
|
alias?: string
|
|
177
152
|
}
|
|
178
153
|
|
|
179
|
-
export type SelectColumn = StarColumn |
|
|
154
|
+
export type SelectColumn = StarColumn | AggregateColumn | DerivedColumn
|
|
180
155
|
|
|
181
156
|
export interface OrderByItem {
|
|
182
157
|
expr: ExprNode
|
|
183
158
|
direction: 'ASC' | 'DESC'
|
|
159
|
+
nulls?: 'FIRST' | 'LAST'
|
|
184
160
|
}
|
|
185
161
|
|
|
186
162
|
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS'
|
|
@@ -205,3 +181,24 @@ export interface ExprCursor {
|
|
|
205
181
|
expectIdentifier(): Token
|
|
206
182
|
parseSubquery?: () => SelectStatement
|
|
207
183
|
}
|
|
184
|
+
|
|
185
|
+
// Tokenizer types
|
|
186
|
+
export type TokenType =
|
|
187
|
+
| 'keyword'
|
|
188
|
+
| 'identifier'
|
|
189
|
+
| 'number'
|
|
190
|
+
| 'string'
|
|
191
|
+
| 'operator'
|
|
192
|
+
| 'comma'
|
|
193
|
+
| 'dot'
|
|
194
|
+
| 'paren'
|
|
195
|
+
| 'semicolon'
|
|
196
|
+
| 'eof'
|
|
197
|
+
|
|
198
|
+
export interface Token {
|
|
199
|
+
type: TokenType
|
|
200
|
+
value: string
|
|
201
|
+
position: number
|
|
202
|
+
numericValue?: number
|
|
203
|
+
originalValue?: string
|
|
204
|
+
}
|
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', 'TRIM'].includes(name)
|
|
16
|
+
return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM'].includes(name)
|
|
17
17
|
}
|