squirreling 0.4.2 → 0.4.3
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/aggregates.js +10 -0
- package/src/execute/expression.js +1 -1
- package/src/parse/comparison.js +198 -0
- package/src/parse/expression.js +3 -219
- package/src/parse/parse.js +12 -21
- package/src/parse/tokenize.js +1 -0
- package/src/types.d.ts +1 -0
- package/src/validation.js +9 -1
package/package.json
CHANGED
|
@@ -16,6 +16,16 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
16
16
|
|
|
17
17
|
if (func === 'COUNT') {
|
|
18
18
|
if (arg.kind === 'star') return rows.length
|
|
19
|
+
if (arg.quantifier === 'distinct') {
|
|
20
|
+
const seen = new Set()
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
23
|
+
if (v !== null && v !== undefined) {
|
|
24
|
+
seen.add(v)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return seen.size
|
|
28
|
+
}
|
|
19
29
|
let count = 0
|
|
20
30
|
for (const row of rows) {
|
|
21
31
|
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
@@ -210,7 +210,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
210
210
|
if (isNaN(num)) return null
|
|
211
211
|
return num
|
|
212
212
|
}
|
|
213
|
-
if (toType === 'TEXT' || toType === 'STRING') {
|
|
213
|
+
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
214
214
|
return String(val)
|
|
215
215
|
}
|
|
216
216
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { isBinaryOp } from '../validation.js'
|
|
2
|
+
import { parseExpression, parsePrimary } from './expression.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @import { ExprCursor, ExprNode } from '../types.js'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {ExprCursor} c
|
|
10
|
+
* @returns {ExprNode}
|
|
11
|
+
*/
|
|
12
|
+
export function parseComparison(c) {
|
|
13
|
+
const left = parsePrimary(c)
|
|
14
|
+
const tok = c.current()
|
|
15
|
+
|
|
16
|
+
// IS [NOT] NULL
|
|
17
|
+
if (tok.type === 'keyword' && tok.value === 'IS') {
|
|
18
|
+
c.consume()
|
|
19
|
+
const notToken = c.current()
|
|
20
|
+
if (notToken.type === 'keyword' && notToken.value === 'NOT') {
|
|
21
|
+
c.consume()
|
|
22
|
+
c.expect('keyword', 'NULL')
|
|
23
|
+
return {
|
|
24
|
+
type: 'unary',
|
|
25
|
+
op: 'IS NOT NULL',
|
|
26
|
+
argument: left,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
c.expect('keyword', 'NULL')
|
|
30
|
+
return {
|
|
31
|
+
type: 'unary',
|
|
32
|
+
op: 'IS NULL',
|
|
33
|
+
argument: left,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// [NOT] LIKE
|
|
38
|
+
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
39
|
+
const nextTok = c.peek(1)
|
|
40
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
41
|
+
c.consume() // NOT
|
|
42
|
+
c.consume() // LIKE
|
|
43
|
+
const right = parsePrimary(c)
|
|
44
|
+
return {
|
|
45
|
+
type: 'unary',
|
|
46
|
+
op: 'NOT',
|
|
47
|
+
argument: {
|
|
48
|
+
type: 'binary',
|
|
49
|
+
op: 'LIKE',
|
|
50
|
+
left,
|
|
51
|
+
right,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (tok.type === 'keyword' && tok.value === 'LIKE') {
|
|
58
|
+
c.consume()
|
|
59
|
+
const right = parsePrimary(c)
|
|
60
|
+
return {
|
|
61
|
+
type: 'binary',
|
|
62
|
+
op: 'LIKE',
|
|
63
|
+
left,
|
|
64
|
+
right,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// [NOT] BETWEEN - convert to range comparison
|
|
69
|
+
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
70
|
+
const nextTok = c.peek(1)
|
|
71
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
72
|
+
c.consume() // NOT
|
|
73
|
+
c.consume() // BETWEEN
|
|
74
|
+
const lower = parsePrimary(c)
|
|
75
|
+
c.expect('keyword', 'AND')
|
|
76
|
+
const upper = parsePrimary(c)
|
|
77
|
+
// NOT BETWEEN -> expr < lower OR expr > upper
|
|
78
|
+
return {
|
|
79
|
+
type: 'binary',
|
|
80
|
+
op: 'OR',
|
|
81
|
+
left: { type: 'binary', op: '<', left, right: lower },
|
|
82
|
+
right: { type: 'binary', op: '>', left, right: upper },
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (tok.type === 'keyword' && tok.value === 'BETWEEN') {
|
|
88
|
+
c.consume()
|
|
89
|
+
const lower = parsePrimary(c)
|
|
90
|
+
c.expect('keyword', 'AND')
|
|
91
|
+
const upper = parsePrimary(c)
|
|
92
|
+
// BETWEEN -> expr >= lower AND expr <= upper
|
|
93
|
+
return {
|
|
94
|
+
type: 'binary',
|
|
95
|
+
op: 'AND',
|
|
96
|
+
left: { type: 'binary', op: '>=', left, right: lower },
|
|
97
|
+
right: { type: 'binary', op: '<=', left, right: upper },
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// [NOT] IN
|
|
102
|
+
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
103
|
+
const nextTok = c.peek(1)
|
|
104
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
|
|
105
|
+
c.consume() // NOT
|
|
106
|
+
c.consume() // IN
|
|
107
|
+
|
|
108
|
+
// Check if it's a subquery or a list of values by peeking ahead
|
|
109
|
+
// parseSubquery expects to consume the opening paren itself
|
|
110
|
+
const parenTok = c.current()
|
|
111
|
+
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
112
|
+
throw new Error('Expected ( after IN')
|
|
113
|
+
}
|
|
114
|
+
const peekTok = c.peek(1)
|
|
115
|
+
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
116
|
+
// Subquery - let parseSubquery handle the parens
|
|
117
|
+
const subquery = c.parseSubquery()
|
|
118
|
+
return {
|
|
119
|
+
type: 'unary',
|
|
120
|
+
op: 'NOT',
|
|
121
|
+
argument: {
|
|
122
|
+
type: 'in',
|
|
123
|
+
expr: left,
|
|
124
|
+
subquery,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Parse list of values - we handle the parens
|
|
129
|
+
c.consume() // '('
|
|
130
|
+
/** @type {ExprNode[]} */
|
|
131
|
+
const values = []
|
|
132
|
+
while (true) {
|
|
133
|
+
values.push(parseExpression(c))
|
|
134
|
+
if (!c.match('comma')) break
|
|
135
|
+
}
|
|
136
|
+
c.expect('paren', ')')
|
|
137
|
+
return {
|
|
138
|
+
type: 'unary',
|
|
139
|
+
op: 'NOT',
|
|
140
|
+
argument: {
|
|
141
|
+
type: 'in valuelist',
|
|
142
|
+
expr: left,
|
|
143
|
+
values,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (tok.type === 'keyword' && tok.value === 'IN') {
|
|
151
|
+
c.consume() // IN
|
|
152
|
+
|
|
153
|
+
// Check if it's a subquery or a list of values by peeking ahead
|
|
154
|
+
// parseSubquery expects to consume the opening paren itself
|
|
155
|
+
const parenTok = c.current()
|
|
156
|
+
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
157
|
+
throw new Error('Expected ( after IN')
|
|
158
|
+
}
|
|
159
|
+
const peekTok = c.peek(1)
|
|
160
|
+
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
161
|
+
// Subquery - let parseSubquery handle the parens
|
|
162
|
+
const subquery = c.parseSubquery()
|
|
163
|
+
return {
|
|
164
|
+
type: 'in',
|
|
165
|
+
expr: left,
|
|
166
|
+
subquery,
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// Parse list of values - we handle the parens
|
|
170
|
+
c.consume() // '('
|
|
171
|
+
/** @type {ExprNode[]} */
|
|
172
|
+
const values = []
|
|
173
|
+
while (true) {
|
|
174
|
+
values.push(parseExpression(c))
|
|
175
|
+
if (!c.match('comma')) break
|
|
176
|
+
}
|
|
177
|
+
c.expect('paren', ')')
|
|
178
|
+
return {
|
|
179
|
+
type: 'in valuelist',
|
|
180
|
+
expr: left,
|
|
181
|
+
values,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (tok.type === 'operator' && isBinaryOp(tok.value)) {
|
|
187
|
+
c.consume()
|
|
188
|
+
const right = parsePrimary(c)
|
|
189
|
+
return {
|
|
190
|
+
type: 'binary',
|
|
191
|
+
op: tok.value,
|
|
192
|
+
left,
|
|
193
|
+
right,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return left
|
|
198
|
+
}
|
package/src/parse/expression.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isAggregateFunc, isStringFunc } from '../validation.js'
|
|
2
|
+
import { parseComparison } from './comparison.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* @import {
|
|
5
|
+
* @import { ExprCursor, ExprNode, WhenClause } from '../types.js'
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -16,7 +17,7 @@ export function parseExpression(c) {
|
|
|
16
17
|
* @param {ExprCursor} c
|
|
17
18
|
* @returns {ExprNode}
|
|
18
19
|
*/
|
|
19
|
-
function parsePrimary(c) {
|
|
20
|
+
export function parsePrimary(c) {
|
|
20
21
|
const tok = c.current()
|
|
21
22
|
|
|
22
23
|
if (tok.type === 'paren' && tok.value === '(') {
|
|
@@ -253,9 +254,6 @@ function parseNot(c) {
|
|
|
253
254
|
const nextTok = c.current()
|
|
254
255
|
if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
|
|
255
256
|
c.consume() // EXISTS
|
|
256
|
-
if (!c.parseSubquery) {
|
|
257
|
-
throw new Error('Subquery parsing not available in this context')
|
|
258
|
-
}
|
|
259
257
|
const subquery = c.parseSubquery()
|
|
260
258
|
return {
|
|
261
259
|
type: 'not exists',
|
|
@@ -271,217 +269,3 @@ function parseNot(c) {
|
|
|
271
269
|
}
|
|
272
270
|
return parseComparison(c)
|
|
273
271
|
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* @param {ExprCursor} c
|
|
277
|
-
* @returns {ExprNode}
|
|
278
|
-
*/
|
|
279
|
-
function parseComparison(c) {
|
|
280
|
-
const left = parsePrimary(c)
|
|
281
|
-
const tok = c.current()
|
|
282
|
-
|
|
283
|
-
// IS [NOT] NULL
|
|
284
|
-
if (tok.type === 'keyword' && tok.value === 'IS') {
|
|
285
|
-
c.consume()
|
|
286
|
-
const notToken = c.current()
|
|
287
|
-
if (notToken.type === 'keyword' && notToken.value === 'NOT') {
|
|
288
|
-
c.consume()
|
|
289
|
-
c.expect('keyword', 'NULL')
|
|
290
|
-
return {
|
|
291
|
-
type: 'unary',
|
|
292
|
-
op: 'IS NOT NULL',
|
|
293
|
-
argument: left,
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
c.expect('keyword', 'NULL')
|
|
297
|
-
return {
|
|
298
|
-
type: 'unary',
|
|
299
|
-
op: 'IS NULL',
|
|
300
|
-
argument: left,
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// [NOT] LIKE
|
|
305
|
-
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
306
|
-
const nextTok = c.peek(1)
|
|
307
|
-
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
308
|
-
c.consume() // NOT
|
|
309
|
-
c.consume() // LIKE
|
|
310
|
-
const right = parsePrimary(c)
|
|
311
|
-
return {
|
|
312
|
-
type: 'unary',
|
|
313
|
-
op: 'NOT',
|
|
314
|
-
argument: {
|
|
315
|
-
type: 'binary',
|
|
316
|
-
op: 'LIKE',
|
|
317
|
-
left,
|
|
318
|
-
right,
|
|
319
|
-
},
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (tok.type === 'keyword' && tok.value === 'LIKE') {
|
|
325
|
-
c.consume()
|
|
326
|
-
const right = parsePrimary(c)
|
|
327
|
-
return {
|
|
328
|
-
type: 'binary',
|
|
329
|
-
op: 'LIKE',
|
|
330
|
-
left,
|
|
331
|
-
right,
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// [NOT] BETWEEN - convert to range comparison
|
|
336
|
-
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
337
|
-
const nextTok = c.peek(1)
|
|
338
|
-
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
339
|
-
c.consume() // NOT
|
|
340
|
-
c.consume() // BETWEEN
|
|
341
|
-
const lower = parsePrimary(c)
|
|
342
|
-
c.expect('keyword', 'AND')
|
|
343
|
-
const upper = parsePrimary(c)
|
|
344
|
-
// NOT BETWEEN -> expr < lower OR expr > upper
|
|
345
|
-
return {
|
|
346
|
-
type: 'binary',
|
|
347
|
-
op: 'OR',
|
|
348
|
-
left: { type: 'binary', op: '<', left, right: lower },
|
|
349
|
-
right: { type: 'binary', op: '>', left, right: upper },
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (tok.type === 'keyword' && tok.value === 'BETWEEN') {
|
|
355
|
-
c.consume()
|
|
356
|
-
const lower = parsePrimary(c)
|
|
357
|
-
c.expect('keyword', 'AND')
|
|
358
|
-
const upper = parsePrimary(c)
|
|
359
|
-
// BETWEEN -> expr >= lower AND expr <= upper
|
|
360
|
-
return {
|
|
361
|
-
type: 'binary',
|
|
362
|
-
op: 'AND',
|
|
363
|
-
left: { type: 'binary', op: '>=', left, right: lower },
|
|
364
|
-
right: { type: 'binary', op: '<=', left, right: upper },
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// [NOT] IN
|
|
369
|
-
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
370
|
-
const nextTok = c.peek(1)
|
|
371
|
-
if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
|
|
372
|
-
c.consume() // NOT
|
|
373
|
-
c.consume() // IN
|
|
374
|
-
|
|
375
|
-
// Check if it's a subquery or a list of values by peeking ahead
|
|
376
|
-
// parseSubquery expects to consume the opening paren itself
|
|
377
|
-
const parenTok = c.current()
|
|
378
|
-
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
379
|
-
throw new Error('Expected ( after IN')
|
|
380
|
-
}
|
|
381
|
-
const peekTok = c.peek(1)
|
|
382
|
-
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
383
|
-
// Subquery - let parseSubquery handle the parens
|
|
384
|
-
if (!c.parseSubquery) {
|
|
385
|
-
throw new Error('Subquery parsing not available in this context')
|
|
386
|
-
}
|
|
387
|
-
const subquery = c.parseSubquery()
|
|
388
|
-
return {
|
|
389
|
-
type: 'unary',
|
|
390
|
-
op: 'NOT',
|
|
391
|
-
argument: {
|
|
392
|
-
type: 'in',
|
|
393
|
-
expr: left,
|
|
394
|
-
subquery,
|
|
395
|
-
},
|
|
396
|
-
}
|
|
397
|
-
} else {
|
|
398
|
-
// Parse list of values - we handle the parens
|
|
399
|
-
c.consume() // '('
|
|
400
|
-
/** @type {ExprNode[]} */
|
|
401
|
-
const values = []
|
|
402
|
-
while (true) {
|
|
403
|
-
values.push(parseExpression(c))
|
|
404
|
-
if (!c.match('comma')) break
|
|
405
|
-
}
|
|
406
|
-
c.expect('paren', ')')
|
|
407
|
-
return {
|
|
408
|
-
type: 'unary',
|
|
409
|
-
op: 'NOT',
|
|
410
|
-
argument: {
|
|
411
|
-
type: 'in valuelist',
|
|
412
|
-
expr: left,
|
|
413
|
-
values,
|
|
414
|
-
},
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (tok.type === 'keyword' && tok.value === 'IN') {
|
|
421
|
-
c.consume() // IN
|
|
422
|
-
|
|
423
|
-
// Check if it's a subquery or a list of values by peeking ahead
|
|
424
|
-
// parseSubquery expects to consume the opening paren itself
|
|
425
|
-
const parenTok = c.current()
|
|
426
|
-
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
427
|
-
throw new Error('Expected ( after IN')
|
|
428
|
-
}
|
|
429
|
-
const peekTok = c.peek(1)
|
|
430
|
-
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
431
|
-
// Subquery - let parseSubquery handle the parens
|
|
432
|
-
if (!c.parseSubquery) {
|
|
433
|
-
throw new Error('Subquery parsing not available in this context')
|
|
434
|
-
}
|
|
435
|
-
const subquery = c.parseSubquery()
|
|
436
|
-
return {
|
|
437
|
-
type: 'in',
|
|
438
|
-
expr: left,
|
|
439
|
-
subquery,
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
// Parse list of values - we handle the parens
|
|
443
|
-
c.consume() // '('
|
|
444
|
-
/** @type {ExprNode[]} */
|
|
445
|
-
const values = []
|
|
446
|
-
while (true) {
|
|
447
|
-
values.push(parseExpression(c))
|
|
448
|
-
if (!c.match('comma')) break
|
|
449
|
-
}
|
|
450
|
-
c.expect('paren', ')')
|
|
451
|
-
return {
|
|
452
|
-
type: 'in valuelist',
|
|
453
|
-
expr: left,
|
|
454
|
-
values,
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (tok.type === 'operator' && isComparisonOperator(tok.value)) {
|
|
460
|
-
c.consume()
|
|
461
|
-
const right = parsePrimary(c)
|
|
462
|
-
return {
|
|
463
|
-
type: 'binary',
|
|
464
|
-
op: tok.value,
|
|
465
|
-
left,
|
|
466
|
-
right,
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return left
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* @param {string} op
|
|
475
|
-
* @returns {op is BinaryOp}
|
|
476
|
-
*/
|
|
477
|
-
function isComparisonOperator(op) {
|
|
478
|
-
return (
|
|
479
|
-
op === '=' ||
|
|
480
|
-
op === '!=' ||
|
|
481
|
-
op === '<>' ||
|
|
482
|
-
op === '<' ||
|
|
483
|
-
op === '>' ||
|
|
484
|
-
op === '<=' ||
|
|
485
|
-
op === '>='
|
|
486
|
-
)
|
|
487
|
-
}
|
package/src/parse/parse.js
CHANGED
|
@@ -229,31 +229,22 @@ function parseAggregateItem(state, func) {
|
|
|
229
229
|
if (cur.type === 'operator' && cur.value === '*') {
|
|
230
230
|
consume(state)
|
|
231
231
|
arg = { kind: 'star' }
|
|
232
|
-
} else if (cur.type === 'identifier' && cur.value === 'CAST') {
|
|
233
|
-
// Handle CAST inside aggregate: SUM(CAST(x AS type))
|
|
234
|
-
expectIdentifier(state) // consume CAST
|
|
235
|
-
expect(state, 'paren', '(')
|
|
236
|
-
const cursor = createExprCursor(state)
|
|
237
|
-
const expr = parseExpression(cursor)
|
|
238
|
-
expect(state, 'keyword', 'AS')
|
|
239
|
-
const toType = expectIdentifier(state).value
|
|
240
|
-
expect(state, 'paren', ')')
|
|
241
|
-
arg = {
|
|
242
|
-
kind: 'expression',
|
|
243
|
-
expr: { type: 'cast', expr, toType },
|
|
244
|
-
}
|
|
245
232
|
} else {
|
|
246
|
-
|
|
247
|
-
let
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
233
|
+
/** @type {'all' | 'distinct'} */
|
|
234
|
+
let quantifier = 'all'
|
|
235
|
+
if (cur.type === 'keyword' && cur.value === 'ALL') {
|
|
236
|
+
consume(state) // consume ALL
|
|
237
|
+
} else if (cur.type === 'keyword' && cur.value === 'DISTINCT') {
|
|
238
|
+
consume(state)
|
|
239
|
+
quantifier = 'distinct'
|
|
253
240
|
}
|
|
241
|
+
|
|
242
|
+
const cursor = createExprCursor(state)
|
|
243
|
+
const expr = parseExpression(cursor)
|
|
254
244
|
arg = {
|
|
255
245
|
kind: 'expression',
|
|
256
|
-
expr
|
|
246
|
+
expr,
|
|
247
|
+
quantifier,
|
|
257
248
|
}
|
|
258
249
|
}
|
|
259
250
|
|
package/src/parse/tokenize.js
CHANGED
package/src/types.d.ts
CHANGED
package/src/validation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* @import {AggregateFunc, StringFunc} from './types.js'
|
|
3
|
+
* @import {AggregateFunc, BinaryOp, StringFunc} from './types.js'
|
|
4
4
|
* @param {string} name
|
|
5
5
|
* @returns {name is AggregateFunc}
|
|
6
6
|
*/
|
|
@@ -15,3 +15,11 @@ export function isAggregateFunc(name) {
|
|
|
15
15
|
export function isStringFunc(name) {
|
|
16
16
|
return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE', 'RANDOM', 'RAND'].includes(name)
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} op
|
|
21
|
+
* @returns {op is BinaryOp}
|
|
22
|
+
*/
|
|
23
|
+
export function isBinaryOp(op) {
|
|
24
|
+
return ['=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
25
|
+
}
|