squirreling 0.2.2 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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,21 @@ 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 subqueries
194
+ if (node.type === 'in') {
195
+ throw new Error('WHERE IN with subqueries is not yet supported.')
196
+ }
197
+ if (node.type === 'not in') {
198
+ throw new Error('WHERE NOT IN with subqueries is not yet supported.')
199
+ }
200
+
201
+ // EXISTS and NOT EXISTS with subqueries
202
+ if (node.type === 'exists') {
203
+ throw new Error('WHERE EXISTS with subqueries is not yet supported.')
204
+ }
205
+ if (node.type === 'not exists') {
206
+ throw new Error('WHERE NOT EXISTS with subqueries is not yet supported.')
207
+ }
208
+
193
209
  throw new Error('Unknown expression node type ' + node.type)
194
210
  }
@@ -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
  /**
@@ -111,6 +111,17 @@ export function parsePrimary(c) {
111
111
  c.consume()
112
112
  return { type: 'literal', value: null }
113
113
  }
114
+ if (tok.value === 'EXISTS') {
115
+ c.consume() // EXISTS
116
+ if (!c.parseSubquery) {
117
+ throw new Error('Subquery parsing not available in this context')
118
+ }
119
+ const subquery = c.parseSubquery()
120
+ return {
121
+ type: 'exists',
122
+ subquery,
123
+ }
124
+ }
114
125
  }
115
126
 
116
127
  if (tok.type === 'operator' && tok.value === '-') {
@@ -123,14 +134,8 @@ export function parsePrimary(c) {
123
134
  }
124
135
  }
125
136
 
126
- throw new Error(
127
- 'Unexpected token in expression at position ' +
128
- tok.position +
129
- ': ' +
130
- tok.type +
131
- ' ' +
132
- tok.value
133
- )
137
+ const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
138
+ throw new Error(`Expected expression but found ${found} at position ${tok.position}`)
134
139
  }
135
140
 
136
141
  /**
@@ -175,6 +180,19 @@ function parseAnd(c) {
175
180
  */
176
181
  function parseNot(c) {
177
182
  if (c.match('keyword', 'NOT')) {
183
+ // Check for NOT EXISTS
184
+ const nextTok = c.current()
185
+ if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
186
+ c.consume() // EXISTS
187
+ if (!c.parseSubquery) {
188
+ throw new Error('Subquery parsing not available in this context')
189
+ }
190
+ const subquery = c.parseSubquery()
191
+ return {
192
+ type: 'not exists',
193
+ subquery,
194
+ }
195
+ }
178
196
  const argument = parseNot(c)
179
197
  return {
180
198
  type: 'unary',
@@ -257,6 +275,37 @@ function parseComparison(c) {
257
275
  }
258
276
  }
259
277
 
278
+ // [NOT] IN
279
+ if (tok.type === 'keyword' && tok.value === 'NOT') {
280
+ const nextTok = c.peek(1)
281
+ if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
282
+ c.consume() // NOT
283
+ c.consume() // IN
284
+ if (!c.parseSubquery) {
285
+ throw new Error('Subquery parsing not available in this context')
286
+ }
287
+ const subquery = c.parseSubquery()
288
+ return {
289
+ type: 'not in',
290
+ expr: left,
291
+ subquery,
292
+ }
293
+ }
294
+ }
295
+
296
+ if (tok.type === 'keyword' && tok.value === 'IN') {
297
+ c.consume() // IN
298
+ if (!c.parseSubquery) {
299
+ throw new Error('Subquery parsing not available in this context')
300
+ }
301
+ const subquery = c.parseSubquery()
302
+ return {
303
+ type: 'in',
304
+ expr: left,
305
+ subquery,
306
+ }
307
+ }
308
+
260
309
  if (tok.type === 'operator' && isComparisonOperator(tok.value)) {
261
310
  c.consume()
262
311
  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,17 @@ export interface BetweenNode {
102
108
  upper: ExprNode
103
109
  }
104
110
 
111
+ export interface InNode {
112
+ type: 'in' | 'not in'
113
+ expr: ExprNode
114
+ subquery: SelectStatement
115
+ }
116
+
117
+ export interface ExistsNode {
118
+ type: 'exists' | 'not exists'
119
+ subquery: SelectStatement
120
+ }
121
+
105
122
  export type ExprNode =
106
123
  | LiteralNode
107
124
  | IdentifierNode
@@ -110,6 +127,8 @@ export type ExprNode =
110
127
  | FunctionNode
111
128
  | CastNode
112
129
  | BetweenNode
130
+ | InNode
131
+ | ExistsNode
113
132
 
114
133
  export interface StarColumn {
115
134
  kind: 'star'
@@ -184,4 +203,5 @@ export interface ExprCursor {
184
203
  match(type: TokenType, value?: string): boolean
185
204
  expect(type: TokenType, value: string): Token
186
205
  expectIdentifier(): Token
206
+ parseSubquery?: () => SelectStatement
187
207
  }