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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -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
  }
@@ -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
- throw new Error(
127
- 'Unexpected token in expression at position ' +
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)
@@ -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} sql
21
+ * @param {string} query
22
22
  * @returns {SelectStatement}
23
23
  */
24
- export function parseSql(sql) {
25
- const tokens = tokenize(sql)
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
- const from = expectIdentifier(state).value // table name
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
- return new Error(`Expected ${expected}${after} at position ${tok.position}`)
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
  }
@@ -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('Invalid number at position ' + pos + ': ' + text + peek())
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('Invalid number at position ' + pos + ': ' + text)
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('Unterminated string literal starting at position ' + pos)
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('Unterminated identifier starting at position ' + pos)
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('Expected SELECT at position ' + pos)
289
+ throw new Error(`Expected SELECT but found "${ch}" at position ${pos}`)
289
290
  }
290
- throw new Error('Unexpected character at position ' + pos + ': ' + ch)
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
  }