squirreling 0.2.2 → 0.2.4
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 +5 -0
- package/src/execute/expression.js +34 -0
- package/src/parse/expression.js +125 -9
- package/src/parse/parse.js +41 -6
- package/src/parse/tokenize.js +7 -6
- package/src/types.d.ts +28 -1
package/package.json
CHANGED
package/src/execute/execute.js
CHANGED
|
@@ -142,6 +142,11 @@ function evaluateSelectAst(select, dataSource) {
|
|
|
142
142
|
throw new Error('JOIN is not supported')
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// Check for unsupported subquery in FROM clause
|
|
146
|
+
if (typeof select.from !== 'string') {
|
|
147
|
+
throw new Error('Subquery in FROM clause is not supported')
|
|
148
|
+
}
|
|
149
|
+
|
|
145
150
|
// SQL priority: from, where, group by, having, select, order by, offset, limit
|
|
146
151
|
|
|
147
152
|
// WHERE clause filtering
|
|
@@ -190,5 +190,39 @@ 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
|
+
|
|
211
|
+
// IN and NOT IN with subqueries
|
|
212
|
+
if (node.type === 'in') {
|
|
213
|
+
throw new Error('WHERE IN with subqueries is not yet supported.')
|
|
214
|
+
}
|
|
215
|
+
if (node.type === 'not in') {
|
|
216
|
+
throw new Error('WHERE NOT IN with subqueries is not yet supported.')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// EXISTS and NOT EXISTS with subqueries
|
|
220
|
+
if (node.type === 'exists') {
|
|
221
|
+
throw new Error('WHERE EXISTS with subqueries is not yet supported.')
|
|
222
|
+
}
|
|
223
|
+
if (node.type === 'not exists') {
|
|
224
|
+
throw new Error('WHERE NOT EXISTS with subqueries is not yet supported.')
|
|
225
|
+
}
|
|
226
|
+
|
|
193
227
|
throw new Error('Unknown expression node type ' + node.type)
|
|
194
228
|
}
|
package/src/parse/expression.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { ExprCursor, ExprNode, BinaryOp } from '../types.js'
|
|
2
|
+
* @import { ExprCursor, ExprNode, BinaryOp, SelectStatement } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -30,6 +30,21 @@ export function parsePrimary(c) {
|
|
|
30
30
|
if (tok.type === 'identifier') {
|
|
31
31
|
const next = c.peek(1)
|
|
32
32
|
|
|
33
|
+
// CAST expression
|
|
34
|
+
if (tok.value === 'CAST' && next.type === 'paren' && next.value === '(') {
|
|
35
|
+
c.consume() // CAST
|
|
36
|
+
c.consume() // '('
|
|
37
|
+
const expr = parseExpression(c)
|
|
38
|
+
c.expect('keyword', 'AS')
|
|
39
|
+
const typeTok = c.expectIdentifier()
|
|
40
|
+
c.expect('paren', ')')
|
|
41
|
+
return {
|
|
42
|
+
type: 'cast',
|
|
43
|
+
expr,
|
|
44
|
+
toType: typeTok.value,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
// function call
|
|
34
49
|
if (next.type === 'paren' && next.value === '(') {
|
|
35
50
|
const funcName = tok.value
|
|
@@ -111,6 +126,17 @@ export function parsePrimary(c) {
|
|
|
111
126
|
c.consume()
|
|
112
127
|
return { type: 'literal', value: null }
|
|
113
128
|
}
|
|
129
|
+
if (tok.value === 'EXISTS') {
|
|
130
|
+
c.consume() // EXISTS
|
|
131
|
+
if (!c.parseSubquery) {
|
|
132
|
+
throw new Error('Subquery parsing not available in this context')
|
|
133
|
+
}
|
|
134
|
+
const subquery = c.parseSubquery()
|
|
135
|
+
return {
|
|
136
|
+
type: 'exists',
|
|
137
|
+
subquery,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
114
140
|
}
|
|
115
141
|
|
|
116
142
|
if (tok.type === 'operator' && tok.value === '-') {
|
|
@@ -123,14 +149,8 @@ export function parsePrimary(c) {
|
|
|
123
149
|
}
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
tok.position +
|
|
129
|
-
': ' +
|
|
130
|
-
tok.type +
|
|
131
|
-
' ' +
|
|
132
|
-
tok.value
|
|
133
|
-
)
|
|
152
|
+
const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
|
|
153
|
+
throw new Error(`Expected expression but found ${found} at position ${tok.position}`)
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
/**
|
|
@@ -175,6 +195,19 @@ function parseAnd(c) {
|
|
|
175
195
|
*/
|
|
176
196
|
function parseNot(c) {
|
|
177
197
|
if (c.match('keyword', 'NOT')) {
|
|
198
|
+
// Check for NOT EXISTS
|
|
199
|
+
const nextTok = c.current()
|
|
200
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
|
|
201
|
+
c.consume() // EXISTS
|
|
202
|
+
if (!c.parseSubquery) {
|
|
203
|
+
throw new Error('Subquery parsing not available in this context')
|
|
204
|
+
}
|
|
205
|
+
const subquery = c.parseSubquery()
|
|
206
|
+
return {
|
|
207
|
+
type: 'not exists',
|
|
208
|
+
subquery,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
178
211
|
const argument = parseNot(c)
|
|
179
212
|
return {
|
|
180
213
|
type: 'unary',
|
|
@@ -257,6 +290,89 @@ function parseComparison(c) {
|
|
|
257
290
|
}
|
|
258
291
|
}
|
|
259
292
|
|
|
293
|
+
// [NOT] IN
|
|
294
|
+
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
295
|
+
const nextTok = c.peek(1)
|
|
296
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
|
|
297
|
+
c.consume() // NOT
|
|
298
|
+
c.consume() // IN
|
|
299
|
+
|
|
300
|
+
// Check if it's a subquery or a list of values by peeking ahead
|
|
301
|
+
// parseSubquery expects to consume the opening paren itself
|
|
302
|
+
const parenTok = c.current()
|
|
303
|
+
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
304
|
+
throw new Error('Expected ( after IN')
|
|
305
|
+
}
|
|
306
|
+
const peekTok = c.peek(1)
|
|
307
|
+
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
308
|
+
// Subquery - let parseSubquery handle the parens
|
|
309
|
+
if (!c.parseSubquery) {
|
|
310
|
+
throw new Error('Subquery parsing not available in this context')
|
|
311
|
+
}
|
|
312
|
+
const subquery = c.parseSubquery()
|
|
313
|
+
return {
|
|
314
|
+
type: 'not in',
|
|
315
|
+
expr: left,
|
|
316
|
+
subquery,
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
// Parse list of values - we handle the parens
|
|
320
|
+
c.consume() // '('
|
|
321
|
+
/** @type {ExprNode[]} */
|
|
322
|
+
const values = []
|
|
323
|
+
while (true) {
|
|
324
|
+
values.push(parseExpression(c))
|
|
325
|
+
if (!c.match('comma')) break
|
|
326
|
+
}
|
|
327
|
+
c.expect('paren', ')')
|
|
328
|
+
return {
|
|
329
|
+
type: 'not in valuelist',
|
|
330
|
+
expr: left,
|
|
331
|
+
values,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (tok.type === 'keyword' && tok.value === 'IN') {
|
|
338
|
+
c.consume() // IN
|
|
339
|
+
|
|
340
|
+
// Check if it's a subquery or a list of values by peeking ahead
|
|
341
|
+
// parseSubquery expects to consume the opening paren itself
|
|
342
|
+
const parenTok = c.current()
|
|
343
|
+
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
344
|
+
throw new Error('Expected ( after IN')
|
|
345
|
+
}
|
|
346
|
+
const peekTok = c.peek(1)
|
|
347
|
+
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
348
|
+
// Subquery - let parseSubquery handle the parens
|
|
349
|
+
if (!c.parseSubquery) {
|
|
350
|
+
throw new Error('Subquery parsing not available in this context')
|
|
351
|
+
}
|
|
352
|
+
const subquery = c.parseSubquery()
|
|
353
|
+
return {
|
|
354
|
+
type: 'in',
|
|
355
|
+
expr: left,
|
|
356
|
+
subquery,
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
// Parse list of values - we handle the parens
|
|
360
|
+
c.consume() // '('
|
|
361
|
+
/** @type {ExprNode[]} */
|
|
362
|
+
const values = []
|
|
363
|
+
while (true) {
|
|
364
|
+
values.push(parseExpression(c))
|
|
365
|
+
if (!c.match('comma')) break
|
|
366
|
+
}
|
|
367
|
+
c.expect('paren', ')')
|
|
368
|
+
return {
|
|
369
|
+
type: 'in valuelist',
|
|
370
|
+
expr: left,
|
|
371
|
+
values,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
260
376
|
if (tok.type === 'operator' && isComparisonOperator(tok.value)) {
|
|
261
377
|
c.consume()
|
|
262
378
|
const right = parsePrimary(c)
|
package/src/parse/parse.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, StringFunc, Token, TokenType } from '../types.js'
|
|
2
|
+
* @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, StringFunc, Token, TokenType } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { tokenize } from './tokenize.js'
|
|
@@ -18,11 +18,11 @@ const RESERVED_AFTER_COLUMN = new Set([
|
|
|
18
18
|
])
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @param {string}
|
|
21
|
+
* @param {string} query
|
|
22
22
|
* @returns {SelectStatement}
|
|
23
23
|
*/
|
|
24
|
-
export function parseSql(
|
|
25
|
-
const tokens = tokenize(
|
|
24
|
+
export function parseSql(query) {
|
|
25
|
+
const tokens = tokenize(query)
|
|
26
26
|
/** @type {ParserState} */
|
|
27
27
|
const state = { tokens, pos: 0 }
|
|
28
28
|
const select = parseSelectInternal(state)
|
|
@@ -123,6 +123,12 @@ function createExprCursor(state) {
|
|
|
123
123
|
match: (type, value) => match(state, type, value),
|
|
124
124
|
expect: (type, value) => expect(state, type, value),
|
|
125
125
|
expectIdentifier: () => expectIdentifier(state),
|
|
126
|
+
parseSubquery: () => {
|
|
127
|
+
expect(state, 'paren', '(')
|
|
128
|
+
const query = parseSelectInternal(state)
|
|
129
|
+
expect(state, 'paren', ')')
|
|
130
|
+
return query
|
|
131
|
+
},
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
|
|
@@ -384,6 +390,24 @@ function parseJoins(state) {
|
|
|
384
390
|
return joins
|
|
385
391
|
}
|
|
386
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Parses a subquery in parentheses with an alias
|
|
395
|
+
* @param {ParserState} state
|
|
396
|
+
* @returns {FromSubquery}
|
|
397
|
+
*/
|
|
398
|
+
function parseSubquery(state) {
|
|
399
|
+
expect(state, 'paren', '(')
|
|
400
|
+
const query = parseSelectInternal(state)
|
|
401
|
+
expect(state, 'paren', ')')
|
|
402
|
+
expect(state, 'keyword', 'AS')
|
|
403
|
+
const aliasTok = expectIdentifier(state)
|
|
404
|
+
return {
|
|
405
|
+
kind: 'subquery',
|
|
406
|
+
query,
|
|
407
|
+
alias: aliasTok.value,
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
387
411
|
/**
|
|
388
412
|
* @param {ParserState} state
|
|
389
413
|
* @returns {SelectStatement}
|
|
@@ -399,7 +423,17 @@ function parseSelectInternal(state) {
|
|
|
399
423
|
const columns = parseSelectList(state)
|
|
400
424
|
|
|
401
425
|
expect(state, 'keyword', 'FROM')
|
|
402
|
-
|
|
426
|
+
|
|
427
|
+
// Check if it's a subquery or table name
|
|
428
|
+
let from
|
|
429
|
+
const tok = current(state)
|
|
430
|
+
if (tok.type === 'paren' && tok.value === '(') {
|
|
431
|
+
// Subquery: SELECT * FROM (SELECT ...) AS alias
|
|
432
|
+
from = parseSubquery(state)
|
|
433
|
+
} else {
|
|
434
|
+
// Simple table name: SELECT * FROM users
|
|
435
|
+
from = expectIdentifier(state).value
|
|
436
|
+
}
|
|
403
437
|
|
|
404
438
|
// Parse JOIN clauses
|
|
405
439
|
const joins = parseJoins(state)
|
|
@@ -521,5 +555,6 @@ function parseError(state, expected) {
|
|
|
521
555
|
const tok = current(state)
|
|
522
556
|
const prevToken = state.tokens[state.pos - 1]
|
|
523
557
|
const after = prevToken ? ` after "${prevToken.originalValue ?? prevToken.value}"` : ''
|
|
524
|
-
|
|
558
|
+
const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
|
|
559
|
+
return new Error(`Expected ${expected}${after} but found ${found} at position ${tok.position}`)
|
|
525
560
|
}
|
package/src/parse/tokenize.js
CHANGED
|
@@ -25,6 +25,7 @@ const KEYWORDS = new Set([
|
|
|
25
25
|
'NULL',
|
|
26
26
|
'LIKE',
|
|
27
27
|
'IN',
|
|
28
|
+
'EXISTS',
|
|
28
29
|
'BETWEEN',
|
|
29
30
|
'CASE',
|
|
30
31
|
'WHEN',
|
|
@@ -122,11 +123,11 @@ export function tokenize(sql) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
if (isAlpha(peek())) {
|
|
125
|
-
throw new Error(
|
|
126
|
+
throw new Error(`Invalid number at position ${pos}: ${text}${peek()}`)
|
|
126
127
|
}
|
|
127
128
|
const num = parseFloat(text)
|
|
128
129
|
if (isNaN(num)) {
|
|
129
|
-
throw new Error(
|
|
130
|
+
throw new Error(`Invalid number at position ${pos}: ${text}`)
|
|
130
131
|
}
|
|
131
132
|
tokens.push({
|
|
132
133
|
type: 'number',
|
|
@@ -167,7 +168,7 @@ export function tokenize(sql) {
|
|
|
167
168
|
let text = ''
|
|
168
169
|
while (i <= length) {
|
|
169
170
|
if (i === length) {
|
|
170
|
-
throw new Error(
|
|
171
|
+
throw new Error(`Unterminated string literal starting at position ${pos}`)
|
|
171
172
|
}
|
|
172
173
|
const c = nextChar()
|
|
173
174
|
if (c === quote) {
|
|
@@ -195,7 +196,7 @@ export function tokenize(sql) {
|
|
|
195
196
|
let text = ''
|
|
196
197
|
while (i <= length) {
|
|
197
198
|
if (i === length) {
|
|
198
|
-
throw new Error(
|
|
199
|
+
throw new Error(`Unterminated identifier starting at position ${pos}`)
|
|
199
200
|
}
|
|
200
201
|
const c = nextChar()
|
|
201
202
|
if (c === quote) {
|
|
@@ -285,9 +286,9 @@ export function tokenize(sql) {
|
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
if (tokens.length === 0) {
|
|
288
|
-
throw new Error(
|
|
289
|
+
throw new Error(`Expected SELECT but found "${ch}" at position ${pos}`)
|
|
289
290
|
}
|
|
290
|
-
throw new Error(
|
|
291
|
+
throw new Error(`Unexpected character "${ch}" at position ${pos}`)
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
tokens.push({
|
package/src/types.d.ts
CHANGED
|
@@ -15,10 +15,16 @@ export interface ExecuteSqlOptions {
|
|
|
15
15
|
|
|
16
16
|
export type SqlPrimitive = string | number | bigint | boolean | null
|
|
17
17
|
|
|
18
|
+
export interface FromSubquery {
|
|
19
|
+
kind: 'subquery'
|
|
20
|
+
query: SelectStatement
|
|
21
|
+
alias: string
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
export interface SelectStatement {
|
|
19
25
|
distinct: boolean
|
|
20
26
|
columns: SelectColumn[]
|
|
21
|
-
from?: string
|
|
27
|
+
from?: string | FromSubquery
|
|
22
28
|
joins: JoinClause[]
|
|
23
29
|
where?: ExprNode
|
|
24
30
|
groupBy: ExprNode[]
|
|
@@ -102,6 +108,23 @@ export interface BetweenNode {
|
|
|
102
108
|
upper: ExprNode
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
export interface InSubqueryNode {
|
|
112
|
+
type: 'in' | 'not in'
|
|
113
|
+
expr: ExprNode
|
|
114
|
+
subquery: SelectStatement
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface InValuesNode {
|
|
118
|
+
type: 'in valuelist' | 'not in valuelist'
|
|
119
|
+
expr: ExprNode
|
|
120
|
+
values: ExprNode[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ExistsNode {
|
|
124
|
+
type: 'exists' | 'not exists'
|
|
125
|
+
subquery: SelectStatement
|
|
126
|
+
}
|
|
127
|
+
|
|
105
128
|
export type ExprNode =
|
|
106
129
|
| LiteralNode
|
|
107
130
|
| IdentifierNode
|
|
@@ -110,6 +133,9 @@ export type ExprNode =
|
|
|
110
133
|
| FunctionNode
|
|
111
134
|
| CastNode
|
|
112
135
|
| BetweenNode
|
|
136
|
+
| InSubqueryNode
|
|
137
|
+
| InValuesNode
|
|
138
|
+
| ExistsNode
|
|
113
139
|
|
|
114
140
|
export interface StarColumn {
|
|
115
141
|
kind: 'star'
|
|
@@ -184,4 +210,5 @@ export interface ExprCursor {
|
|
|
184
210
|
match(type: TokenType, value?: string): boolean
|
|
185
211
|
expect(type: TokenType, value: string): Token
|
|
186
212
|
expectIdentifier(): Token
|
|
213
|
+
parseSubquery?: () => SelectStatement
|
|
187
214
|
}
|