squirreling 0.4.4 → 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/package.json +1 -1
- package/src/execute/utils.js +2 -1
- package/src/parse/comparison.js +48 -47
- package/src/parse/expression.js +83 -68
- package/src/parse/joins.js +77 -0
- package/src/parse/parse.js +14 -230
- package/src/parse/state.js +92 -0
- package/src/types.d.ts +0 -10
- package/src/validation.js +11 -0
package/package.json
CHANGED
package/src/execute/utils.js
CHANGED
|
@@ -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
|
|
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/parse/comparison.js
CHANGED
|
@@ -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 {
|
|
6
|
+
* @import { ExprNode, ParserState } from '../types.js'
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* @param {
|
|
10
|
+
* @param {ParserState} state
|
|
10
11
|
* @returns {ExprNode}
|
|
11
12
|
*/
|
|
12
|
-
export function parseComparison(
|
|
13
|
-
const left = parsePrimary(
|
|
14
|
-
const tok =
|
|
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
|
-
|
|
19
|
-
const notToken =
|
|
19
|
+
consume(state)
|
|
20
|
+
const notToken = current(state)
|
|
20
21
|
if (notToken.type === 'keyword' && notToken.value === 'NOT') {
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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 =
|
|
40
|
+
const nextTok = peekToken(state, 1)
|
|
40
41
|
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const right = parsePrimary(
|
|
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
|
-
|
|
59
|
-
const right = parsePrimary(
|
|
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 =
|
|
71
|
+
const nextTok = peekToken(state, 1)
|
|
71
72
|
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const lower = parsePrimary(
|
|
75
|
-
|
|
76
|
-
const upper = parsePrimary(
|
|
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
|
-
|
|
89
|
-
const lower = parsePrimary(
|
|
90
|
-
|
|
91
|
-
const upper = parsePrimary(
|
|
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 =
|
|
104
|
+
const nextTok = peekToken(state, 1)
|
|
104
105
|
if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
|
|
105
|
-
|
|
106
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
130
|
+
consume(state) // '('
|
|
130
131
|
/** @type {ExprNode[]} */
|
|
131
132
|
const values = []
|
|
132
133
|
while (true) {
|
|
133
|
-
values.push(parseExpression(
|
|
134
|
-
if (!
|
|
134
|
+
values.push(parseExpression(state))
|
|
135
|
+
if (!match(state, 'comma')) break
|
|
135
136
|
}
|
|
136
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
171
|
+
consume(state) // '('
|
|
171
172
|
/** @type {ExprNode[]} */
|
|
172
173
|
const values = []
|
|
173
174
|
while (true) {
|
|
174
|
-
values.push(parseExpression(
|
|
175
|
-
if (!
|
|
175
|
+
values.push(parseExpression(state))
|
|
176
|
+
if (!match(state, 'comma')) break
|
|
176
177
|
}
|
|
177
|
-
|
|
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
|
-
|
|
188
|
-
const right = parsePrimary(
|
|
188
|
+
consume(state)
|
|
189
|
+
const right = parsePrimary(state)
|
|
189
190
|
return {
|
|
190
191
|
type: 'binary',
|
|
191
192
|
op: tok.value,
|
package/src/parse/expression.js
CHANGED
|
@@ -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 {
|
|
7
|
+
* @import { ExprNode, ParserState, SelectStatement, WhenClause } from '../types.js'
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
* @param {
|
|
11
|
+
* @param {ParserState} state
|
|
10
12
|
* @returns {ExprNode}
|
|
11
13
|
*/
|
|
12
|
-
export function parseExpression(
|
|
13
|
-
return parseOr(
|
|
14
|
+
export function parseExpression(state) {
|
|
15
|
+
return parseOr(state)
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
* @param {
|
|
19
|
+
* @param {ParserState} state
|
|
18
20
|
* @returns {ExprNode}
|
|
19
21
|
*/
|
|
20
|
-
export function parsePrimary(
|
|
21
|
-
const tok =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
36
|
-
const expr = parseExpression(
|
|
37
|
-
|
|
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 =
|
|
44
|
+
const next = peekToken(state, 1)
|
|
43
45
|
|
|
44
46
|
// CAST expression
|
|
45
47
|
if (tok.value === 'CAST' && next.type === 'paren' && next.value === '(') {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const expr = parseExpression(
|
|
49
|
-
|
|
50
|
-
const typeTok =
|
|
51
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
+
consume(state) // function name
|
|
71
|
+
consume(state) // '('
|
|
70
72
|
|
|
71
73
|
/** @type {ExprNode[]} */
|
|
72
74
|
const args = []
|
|
73
75
|
|
|
74
|
-
if (
|
|
76
|
+
if (current(state).type !== 'paren' || current(state).value !== ')') {
|
|
75
77
|
while (true) {
|
|
76
78
|
// Handle COUNT(*) - treat * as a special identifier
|
|
77
|
-
if (
|
|
78
|
-
|
|
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(
|
|
86
|
+
const arg = parseExpression(state)
|
|
85
87
|
args.push(arg)
|
|
86
88
|
}
|
|
87
|
-
if (!
|
|
89
|
+
if (!match(state, 'comma')) break
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
|
|
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
|
-
|
|
102
|
+
consume(state)
|
|
101
103
|
let name = tok.value
|
|
102
104
|
|
|
103
105
|
// table.column
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
const columnTok =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
consume(state)
|
|
135
137
|
return { type: 'literal', value: true }
|
|
136
138
|
}
|
|
137
139
|
if (tok.value === 'FALSE') {
|
|
138
|
-
|
|
140
|
+
consume(state)
|
|
139
141
|
return { type: 'literal', value: false }
|
|
140
142
|
}
|
|
141
143
|
if (tok.value === 'NULL') {
|
|
142
|
-
|
|
144
|
+
consume(state)
|
|
143
145
|
return { type: 'literal', value: null }
|
|
144
146
|
}
|
|
145
147
|
if (tok.value === 'EXISTS') {
|
|
146
|
-
|
|
147
|
-
const subquery =
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
164
|
+
caseExpr = parseExpression(state)
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
// Parse WHEN clauses
|
|
166
168
|
/** @type {WhenClause[]} */
|
|
167
169
|
const whenClauses = []
|
|
168
|
-
while (
|
|
169
|
-
const condition = parseExpression(
|
|
170
|
-
|
|
171
|
-
const result = parseExpression(
|
|
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 (
|
|
183
|
-
elseResult = parseExpression(
|
|
184
|
+
if (match(state, 'keyword', 'ELSE')) {
|
|
185
|
+
elseResult = parseExpression(state)
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
|
|
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
|
-
|
|
199
|
-
const argument = parsePrimary(
|
|
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 {
|
|
214
|
+
* @param {ParserState} state
|
|
213
215
|
* @returns {ExprNode}
|
|
214
216
|
*/
|
|
215
|
-
function parseOr(
|
|
216
|
-
let node = parseAnd(
|
|
217
|
-
while (
|
|
218
|
-
const right = parseAnd(
|
|
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 {
|
|
232
|
+
* @param {ParserState} state
|
|
231
233
|
* @returns {ExprNode}
|
|
232
234
|
*/
|
|
233
|
-
function parseAnd(
|
|
234
|
-
let node = parseNot(
|
|
235
|
-
while (
|
|
236
|
-
const right = parseNot(
|
|
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 {
|
|
250
|
+
* @param {ParserState} state
|
|
249
251
|
* @returns {ExprNode}
|
|
250
252
|
*/
|
|
251
|
-
function parseNot(
|
|
252
|
-
if (
|
|
253
|
+
function parseNot(state) {
|
|
254
|
+
if (match(state, 'keyword', 'NOT')) {
|
|
253
255
|
// Check for NOT EXISTS
|
|
254
|
-
const nextTok =
|
|
256
|
+
const nextTok = current(state)
|
|
255
257
|
if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
|
|
256
|
-
|
|
257
|
-
const subquery =
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/src/parse/parse.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
@@ -197,16 +197,6 @@ export interface ParserState {
|
|
|
197
197
|
pos: number
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
export interface ExprCursor {
|
|
201
|
-
current(): Token
|
|
202
|
-
peek(offset: number): Token
|
|
203
|
-
consume(): Token
|
|
204
|
-
match(type: TokenType, value?: string): boolean
|
|
205
|
-
expect(type: TokenType, value: string): Token
|
|
206
|
-
expectIdentifier(): Token
|
|
207
|
-
parseSubquery: () => SelectStatement
|
|
208
|
-
}
|
|
209
|
-
|
|
210
200
|
// Tokenizer types
|
|
211
201
|
export type TokenType =
|
|
212
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
|
+
])
|