squirreling 0.4.3 → 0.4.5

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/README.md CHANGED
@@ -29,7 +29,7 @@ Squirreling is a streaming async SQL engine for JavaScript. It is designed to pr
29
29
 
30
30
  Squirreling returns an async generator, allowing you to process rows one at a time without loading everything into memory.
31
31
 
32
- ```javascript
32
+ ```typescript
33
33
  import { executeSql } from 'squirreling'
34
34
 
35
35
  // In-memory table
@@ -40,12 +40,18 @@ const users = [
40
40
  // ...more rows
41
41
  ]
42
42
 
43
- // Process rows as they arrive (streaming)
44
- for await (const { cnt } of executeSql({
43
+ type AsyncRow = Record<string, AsyncCell>
44
+ type AsyncCell = () => Promise<SqlPrimitive>
45
+
46
+ // Returns an async iterable of rows with async cells
47
+ const asyncRows: AsyncIterable<AsyncRow> = executeSql({
45
48
  tables: { users },
46
49
  query: 'SELECT count(*) as cnt FROM users WHERE active = TRUE LIMIT 10',
47
- })) {
48
- console.log('Count', cnt)
50
+ })
51
+
52
+ // Process rows as they arrive (streaming)
53
+ for await (const { cnt } of asyncRows) {
54
+ console.log('Count', await cnt())
49
55
  }
50
56
  ```
51
57
 
@@ -54,7 +60,8 @@ There is an exported helper function `collect` to gather all rows into an array
54
60
  ```javascript
55
61
  import { collect, executeSql } from 'squirreling'
56
62
 
57
- const allUsers = await collect(executeSql({
63
+ // Collect all rows and cells into a materialized array
64
+ const allUsers: Record<string, SqlPrimitive>[] = await collect(executeSql({
58
65
  tables: { users },
59
66
  query: 'SELECT * FROM users',
60
67
  }))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @import { ExprNode, SelectStatement, SelectColumn } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * Extracts column names needed from a SELECT statement.
7
+ *
8
+ * @param {SelectStatement} select
9
+ * @returns {string[] | undefined} array of column names, or undefined if all columns needed
10
+ */
11
+ export function extractColumns(select) {
12
+ // If any column is SELECT *, we need all columns
13
+ if (select.columns.some(col => col.kind === 'star')) {
14
+ return undefined
15
+ }
16
+
17
+ /** @type {Set<string>} */
18
+ const columns = new Set()
19
+
20
+ // Columns from SELECT list
21
+ for (const col of select.columns) {
22
+ collectColumnsFromSelectColumn(col, columns)
23
+ }
24
+
25
+ // Columns from WHERE
26
+ collectColumnsFromExpr(select.where, columns)
27
+
28
+ // Columns from ORDER BY
29
+ for (const item of select.orderBy) {
30
+ collectColumnsFromExpr(item.expr, columns)
31
+ }
32
+
33
+ // Columns from GROUP BY
34
+ for (const expr of select.groupBy) {
35
+ collectColumnsFromExpr(expr, columns)
36
+ }
37
+
38
+ // Columns from HAVING
39
+ collectColumnsFromExpr(select.having, columns)
40
+
41
+ return [...columns]
42
+ }
43
+
44
+ /**
45
+ * Collects column names from a SELECT column
46
+ *
47
+ * @param {SelectColumn} col
48
+ * @param {Set<string>} columns
49
+ */
50
+ function collectColumnsFromSelectColumn(col, columns) {
51
+ if (col.kind === 'derived') {
52
+ collectColumnsFromExpr(col.expr, columns)
53
+ } else if (col.kind === 'aggregate') {
54
+ if (col.arg.kind === 'expression') {
55
+ collectColumnsFromExpr(col.arg.expr, columns)
56
+ }
57
+ // 'star' aggregate (COUNT(*)) doesn't reference specific columns
58
+ }
59
+ // 'star' columns handled separately (returns undefined for all columns)
60
+ }
61
+
62
+ /**
63
+ * Recursively collects column names (identifiers) from an expression
64
+ *
65
+ * @param {ExprNode | undefined} expr
66
+ * @param {Set<string>} columns
67
+ */
68
+ function collectColumnsFromExpr(expr, columns) {
69
+ if (!expr) return
70
+ if (expr.type === 'identifier') {
71
+ columns.add(expr.name)
72
+ } else if (expr.type === 'literal') {
73
+ // No columns
74
+ } else if (expr.type === 'binary') {
75
+ collectColumnsFromExpr(expr.left, columns)
76
+ collectColumnsFromExpr(expr.right, columns)
77
+ } else if (expr.type === 'unary') {
78
+ collectColumnsFromExpr(expr.argument, columns)
79
+ } else if (expr.type === 'function') {
80
+ for (const arg of expr.args) {
81
+ collectColumnsFromExpr(arg, columns)
82
+ }
83
+ } else if (expr.type === 'cast') {
84
+ collectColumnsFromExpr(expr.expr, columns)
85
+ } else if (expr.type === 'in valuelist') {
86
+ collectColumnsFromExpr(expr.expr, columns)
87
+ for (const val of expr.values) {
88
+ collectColumnsFromExpr(val, columns)
89
+ }
90
+ } else if (expr.type === 'in') {
91
+ collectColumnsFromExpr(expr.expr, columns)
92
+ // Subquery columns are from a different scope, don't collect
93
+ } else if (expr.type === 'exists' || expr.type === 'not exists') {
94
+ // Subquery columns are from a different scope, don't collect
95
+ } else if (expr.type === 'case') {
96
+ if (expr.caseExpr) {
97
+ collectColumnsFromExpr(expr.caseExpr, columns)
98
+ }
99
+ for (const when of expr.whenClauses) {
100
+ collectColumnsFromExpr(when.condition, columns)
101
+ collectColumnsFromExpr(when.result, columns)
102
+ }
103
+ if (expr.elseResult) {
104
+ collectColumnsFromExpr(expr.elseResult, columns)
105
+ }
106
+ }
107
+ }
@@ -5,9 +5,10 @@ import { evaluateExpr } from './expression.js'
5
5
  import { evaluateHavingExpr } from './having.js'
6
6
  import { executeJoins } from './join.js'
7
7
  import { compareForTerm, defaultDerivedAlias } from './utils.js'
8
+ import { extractColumns } from './columns.js'
8
9
 
9
10
  /**
10
- * @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
11
+ * @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, ExprNode, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
11
12
  */
12
13
 
13
14
  /**
@@ -239,7 +240,16 @@ async function* evaluateStreaming(select, dataSource, tables) {
239
240
  /** @type {Set<string> | undefined} */
240
241
  const seen = select.distinct ? new Set() : undefined
241
242
 
242
- for await (const row of dataSource.getRows()) {
243
+ // hints for data source optimization
244
+ /** @type {QueryHints} */
245
+ const hints = {
246
+ columns: extractColumns(select),
247
+ where: select.where,
248
+ limit: select.limit,
249
+ offset: select.offset,
250
+ }
251
+
252
+ for await (const row of dataSource.getRows(hints)) {
243
253
  // WHERE filter
244
254
  if (select.where) {
245
255
  const pass = await evaluateExpr({ node: select.where, row, tables })
@@ -301,10 +311,18 @@ async function* evaluateStreaming(select, dataSource, tables) {
301
311
  * @yields {AsyncRow}
302
312
  */
303
313
  async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
314
+ // Build hints for data source optimization
315
+ // Note: limit/offset not passed here since buffering needs all rows for sorting/grouping
316
+ /** @type {QueryHints} */
317
+ const hints = {
318
+ where: select.where,
319
+ columns: extractColumns(select),
320
+ }
321
+
304
322
  // Step 1: Collect all rows from data source
305
323
  /** @type {AsyncRow[]} */
306
324
  const working = []
307
- for await (const row of dataSource.getRows()) {
325
+ for await (const row of dataSource.getRows(hints)) {
308
326
  working.push(row)
309
327
  }
310
328
 
@@ -24,8 +24,9 @@ export function compareForTerm(a, b, term) {
24
24
  // Compare non-null values
25
25
  if (a === b) return 0
26
26
 
27
+ const primitives = ['number', 'bigint', 'boolean', 'string']
27
28
  let cmp
28
- if (typeof a === 'number' && typeof b === 'number') {
29
+ if (primitives.includes(typeof a) && primitives.includes(typeof b)) {
29
30
  cmp = a < b ? -1 : a > b ? 1 : 0
30
31
  } else {
31
32
  const aa = String(a)
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AsyncRow, ExecuteSqlOptions, SelectStatement, SqlPrimitive } from './types.js'
1
+ import type { AsyncDataSource, AsyncRow, ExecuteSqlOptions, SelectStatement, SqlPrimitive } from './types.js'
2
2
  export type { AsyncDataSource, AsyncRow, SqlPrimitive } from './types.js'
3
3
 
4
4
  /**
@@ -26,3 +26,5 @@ export function parseSql(query: string): SelectStatement
26
26
  * @returns array of all yielded values
27
27
  */
28
28
  export function collect<T>(asyncGen: AsyncGenerator<AsyncRow>): Promise<Record<string, SqlPrimitive>[]>
29
+
30
+ export function cachedDataSource(source: AsyncDataSource): AsyncDataSource
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { executeSql } from './execute/execute.js'
2
2
  export { parseSql } from './parse/parse.js'
3
3
  export { collect } from './execute/utils.js'
4
+ export { cachedDataSource } from './backend/dataSource.js'
@@ -1,32 +1,33 @@
1
1
  import { isBinaryOp } from '../validation.js'
2
- import { parseExpression, parsePrimary } from './expression.js'
2
+ import { parseExpression, parsePrimary, parseSubquery } from './expression.js'
3
+ import { consume, current, expect, match, peekToken } from './state.js'
3
4
 
4
5
  /**
5
- * @import { ExprCursor, ExprNode } from '../types.js'
6
+ * @import { ExprNode, ParserState } from '../types.js'
6
7
  */
7
8
 
8
9
  /**
9
- * @param {ExprCursor} c
10
+ * @param {ParserState} state
10
11
  * @returns {ExprNode}
11
12
  */
12
- export function parseComparison(c) {
13
- const left = parsePrimary(c)
14
- const tok = c.current()
13
+ export function parseComparison(state) {
14
+ const left = parsePrimary(state)
15
+ const tok = current(state)
15
16
 
16
17
  // IS [NOT] NULL
17
18
  if (tok.type === 'keyword' && tok.value === 'IS') {
18
- c.consume()
19
- const notToken = c.current()
19
+ consume(state)
20
+ const notToken = current(state)
20
21
  if (notToken.type === 'keyword' && notToken.value === 'NOT') {
21
- c.consume()
22
- c.expect('keyword', 'NULL')
22
+ consume(state)
23
+ expect(state, 'keyword', 'NULL')
23
24
  return {
24
25
  type: 'unary',
25
26
  op: 'IS NOT NULL',
26
27
  argument: left,
27
28
  }
28
29
  }
29
- c.expect('keyword', 'NULL')
30
+ expect(state, 'keyword', 'NULL')
30
31
  return {
31
32
  type: 'unary',
32
33
  op: 'IS NULL',
@@ -36,11 +37,11 @@ export function parseComparison(c) {
36
37
 
37
38
  // [NOT] LIKE
38
39
  if (tok.type === 'keyword' && tok.value === 'NOT') {
39
- const nextTok = c.peek(1)
40
+ const nextTok = peekToken(state, 1)
40
41
  if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
41
- c.consume() // NOT
42
- c.consume() // LIKE
43
- const right = parsePrimary(c)
42
+ consume(state) // NOT
43
+ consume(state) // LIKE
44
+ const right = parsePrimary(state)
44
45
  return {
45
46
  type: 'unary',
46
47
  op: 'NOT',
@@ -55,8 +56,8 @@ export function parseComparison(c) {
55
56
  }
56
57
 
57
58
  if (tok.type === 'keyword' && tok.value === 'LIKE') {
58
- c.consume()
59
- const right = parsePrimary(c)
59
+ consume(state)
60
+ const right = parsePrimary(state)
60
61
  return {
61
62
  type: 'binary',
62
63
  op: 'LIKE',
@@ -67,13 +68,13 @@ export function parseComparison(c) {
67
68
 
68
69
  // [NOT] BETWEEN - convert to range comparison
69
70
  if (tok.type === 'keyword' && tok.value === 'NOT') {
70
- const nextTok = c.peek(1)
71
+ const nextTok = peekToken(state, 1)
71
72
  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)
73
+ consume(state) // NOT
74
+ consume(state) // BETWEEN
75
+ const lower = parsePrimary(state)
76
+ expect(state, 'keyword', 'AND')
77
+ const upper = parsePrimary(state)
77
78
  // NOT BETWEEN -> expr < lower OR expr > upper
78
79
  return {
79
80
  type: 'binary',
@@ -85,10 +86,10 @@ export function parseComparison(c) {
85
86
  }
86
87
 
87
88
  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)
89
+ consume(state)
90
+ const lower = parsePrimary(state)
91
+ expect(state, 'keyword', 'AND')
92
+ const upper = parsePrimary(state)
92
93
  // BETWEEN -> expr >= lower AND expr <= upper
93
94
  return {
94
95
  type: 'binary',
@@ -100,21 +101,21 @@ export function parseComparison(c) {
100
101
 
101
102
  // [NOT] IN
102
103
  if (tok.type === 'keyword' && tok.value === 'NOT') {
103
- const nextTok = c.peek(1)
104
+ const nextTok = peekToken(state, 1)
104
105
  if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
105
- c.consume() // NOT
106
- c.consume() // IN
106
+ consume(state) // NOT
107
+ consume(state) // IN
107
108
 
108
109
  // Check if it's a subquery or a list of values by peeking ahead
109
110
  // parseSubquery expects to consume the opening paren itself
110
- const parenTok = c.current()
111
+ const parenTok = current(state)
111
112
  if (parenTok.type !== 'paren' || parenTok.value !== '(') {
112
113
  throw new Error('Expected ( after IN')
113
114
  }
114
- const peekTok = c.peek(1)
115
+ const peekTok = peekToken(state, 1)
115
116
  if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
116
117
  // Subquery - let parseSubquery handle the parens
117
- const subquery = c.parseSubquery()
118
+ const subquery = parseSubquery(state)
118
119
  return {
119
120
  type: 'unary',
120
121
  op: 'NOT',
@@ -126,14 +127,14 @@ export function parseComparison(c) {
126
127
  }
127
128
  } else {
128
129
  // Parse list of values - we handle the parens
129
- c.consume() // '('
130
+ consume(state) // '('
130
131
  /** @type {ExprNode[]} */
131
132
  const values = []
132
133
  while (true) {
133
- values.push(parseExpression(c))
134
- if (!c.match('comma')) break
134
+ values.push(parseExpression(state))
135
+ if (!match(state, 'comma')) break
135
136
  }
136
- c.expect('paren', ')')
137
+ expect(state, 'paren', ')')
137
138
  return {
138
139
  type: 'unary',
139
140
  op: 'NOT',
@@ -148,18 +149,18 @@ export function parseComparison(c) {
148
149
  }
149
150
 
150
151
  if (tok.type === 'keyword' && tok.value === 'IN') {
151
- c.consume() // IN
152
+ consume(state) // IN
152
153
 
153
154
  // Check if it's a subquery or a list of values by peeking ahead
154
155
  // parseSubquery expects to consume the opening paren itself
155
- const parenTok = c.current()
156
+ const parenTok = current(state)
156
157
  if (parenTok.type !== 'paren' || parenTok.value !== '(') {
157
158
  throw new Error('Expected ( after IN')
158
159
  }
159
- const peekTok = c.peek(1)
160
+ const peekTok = peekToken(state, 1)
160
161
  if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
161
162
  // Subquery - let parseSubquery handle the parens
162
- const subquery = c.parseSubquery()
163
+ const subquery = parseSubquery(state)
163
164
  return {
164
165
  type: 'in',
165
166
  expr: left,
@@ -167,14 +168,14 @@ export function parseComparison(c) {
167
168
  }
168
169
  } else {
169
170
  // Parse list of values - we handle the parens
170
- c.consume() // '('
171
+ consume(state) // '('
171
172
  /** @type {ExprNode[]} */
172
173
  const values = []
173
174
  while (true) {
174
- values.push(parseExpression(c))
175
- if (!c.match('comma')) break
175
+ values.push(parseExpression(state))
176
+ if (!match(state, 'comma')) break
176
177
  }
177
- c.expect('paren', ')')
178
+ expect(state, 'paren', ')')
178
179
  return {
179
180
  type: 'in valuelist',
180
181
  expr: left,
@@ -184,8 +185,8 @@ export function parseComparison(c) {
184
185
  }
185
186
 
186
187
  if (tok.type === 'operator' && isBinaryOp(tok.value)) {
187
- c.consume()
188
- const right = parsePrimary(c)
188
+ consume(state)
189
+ const right = parsePrimary(state)
189
190
  return {
190
191
  type: 'binary',
191
192
  op: tok.value,
@@ -1,54 +1,56 @@
1
1
  import { isAggregateFunc, isStringFunc } from '../validation.js'
2
2
  import { parseComparison } from './comparison.js'
3
+ import { parseSelectInternal } from './parse.js'
4
+ import { consume, current, expect, expectIdentifier, match, peekToken } from './state.js'
3
5
 
4
6
  /**
5
- * @import { ExprCursor, ExprNode, WhenClause } from '../types.js'
7
+ * @import { ExprNode, ParserState, SelectStatement, WhenClause } from '../types.js'
6
8
  */
7
9
 
8
10
  /**
9
- * @param {ExprCursor} c
11
+ * @param {ParserState} state
10
12
  * @returns {ExprNode}
11
13
  */
12
- export function parseExpression(c) {
13
- return parseOr(c)
14
+ export function parseExpression(state) {
15
+ return parseOr(state)
14
16
  }
15
17
 
16
18
  /**
17
- * @param {ExprCursor} c
19
+ * @param {ParserState} state
18
20
  * @returns {ExprNode}
19
21
  */
20
- export function parsePrimary(c) {
21
- const tok = c.current()
22
+ export function parsePrimary(state) {
23
+ const tok = current(state)
22
24
 
23
25
  if (tok.type === 'paren' && tok.value === '(') {
24
26
  // Peek ahead to see if this is a scalar subquery
25
- const nextTok = c.peek(1)
27
+ const nextTok = peekToken(state, 1)
26
28
  if (nextTok.type === 'keyword' && nextTok.value === 'SELECT') {
27
29
  // It's a scalar subquery
28
- const subquery = c.parseSubquery()
30
+ const subquery = parseSubquery(state)
29
31
  return {
30
32
  type: 'subquery',
31
33
  subquery,
32
34
  }
33
35
  }
34
36
  // Regular grouped expression
35
- c.consume()
36
- const expr = parseExpression(c)
37
- c.expect('paren', ')')
37
+ consume(state)
38
+ const expr = parseExpression(state)
39
+ expect(state, 'paren', ')')
38
40
  return expr
39
41
  }
40
42
 
41
43
  if (tok.type === 'identifier') {
42
- const next = c.peek(1)
44
+ const next = peekToken(state, 1)
43
45
 
44
46
  // CAST expression
45
47
  if (tok.value === 'CAST' && next.type === 'paren' && next.value === '(') {
46
- c.consume() // CAST
47
- c.consume() // '('
48
- const expr = parseExpression(c)
49
- c.expect('keyword', 'AS')
50
- const typeTok = c.expectIdentifier()
51
- c.expect('paren', ')')
48
+ consume(state) // CAST
49
+ consume(state) // '('
50
+ const expr = parseExpression(state)
51
+ expect(state, 'keyword', 'AS')
52
+ const typeTok = expectIdentifier(state)
53
+ expect(state, 'paren', ')')
52
54
  return {
53
55
  type: 'cast',
54
56
  expr,
@@ -65,30 +67,30 @@ export function parsePrimary(c) {
65
67
  throw new Error(`Unknown function "${funcName}" at position ${tok.position}`)
66
68
  }
67
69
 
68
- c.consume() // function name
69
- c.consume() // '('
70
+ consume(state) // function name
71
+ consume(state) // '('
70
72
 
71
73
  /** @type {ExprNode[]} */
72
74
  const args = []
73
75
 
74
- if (c.current().type !== 'paren' || c.current().value !== ')') {
76
+ if (current(state).type !== 'paren' || current(state).value !== ')') {
75
77
  while (true) {
76
78
  // Handle COUNT(*) - treat * as a special identifier
77
- if (c.current().type === 'operator' && c.current().value === '*') {
78
- c.consume()
79
+ if (current(state).type === 'operator' && current(state).value === '*') {
80
+ consume(state)
79
81
  args.push({
80
82
  type: 'identifier',
81
83
  name: '*',
82
84
  })
83
85
  } else {
84
- const arg = parseExpression(c)
86
+ const arg = parseExpression(state)
85
87
  args.push(arg)
86
88
  }
87
- if (!c.match('comma')) break
89
+ if (!match(state, 'comma')) break
88
90
  }
89
91
  }
90
92
 
91
- c.expect('paren', ')')
93
+ expect(state, 'paren', ')')
92
94
 
93
95
  return {
94
96
  type: 'function',
@@ -97,13 +99,13 @@ export function parsePrimary(c) {
97
99
  }
98
100
  }
99
101
 
100
- c.consume()
102
+ consume(state)
101
103
  let name = tok.value
102
104
 
103
105
  // table.column
104
- if (c.current().type === 'dot') {
105
- c.consume()
106
- const columnTok = c.expectIdentifier()
106
+ if (current(state).type === 'dot') {
107
+ consume(state)
108
+ const columnTok = expectIdentifier(state)
107
109
  name = name + '.' + columnTok.value
108
110
  }
109
111
 
@@ -114,7 +116,7 @@ export function parsePrimary(c) {
114
116
  }
115
117
 
116
118
  if (tok.type === 'number') {
117
- c.consume()
119
+ consume(state)
118
120
  return {
119
121
  type: 'literal',
120
122
  value: tok.numericValue ?? null,
@@ -122,7 +124,7 @@ export function parsePrimary(c) {
122
124
  }
123
125
 
124
126
  if (tok.type === 'string') {
125
- c.consume()
127
+ consume(state)
126
128
  return {
127
129
  type: 'literal',
128
130
  value: tok.value,
@@ -131,44 +133,44 @@ export function parsePrimary(c) {
131
133
 
132
134
  if (tok.type === 'keyword') {
133
135
  if (tok.value === 'TRUE') {
134
- c.consume()
136
+ consume(state)
135
137
  return { type: 'literal', value: true }
136
138
  }
137
139
  if (tok.value === 'FALSE') {
138
- c.consume()
140
+ consume(state)
139
141
  return { type: 'literal', value: false }
140
142
  }
141
143
  if (tok.value === 'NULL') {
142
- c.consume()
144
+ consume(state)
143
145
  return { type: 'literal', value: null }
144
146
  }
145
147
  if (tok.value === 'EXISTS') {
146
- c.consume() // EXISTS
147
- const subquery = c.parseSubquery()
148
+ consume(state) // EXISTS
149
+ const subquery = parseSubquery(state)
148
150
  return {
149
151
  type: 'exists',
150
152
  subquery,
151
153
  }
152
154
  }
153
155
  if (tok.value === 'CASE') {
154
- c.consume() // CASE
156
+ consume(state) // CASE
155
157
 
156
158
  // Check if it's simple CASE (CASE expr WHEN ...) or searched CASE (CASE WHEN ...)
157
159
  /** @type {ExprNode | undefined} */
158
160
  let caseExpr
159
- const nextTok = c.current()
161
+ const nextTok = current(state)
160
162
  if (nextTok.type !== 'keyword' || nextTok.value !== 'WHEN') {
161
163
  // Simple CASE: parse the case expression
162
- caseExpr = parseExpression(c)
164
+ caseExpr = parseExpression(state)
163
165
  }
164
166
 
165
167
  // Parse WHEN clauses
166
168
  /** @type {WhenClause[]} */
167
169
  const whenClauses = []
168
- while (c.match('keyword', 'WHEN')) {
169
- const condition = parseExpression(c)
170
- c.expect('keyword', 'THEN')
171
- const result = parseExpression(c)
170
+ while (match(state, 'keyword', 'WHEN')) {
171
+ const condition = parseExpression(state)
172
+ expect(state, 'keyword', 'THEN')
173
+ const result = parseExpression(state)
172
174
  whenClauses.push({ condition, result })
173
175
  }
174
176
 
@@ -179,11 +181,11 @@ export function parsePrimary(c) {
179
181
  // Parse optional ELSE clause
180
182
  /** @type {ExprNode | undefined} */
181
183
  let elseResult
182
- if (c.match('keyword', 'ELSE')) {
183
- elseResult = parseExpression(c)
184
+ if (match(state, 'keyword', 'ELSE')) {
185
+ elseResult = parseExpression(state)
184
186
  }
185
187
 
186
- c.expect('keyword', 'END')
188
+ expect(state, 'keyword', 'END')
187
189
 
188
190
  return {
189
191
  type: 'case',
@@ -195,8 +197,8 @@ export function parsePrimary(c) {
195
197
  }
196
198
 
197
199
  if (tok.type === 'operator' && tok.value === '-') {
198
- c.consume()
199
- const argument = parsePrimary(c)
200
+ consume(state)
201
+ const argument = parsePrimary(state)
200
202
  return {
201
203
  type: 'unary',
202
204
  op: '-',
@@ -209,13 +211,13 @@ export function parsePrimary(c) {
209
211
  }
210
212
 
211
213
  /**
212
- * @param {ExprCursor} c
214
+ * @param {ParserState} state
213
215
  * @returns {ExprNode}
214
216
  */
215
- function parseOr(c) {
216
- let node = parseAnd(c)
217
- while (c.match('keyword', 'OR')) {
218
- const right = parseAnd(c)
217
+ function parseOr(state) {
218
+ let node = parseAnd(state)
219
+ while (match(state, 'keyword', 'OR')) {
220
+ const right = parseAnd(state)
219
221
  node = {
220
222
  type: 'binary',
221
223
  op: 'OR',
@@ -227,13 +229,13 @@ function parseOr(c) {
227
229
  }
228
230
 
229
231
  /**
230
- * @param {ExprCursor} c
232
+ * @param {ParserState} state
231
233
  * @returns {ExprNode}
232
234
  */
233
- function parseAnd(c) {
234
- let node = parseNot(c)
235
- while (c.match('keyword', 'AND')) {
236
- const right = parseNot(c)
235
+ function parseAnd(state) {
236
+ let node = parseNot(state)
237
+ while (match(state, 'keyword', 'AND')) {
238
+ const right = parseNot(state)
237
239
  node = {
238
240
  type: 'binary',
239
241
  op: 'AND',
@@ -245,27 +247,40 @@ function parseAnd(c) {
245
247
  }
246
248
 
247
249
  /**
248
- * @param {ExprCursor} c
250
+ * @param {ParserState} state
249
251
  * @returns {ExprNode}
250
252
  */
251
- function parseNot(c) {
252
- if (c.match('keyword', 'NOT')) {
253
+ function parseNot(state) {
254
+ if (match(state, 'keyword', 'NOT')) {
253
255
  // Check for NOT EXISTS
254
- const nextTok = c.current()
256
+ const nextTok = current(state)
255
257
  if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
256
- c.consume() // EXISTS
257
- const subquery = c.parseSubquery()
258
+ consume(state) // EXISTS
259
+ const subquery = parseSubquery(state)
258
260
  return {
259
261
  type: 'not exists',
260
262
  subquery,
261
263
  }
262
264
  }
263
- const argument = parseNot(c)
265
+ const argument = parseNot(state)
264
266
  return {
265
267
  type: 'unary',
266
268
  op: 'NOT',
267
269
  argument,
268
270
  }
269
271
  }
270
- return parseComparison(c)
272
+ return parseComparison(state)
273
+ }
274
+
275
+ /**
276
+ * Creates an ExprCursor adapter for the ParserState.
277
+ *
278
+ * @param {ParserState} state
279
+ * @returns {SelectStatement}
280
+ */
281
+ export function parseSubquery(state) {
282
+ expect(state, 'paren', '(')
283
+ const query = parseSelectInternal(state)
284
+ expect(state, 'paren', ')')
285
+ return query
271
286
  }
@@ -0,0 +1,77 @@
1
+ import { parseExpression } from './expression.js'
2
+ import { parseTableAlias } from './parse.js'
3
+ import { consume, current, expect, expectIdentifier, match } from './state.js'
4
+
5
+ /**
6
+ * @import { JoinClause, JoinType, ParserState } from '../types.js'
7
+ * @param {ParserState} state
8
+ * @returns {JoinClause[]}
9
+ */
10
+ export function parseJoins(state) {
11
+ /** @type {JoinClause[]} */
12
+ const joins = []
13
+
14
+ while (true) {
15
+ const tok = current(state)
16
+
17
+ // Check for join keywords
18
+ /** @type {JoinType} */
19
+ let joinType = 'INNER'
20
+
21
+ if (tok.type === 'keyword') {
22
+ if (tok.value === 'INNER') {
23
+ consume(state)
24
+ joinType = 'INNER'
25
+ } else if (tok.value === 'LEFT') {
26
+ consume(state)
27
+ if (match(state, 'keyword', 'OUTER')) {
28
+ // LEFT OUTER JOIN
29
+ }
30
+ joinType = 'LEFT'
31
+ } else if (tok.value === 'RIGHT') {
32
+ consume(state)
33
+ if (match(state, 'keyword', 'OUTER')) {
34
+ // RIGHT OUTER JOIN
35
+ }
36
+ joinType = 'RIGHT'
37
+ } else if (tok.value === 'FULL') {
38
+ consume(state)
39
+ if (match(state, 'keyword', 'OUTER')) {
40
+ // FULL OUTER JOIN
41
+ }
42
+ joinType = 'FULL'
43
+ } else if (tok.value === 'JOIN') {
44
+ // Just JOIN (defaults to INNER)
45
+ consume(state)
46
+ } else {
47
+ // Not a join keyword, stop parsing joins
48
+ break
49
+ }
50
+
51
+ // If we consumed a join type keyword (INNER/LEFT/RIGHT/FULL), expect JOIN
52
+ if (tok.value !== 'JOIN') {
53
+ expect(state, 'keyword', 'JOIN')
54
+ }
55
+ } else {
56
+ // No more joins
57
+ break
58
+ }
59
+
60
+ // Parse table name and optional alias
61
+ const tableName = expectIdentifier(state).value
62
+ const tableAlias = parseTableAlias(state)
63
+
64
+ // Parse ON condition
65
+ expect(state, 'keyword', 'ON')
66
+ const condition = parseExpression(state)
67
+
68
+ joins.push({
69
+ joinType,
70
+ table: tableName,
71
+ alias: tableAlias,
72
+ on: condition,
73
+ })
74
+ }
75
+
76
+ return joins
77
+ }
@@ -1,39 +1,13 @@
1
1
  import { tokenize } from './tokenize.js'
2
2
  import { parseExpression } from './expression.js'
3
- import { isAggregateFunc } from '../validation.js'
3
+ import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE, isAggregateFunc } from '../validation.js'
4
+ import { consume, current, expect, expectIdentifier, match, parseError, peekToken } from './state.js'
5
+ import { parseJoins } from './joins.js'
4
6
 
5
7
  /**
6
- * @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, FromTable, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, Token, TokenType } from '../types.js'
8
+ * @import { AggregateColumn, AggregateArg, AggregateFunc, ExprNode, FromSubquery, FromTable, OrderByItem, ParserState, SelectStatement, SelectColumn } from '../types.js'
7
9
  */
8
10
 
9
- // Keywords that cannot be used as implicit aliases after a column
10
- const RESERVED_AFTER_COLUMN = new Set([
11
- 'FROM',
12
- 'WHERE',
13
- 'GROUP',
14
- 'HAVING',
15
- 'ORDER',
16
- 'LIMIT',
17
- 'OFFSET',
18
- ])
19
-
20
- // Keywords that cannot be used as table aliases
21
- const RESERVED_AFTER_TABLE = new Set([
22
- 'WHERE',
23
- 'GROUP',
24
- 'HAVING',
25
- 'ORDER',
26
- 'LIMIT',
27
- 'OFFSET',
28
- 'JOIN',
29
- 'INNER',
30
- 'LEFT',
31
- 'RIGHT',
32
- 'FULL',
33
- 'CROSS',
34
- 'ON',
35
- ])
36
-
37
11
  /**
38
12
  * @param {string} query
39
13
  * @returns {SelectStatement}
@@ -52,104 +26,6 @@ export function parseSql(query) {
52
26
  return select
53
27
  }
54
28
 
55
- /**
56
- * @param {ParserState} state
57
- * @returns {Token}
58
- */
59
- function current(state) {
60
- return state.tokens[state.pos]
61
- }
62
-
63
- /**
64
- * @param {ParserState} state
65
- * @param {number} offset
66
- * @returns {Token}
67
- */
68
- function peekToken(state, offset) {
69
- const idx = state.pos + offset
70
- if (idx >= state.tokens.length) {
71
- return state.tokens[state.tokens.length - 1]
72
- }
73
- return state.tokens[idx]
74
- }
75
-
76
- /**
77
- * @param {ParserState} state
78
- * @returns {Token}
79
- */
80
- function consume(state) {
81
- const tok = current(state)
82
- if (state.pos < state.tokens.length - 1) {
83
- state.pos += 1
84
- }
85
- return tok
86
- }
87
-
88
- /**
89
- * @param {ParserState} state
90
- * @param {TokenType} type
91
- * @param {string} [value]
92
- * @returns {boolean}
93
- */
94
- function match(state, type, value) {
95
- const tok = current(state)
96
- if (tok.type !== type) return false
97
- if (typeof value === 'string' && tok.value !== value) return false
98
- consume(state)
99
- return true
100
- }
101
-
102
- /**
103
- * @param {ParserState} state
104
- * @param {TokenType} type
105
- * @param {string} value
106
- * @returns {Token}
107
- */
108
- function expect(state, type, value) {
109
- const tok = current(state)
110
- if (tok.type !== type || tok.value !== value) {
111
- throw parseError(state, value)
112
- }
113
- consume(state)
114
- return tok
115
- }
116
-
117
- /**
118
- * @param {ParserState} state
119
- * @returns {Token}
120
- */
121
- function expectIdentifier(state) {
122
- const tok = current(state)
123
- if (tok.type !== 'identifier') {
124
- throw parseError(state, 'identifier')
125
- }
126
- consume(state)
127
- return tok
128
- }
129
-
130
- /**
131
- * Creates an ExprCursor adapter for the ParserState.
132
- *
133
- * @param {ParserState} state
134
- * @returns {ExprCursor}
135
- */
136
- function createExprCursor(state) {
137
- return {
138
- current: () => current(state),
139
- peek: (offset) => peekToken(state, offset),
140
- consume: () => consume(state),
141
- match: (type, value) => match(state, type, value),
142
- expect: (type, value) => expect(state, type, value),
143
- expectIdentifier: () => expectIdentifier(state),
144
- parseSubquery: () => {
145
- expect(state, 'paren', '(')
146
- const query = parseSelectInternal(state)
147
- expect(state, 'paren', ')')
148
- return query
149
- },
150
- }
151
- }
152
-
153
29
  /**
154
30
  * @param {ParserState} state
155
31
  * @returns {SelectColumn[]}
@@ -208,8 +84,7 @@ function parseSelectItem(state) {
208
84
  }
209
85
 
210
86
  // Delegate to expression parser
211
- const cursor = createExprCursor(state)
212
- const expr = parseExpression(cursor)
87
+ const expr = parseExpression(state)
213
88
  const alias = parseAs(state)
214
89
  return { kind: 'derived', expr, alias }
215
90
  }
@@ -239,8 +114,7 @@ function parseAggregateItem(state, func) {
239
114
  quantifier = 'distinct'
240
115
  }
241
116
 
242
- const cursor = createExprCursor(state)
243
- const expr = parseExpression(cursor)
117
+ const expr = parseExpression(state)
244
118
  arg = {
245
119
  kind: 'expression',
246
120
  expr,
@@ -260,7 +134,7 @@ function parseAggregateItem(state, func) {
260
134
  * @param {ParserState} state
261
135
  * @returns {string | undefined}
262
136
  */
263
- function parseTableAlias(state) {
137
+ export function parseTableAlias(state) {
264
138
  // Check for explicit AS keyword
265
139
  if (match(state, 'keyword', 'AS')) {
266
140
  const aliasTok = expectIdentifier(state)
@@ -302,86 +176,12 @@ function parseAs(state) {
302
176
  }
303
177
  }
304
178
 
305
- /**
306
- * @param {ParserState} state
307
- * @returns {JoinClause[]}
308
- */
309
- function parseJoins(state) {
310
- /** @type {JoinClause[]} */
311
- const joins = []
312
-
313
- while (true) {
314
- const tok = current(state)
315
-
316
- // Check for join keywords
317
- /** @type {JoinType} */
318
- let joinType = 'INNER'
319
-
320
- if (tok.type === 'keyword') {
321
- if (tok.value === 'INNER') {
322
- consume(state)
323
- joinType = 'INNER'
324
- } else if (tok.value === 'LEFT') {
325
- consume(state)
326
- if (match(state, 'keyword', 'OUTER')) {
327
- // LEFT OUTER JOIN
328
- }
329
- joinType = 'LEFT'
330
- } else if (tok.value === 'RIGHT') {
331
- consume(state)
332
- if (match(state, 'keyword', 'OUTER')) {
333
- // RIGHT OUTER JOIN
334
- }
335
- joinType = 'RIGHT'
336
- } else if (tok.value === 'FULL') {
337
- consume(state)
338
- if (match(state, 'keyword', 'OUTER')) {
339
- // FULL OUTER JOIN
340
- }
341
- joinType = 'FULL'
342
- } else if (tok.value === 'JOIN') {
343
- // Just JOIN (defaults to INNER)
344
- consume(state)
345
- } else {
346
- // Not a join keyword, stop parsing joins
347
- break
348
- }
349
-
350
- // If we consumed a join type keyword (INNER/LEFT/RIGHT/FULL), expect JOIN
351
- if (tok.value !== 'JOIN') {
352
- expect(state, 'keyword', 'JOIN')
353
- }
354
- } else {
355
- // No more joins
356
- break
357
- }
358
-
359
- // Parse table name and optional alias
360
- const tableName = expectIdentifier(state).value
361
- const tableAlias = parseTableAlias(state)
362
-
363
- // Parse ON condition
364
- expect(state, 'keyword', 'ON')
365
- const cursor = createExprCursor(state)
366
- const condition = parseExpression(cursor)
367
-
368
- joins.push({
369
- joinType,
370
- table: tableName,
371
- alias: tableAlias,
372
- on: condition,
373
- })
374
- }
375
-
376
- return joins
377
- }
378
-
379
179
  /**
380
180
  * Parses a subquery in parentheses with an alias
381
181
  * @param {ParserState} state
382
182
  * @returns {FromSubquery}
383
183
  */
384
- function parseSubquery(state) {
184
+ function parseFromSubquery(state) {
385
185
  expect(state, 'paren', '(')
386
186
  const query = parseSelectInternal(state)
387
187
  expect(state, 'paren', ')')
@@ -394,7 +194,7 @@ function parseSubquery(state) {
394
194
  * @param {ParserState} state
395
195
  * @returns {SelectStatement}
396
196
  */
397
- function parseSelectInternal(state) {
197
+ export function parseSelectInternal(state) {
398
198
  expect(state, 'keyword', 'SELECT')
399
199
 
400
200
  let distinct = false
@@ -412,7 +212,7 @@ function parseSelectInternal(state) {
412
212
  const tok = current(state)
413
213
  if (tok.type === 'paren' && tok.value === '(') {
414
214
  // Subquery: SELECT * FROM (SELECT ...) AS alias
415
- from = parseSubquery(state)
215
+ from = parseFromSubquery(state)
416
216
  } else {
417
217
  // Simple table name: SELECT * FROM users
418
218
  const table = expectIdentifier(state).value
@@ -436,29 +236,27 @@ function parseSelectInternal(state) {
436
236
  /** @type {number | undefined} */
437
237
  let offset
438
238
 
439
- const cursor = createExprCursor(state)
440
-
441
239
  if (match(state, 'keyword', 'WHERE')) {
442
- where = parseExpression(cursor)
240
+ where = parseExpression(state)
443
241
  }
444
242
 
445
243
  if (match(state, 'keyword', 'GROUP')) {
446
244
  expect(state, 'keyword', 'BY')
447
245
  while (true) {
448
- const expr = parseExpression(cursor)
246
+ const expr = parseExpression(state)
449
247
  groupBy.push(expr)
450
248
  if (!match(state, 'comma')) break
451
249
  }
452
250
  }
453
251
 
454
252
  if (match(state, 'keyword', 'HAVING')) {
455
- having = parseExpression(cursor)
253
+ having = parseExpression(state)
456
254
  }
457
255
 
458
256
  if (match(state, 'keyword', 'ORDER')) {
459
257
  expect(state, 'keyword', 'BY')
460
258
  while (true) {
461
- const expr = parseExpression(cursor)
259
+ const expr = parseExpression(state)
462
260
  /** @type {'ASC' | 'DESC'} */
463
261
  let direction = 'ASC'
464
262
  if (match(state, 'keyword', 'ASC')) {
@@ -544,17 +342,3 @@ function parseSelectInternal(state) {
544
342
  offset,
545
343
  }
546
344
  }
547
-
548
- /**
549
- * Helper function to create consistent parser error messages.
550
- * @param {ParserState} state
551
- * @param {string} expected - Description of what was expected
552
- * @returns {Error}
553
- */
554
- function parseError(state, expected) {
555
- const tok = current(state)
556
- const prevToken = state.tokens[state.pos - 1]
557
- const after = prevToken ? ` after "${prevToken.originalValue ?? prevToken.value}"` : ''
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}`)
560
- }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @import { ParserState, Token, TokenType } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * @param {ParserState} state
7
+ * @returns {Token}
8
+ */
9
+ export function current(state) {
10
+ return state.tokens[state.pos]
11
+ }
12
+
13
+ /**
14
+ * @param {ParserState} state
15
+ * @param {number} offset
16
+ * @returns {Token}
17
+ */
18
+ export function peekToken(state, offset) {
19
+ const idx = state.pos + offset
20
+ if (idx >= state.tokens.length) {
21
+ return state.tokens[state.tokens.length - 1]
22
+ }
23
+ return state.tokens[idx]
24
+ }
25
+
26
+ /**
27
+ * @param {ParserState} state
28
+ * @returns {Token}
29
+ */
30
+ export function consume(state) {
31
+ const tok = current(state)
32
+ if (state.pos < state.tokens.length - 1) {
33
+ state.pos += 1
34
+ }
35
+ return tok
36
+ }
37
+
38
+ /**
39
+ * @param {ParserState} state
40
+ * @param {TokenType} type
41
+ * @param {string} [value]
42
+ * @returns {boolean}
43
+ */
44
+ export function match(state, type, value) {
45
+ const tok = current(state)
46
+ if (tok.type !== type) return false
47
+ if (typeof value === 'string' && tok.value !== value) return false
48
+ consume(state)
49
+ return true
50
+ }
51
+
52
+ /**
53
+ * @param {ParserState} state
54
+ * @param {TokenType} type
55
+ * @param {string} value
56
+ * @returns {Token}
57
+ */
58
+ export function expect(state, type, value) {
59
+ const tok = current(state)
60
+ if (tok.type !== type || tok.value !== value) {
61
+ throw parseError(state, value)
62
+ }
63
+ consume(state)
64
+ return tok
65
+ }
66
+
67
+ /**
68
+ * @param {ParserState} state
69
+ * @returns {Token}
70
+ */
71
+ export function expectIdentifier(state) {
72
+ const tok = current(state)
73
+ if (tok.type !== 'identifier') {
74
+ throw parseError(state, 'identifier')
75
+ }
76
+ consume(state)
77
+ return tok
78
+ }
79
+
80
+ /**
81
+ * Helper function to create consistent parser error messages.
82
+ * @param {ParserState} state
83
+ * @param {string} expected - Description of what was expected
84
+ * @returns {Error}
85
+ */
86
+ export function parseError(state, expected) {
87
+ const tok = current(state)
88
+ const prevToken = state.tokens[state.pos - 1]
89
+ const after = prevToken ? ` after "${prevToken.originalValue ?? prevToken.value}"` : ''
90
+ const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
91
+ return new Error(`Expected ${expected}${after} but found ${found} at position ${tok.position}`)
92
+ }
package/src/types.d.ts CHANGED
@@ -1,10 +1,21 @@
1
1
 
2
+ /**
3
+ * Hints passed to data sources for query optimization.
4
+ * All hints are optional and "best effort" - sources may ignore them.
5
+ */
6
+ export interface QueryHints {
7
+ columns?: string[] // columns needed
8
+ where?: ExprNode // where clause
9
+ limit?: number
10
+ offset?: number
11
+ }
12
+
2
13
  /**
3
14
  * Async data source for streaming SQL execution.
4
15
  * Provides an async iterator over rows.
5
16
  */
6
17
  export interface AsyncDataSource {
7
- getRows(): AsyncIterable<AsyncRow>
18
+ getRows(hints?: QueryHints): AsyncIterable<AsyncRow>
8
19
  }
9
20
  export type AsyncRow = Record<string, AsyncCell>
10
21
  export type AsyncCell = () => Promise<SqlPrimitive>
@@ -43,17 +54,9 @@ export interface FromSubquery {
43
54
  alias: string
44
55
  }
45
56
 
46
- export type BinaryOp =
47
- | 'AND'
48
- | 'OR'
49
- | '='
50
- | '!='
51
- | '<>'
52
- | '<'
53
- | '>'
54
- | '<='
55
- | '>='
56
- | 'LIKE'
57
+ export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp
58
+
59
+ export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
57
60
 
58
61
  export interface LiteralNode {
59
62
  type: 'literal'
@@ -194,16 +197,6 @@ export interface ParserState {
194
197
  pos: number
195
198
  }
196
199
 
197
- export interface ExprCursor {
198
- current(): Token
199
- peek(offset: number): Token
200
- consume(): Token
201
- match(type: TokenType, value?: string): boolean
202
- expect(type: TokenType, value: string): Token
203
- expectIdentifier(): Token
204
- parseSubquery: () => SelectStatement
205
- }
206
-
207
200
  // Tokenizer types
208
201
  export type TokenType =
209
202
  | 'keyword'
package/src/validation.js CHANGED
@@ -23,3 +23,14 @@ export function isStringFunc(name) {
23
23
  export function isBinaryOp(op) {
24
24
  return ['=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
25
25
  }
26
+
27
+ // Keywords that cannot be used as implicit aliases after a column
28
+ export const RESERVED_AFTER_COLUMN = new Set([
29
+ 'FROM', 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
30
+ ])
31
+
32
+ // Keywords that cannot be used as table aliases
33
+ export const RESERVED_AFTER_TABLE = new Set([
34
+ 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET', 'JOIN', 'INNER',
35
+ 'LEFT', 'RIGHT', 'FULL', 'CROSS', 'ON',
36
+ ])