squirreling 0.2.4 → 0.2.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.
- package/package.json +1 -1
- package/src/execute/execute.js +49 -49
- package/src/execute/expression.js +33 -4
- package/src/parse/expression.js +52 -6
- package/src/parse/parse.js +42 -76
- package/src/parse/tokenize.js +1 -0
- package/src/types.d.ts +40 -37
- 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
|
}
|
|
@@ -224,5 +224,34 @@ export function evaluateExpr(node, row) {
|
|
|
224
224
|
throw new Error('WHERE NOT EXISTS with subqueries is not yet supported.')
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// CASE expressions
|
|
228
|
+
if (node.type === 'case') {
|
|
229
|
+
// For simple CASE: evaluate the case expression once
|
|
230
|
+
const caseValue = node.caseExpr ? evaluateExpr(node.caseExpr, row) : undefined
|
|
231
|
+
|
|
232
|
+
// Iterate through WHEN clauses
|
|
233
|
+
for (const whenClause of node.whenClauses) {
|
|
234
|
+
let conditionResult
|
|
235
|
+
if (caseValue !== undefined) {
|
|
236
|
+
// Simple CASE: compare caseValue with condition
|
|
237
|
+
const whenValue = evaluateExpr(whenClause.condition, row)
|
|
238
|
+
conditionResult = caseValue === whenValue
|
|
239
|
+
} else {
|
|
240
|
+
// Searched CASE: evaluate condition as boolean
|
|
241
|
+
conditionResult = evaluateExpr(whenClause.condition, row)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (conditionResult) {
|
|
245
|
+
return evaluateExpr(whenClause.result, row)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// No WHEN clause matched, return ELSE result or NULL
|
|
250
|
+
if (node.elseResult) {
|
|
251
|
+
return evaluateExpr(node.elseResult, row)
|
|
252
|
+
}
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
227
256
|
throw new Error('Unknown expression node type ' + node.type)
|
|
228
257
|
}
|
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 === '(') {
|
|
@@ -48,7 +47,12 @@ export function parsePrimary(c) {
|
|
|
48
47
|
// function call
|
|
49
48
|
if (next.type === 'paren' && next.value === '(') {
|
|
50
49
|
const funcName = tok.value
|
|
51
|
-
|
|
50
|
+
|
|
51
|
+
// validate function names
|
|
52
|
+
if (!isStringFunc(funcName) && !isAggregateFunc(funcName)) {
|
|
53
|
+
throw new Error(`Unknown function "${funcName}" at position ${tok.position}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
c.consume() // function name
|
|
53
57
|
c.consume() // '('
|
|
54
58
|
|
|
@@ -137,6 +141,48 @@ export function parsePrimary(c) {
|
|
|
137
141
|
subquery,
|
|
138
142
|
}
|
|
139
143
|
}
|
|
144
|
+
if (tok.value === 'CASE') {
|
|
145
|
+
c.consume() // CASE
|
|
146
|
+
|
|
147
|
+
// Check if it's simple CASE (CASE expr WHEN ...) or searched CASE (CASE WHEN ...)
|
|
148
|
+
/** @type {import('../types.js').ExprNode | undefined} */
|
|
149
|
+
let caseExpr
|
|
150
|
+
const nextTok = c.current()
|
|
151
|
+
if (nextTok.type !== 'keyword' || nextTok.value !== 'WHEN') {
|
|
152
|
+
// Simple CASE: parse the case expression
|
|
153
|
+
caseExpr = parseExpression(c)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Parse WHEN clauses
|
|
157
|
+
/** @type {import('../types.js').WhenClause[]} */
|
|
158
|
+
const whenClauses = []
|
|
159
|
+
while (c.match('keyword', 'WHEN')) {
|
|
160
|
+
const condition = parseExpression(c)
|
|
161
|
+
c.expect('keyword', 'THEN')
|
|
162
|
+
const result = parseExpression(c)
|
|
163
|
+
whenClauses.push({ condition, result })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (whenClauses.length === 0) {
|
|
167
|
+
throw new Error('CASE expression must have at least one WHEN clause')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Parse optional ELSE clause
|
|
171
|
+
/** @type {import('../types.js').ExprNode | undefined} */
|
|
172
|
+
let elseResult
|
|
173
|
+
if (c.match('keyword', 'ELSE')) {
|
|
174
|
+
elseResult = parseExpression(c)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
c.expect('keyword', 'END')
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
type: 'case',
|
|
181
|
+
caseExpr,
|
|
182
|
+
whenClauses,
|
|
183
|
+
elseResult,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
if (tok.type === 'operator' && tok.value === '-') {
|
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' })
|
|
@@ -162,60 +177,24 @@ function parseSelectList(state) {
|
|
|
162
177
|
function parseSelectItem(state) {
|
|
163
178
|
const tok = current(state)
|
|
164
179
|
|
|
165
|
-
if (tok.type
|
|
180
|
+
if (tok.type === 'keyword' && tok.value !== 'CASE' || tok.type === 'eof') {
|
|
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'
|
|
@@ -125,6 +105,18 @@ export interface ExistsNode {
|
|
|
125
105
|
subquery: SelectStatement
|
|
126
106
|
}
|
|
127
107
|
|
|
108
|
+
export interface WhenClause {
|
|
109
|
+
condition: ExprNode
|
|
110
|
+
result: ExprNode
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CaseNode {
|
|
114
|
+
type: 'case'
|
|
115
|
+
caseExpr?: ExprNode
|
|
116
|
+
whenClauses: WhenClause[]
|
|
117
|
+
elseResult?: ExprNode
|
|
118
|
+
}
|
|
119
|
+
|
|
128
120
|
export type ExprNode =
|
|
129
121
|
| LiteralNode
|
|
130
122
|
| IdentifierNode
|
|
@@ -136,21 +128,17 @@ export type ExprNode =
|
|
|
136
128
|
| InSubqueryNode
|
|
137
129
|
| InValuesNode
|
|
138
130
|
| ExistsNode
|
|
131
|
+
| CaseNode
|
|
139
132
|
|
|
140
133
|
export interface StarColumn {
|
|
141
134
|
kind: 'star'
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export interface SimpleColumn {
|
|
146
|
-
kind: 'column'
|
|
147
|
-
column: string
|
|
135
|
+
table?: string
|
|
148
136
|
alias?: string
|
|
149
137
|
}
|
|
150
138
|
|
|
151
139
|
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
|
|
152
140
|
|
|
153
|
-
export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'TRIM'
|
|
141
|
+
export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM'
|
|
154
142
|
|
|
155
143
|
export interface AggregateArgStar {
|
|
156
144
|
kind: 'star'
|
|
@@ -170,24 +158,18 @@ export interface AggregateColumn {
|
|
|
170
158
|
alias?: string
|
|
171
159
|
}
|
|
172
160
|
|
|
173
|
-
export interface
|
|
174
|
-
kind: '
|
|
175
|
-
func: StringFunc
|
|
176
|
-
args: ExprNode[]
|
|
177
|
-
alias?: string
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export interface OperationColumn {
|
|
181
|
-
kind: 'operation'
|
|
161
|
+
export interface DerivedColumn {
|
|
162
|
+
kind: 'derived'
|
|
182
163
|
expr: ExprNode
|
|
183
164
|
alias?: string
|
|
184
165
|
}
|
|
185
166
|
|
|
186
|
-
export type SelectColumn = StarColumn |
|
|
167
|
+
export type SelectColumn = StarColumn | AggregateColumn | DerivedColumn
|
|
187
168
|
|
|
188
169
|
export interface OrderByItem {
|
|
189
170
|
expr: ExprNode
|
|
190
171
|
direction: 'ASC' | 'DESC'
|
|
172
|
+
nulls?: 'FIRST' | 'LAST'
|
|
191
173
|
}
|
|
192
174
|
|
|
193
175
|
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS'
|
|
@@ -212,3 +194,24 @@ export interface ExprCursor {
|
|
|
212
194
|
expectIdentifier(): Token
|
|
213
195
|
parseSubquery?: () => SelectStatement
|
|
214
196
|
}
|
|
197
|
+
|
|
198
|
+
// Tokenizer types
|
|
199
|
+
export type TokenType =
|
|
200
|
+
| 'keyword'
|
|
201
|
+
| 'identifier'
|
|
202
|
+
| 'number'
|
|
203
|
+
| 'string'
|
|
204
|
+
| 'operator'
|
|
205
|
+
| 'comma'
|
|
206
|
+
| 'dot'
|
|
207
|
+
| 'paren'
|
|
208
|
+
| 'semicolon'
|
|
209
|
+
| 'eof'
|
|
210
|
+
|
|
211
|
+
export interface Token {
|
|
212
|
+
type: TokenType
|
|
213
|
+
value: string
|
|
214
|
+
position: number
|
|
215
|
+
numericValue?: number
|
|
216
|
+
originalValue?: string
|
|
217
|
+
}
|
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
|
}
|