@typed-sql/ast 1.0.0-beta.0
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/packages/ast/src/index.d.ts +6 -0
- package/dist/packages/ast/src/index.d.ts.map +1 -0
- package/dist/packages/ast/src/index.js +3 -0
- package/dist/packages/ast/src/index.js.map +1 -0
- package/dist/packages/ast/src/parser.d.ts +14 -0
- package/dist/packages/ast/src/parser.d.ts.map +1 -0
- package/dist/packages/ast/src/parser.js +800 -0
- package/dist/packages/ast/src/parser.js.map +1 -0
- package/dist/packages/ast/src/tokenizer.d.ts +15 -0
- package/dist/packages/ast/src/tokenizer.d.ts.map +1 -0
- package/dist/packages/ast/src/tokenizer.js +285 -0
- package/dist/packages/ast/src/tokenizer.js.map +1 -0
- package/dist/packages/ast/src/types.d.ts +249 -0
- package/dist/packages/ast/src/types.d.ts.map +1 -0
- package/dist/packages/ast/src/types.js +9 -0
- package/dist/packages/ast/src/types.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import { SqlTokenizeError, tokenize } from "./tokenizer.js";
|
|
2
|
+
import { mergeRanges, } from "./types.js";
|
|
3
|
+
export const DEFAULT_MAX_PARSE_DEPTH = 128;
|
|
4
|
+
export class SqlParseError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
range;
|
|
7
|
+
constructor(message, range, code = "TSQ001") {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "SqlParseError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.range = range;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const precedence = new Map([
|
|
15
|
+
["OR", 1],
|
|
16
|
+
["AND", 2],
|
|
17
|
+
["=", 3], ["!=", 3], ["<>", 3], ["<", 3], ["<=", 3], [">", 3], [">=", 3],
|
|
18
|
+
["IS", 3], ["LIKE", 3], ["ILIKE", 3], ["SIMILAR TO", 3], ["~", 3], ["~*", 3], ["!~", 3], ["!~*", 3],
|
|
19
|
+
["@>", 3], ["<@", 3], ["?", 3], ["?|", 3], ["?&", 3], ["&&", 3],
|
|
20
|
+
["||", 4], ["->", 4], ["->>", 4], ["#>", 4], ["#>>", 4],
|
|
21
|
+
["+", 5], ["-", 5], ["*", 6], ["/", 6], ["%", 6], ["^", 7],
|
|
22
|
+
]);
|
|
23
|
+
const typeContinuationWords = new Set([
|
|
24
|
+
"BIT", "CHARACTER", "DOUBLE", "INTERVAL", "PRECISION", "TIME", "TIMESTAMP",
|
|
25
|
+
"VARYING", "WITH", "WITHOUT", "ZONE",
|
|
26
|
+
]);
|
|
27
|
+
class Parser {
|
|
28
|
+
#source;
|
|
29
|
+
#tokens;
|
|
30
|
+
#maxDepth;
|
|
31
|
+
#syntax;
|
|
32
|
+
#index = 0;
|
|
33
|
+
#depth = 0;
|
|
34
|
+
constructor(source, options) {
|
|
35
|
+
this.#source = source;
|
|
36
|
+
this.#tokens = tokenize(source, options);
|
|
37
|
+
this.#maxDepth = options.maxDepth ?? DEFAULT_MAX_PARSE_DEPTH;
|
|
38
|
+
this.#syntax = options.syntax ?? "postgres";
|
|
39
|
+
if (!Number.isSafeInteger(this.#maxDepth) || this.#maxDepth < 1)
|
|
40
|
+
throw new TypeError("maxDepth must be a positive safe integer");
|
|
41
|
+
}
|
|
42
|
+
parse() {
|
|
43
|
+
const statement = this.#parseStatement();
|
|
44
|
+
this.#matchPunctuation(";");
|
|
45
|
+
this.#expect("eof", "end of query");
|
|
46
|
+
return statement;
|
|
47
|
+
}
|
|
48
|
+
#parseStatement() {
|
|
49
|
+
return this.#nested(() => {
|
|
50
|
+
const withClause = this.#current().value === "WITH" ? this.#parseWithClause() : undefined;
|
|
51
|
+
if (this.#current().value === "SELECT")
|
|
52
|
+
return this.#parseSelect(withClause);
|
|
53
|
+
if (this.#current().value === "INSERT")
|
|
54
|
+
return this.#parseInsert(withClause);
|
|
55
|
+
if (this.#current().value === "UPDATE")
|
|
56
|
+
return this.#parseUpdate(withClause);
|
|
57
|
+
if (this.#current().value === "DELETE")
|
|
58
|
+
return this.#parseDelete(withClause);
|
|
59
|
+
throw this.#error(`Expected SELECT, INSERT, UPDATE, or DELETE, found ${this.#current().text || "end of query"}`, this.#current().range);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
#parseWithClause() {
|
|
63
|
+
const start = this.#expectKeyword("WITH").range;
|
|
64
|
+
const recursive = this.#matchKeyword("RECURSIVE");
|
|
65
|
+
const queries = [];
|
|
66
|
+
do {
|
|
67
|
+
const name = this.#parseIdentifier();
|
|
68
|
+
const columns = [];
|
|
69
|
+
if (this.#matchPunctuation("(")) {
|
|
70
|
+
if (!this.#matchPunctuation(")")) {
|
|
71
|
+
do
|
|
72
|
+
columns.push(this.#parseIdentifier());
|
|
73
|
+
while (this.#matchPunctuation(","));
|
|
74
|
+
this.#expectPunctuation(")");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
this.#expectKeyword("AS");
|
|
78
|
+
this.#expectPunctuation("(");
|
|
79
|
+
const statement = this.#parseStatement();
|
|
80
|
+
const close = this.#expectPunctuation(")");
|
|
81
|
+
queries.push({ name, columns, statement, range: mergeRanges(name.range, close.range) });
|
|
82
|
+
} while (this.#matchPunctuation(","));
|
|
83
|
+
return { recursive, queries, range: mergeRanges(start, queries.at(-1)?.range ?? start) };
|
|
84
|
+
}
|
|
85
|
+
#parseSelect(withClause) {
|
|
86
|
+
const start = withClause?.range ?? this.#current().range;
|
|
87
|
+
this.#expectKeyword("SELECT");
|
|
88
|
+
let distinct = false;
|
|
89
|
+
const distinctOn = [];
|
|
90
|
+
if (this.#matchKeyword("DISTINCT")) {
|
|
91
|
+
distinct = true;
|
|
92
|
+
if (this.#matchKeyword("ON")) {
|
|
93
|
+
this.#expectPunctuation("(");
|
|
94
|
+
distinctOn.push(...this.#parseExpressionList());
|
|
95
|
+
this.#expectPunctuation(")");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
this.#matchKeyword("ALL");
|
|
100
|
+
const columns = this.#parseSelectList();
|
|
101
|
+
let from;
|
|
102
|
+
const joins = [];
|
|
103
|
+
let where;
|
|
104
|
+
const groupBy = [];
|
|
105
|
+
let having;
|
|
106
|
+
const windows = [];
|
|
107
|
+
let orderBy = [];
|
|
108
|
+
let limit;
|
|
109
|
+
let offset;
|
|
110
|
+
if (this.#matchKeyword("FROM")) {
|
|
111
|
+
from = this.#parseTableReference();
|
|
112
|
+
while (true) {
|
|
113
|
+
if (this.#matchPunctuation(",")) {
|
|
114
|
+
const table = this.#parseTableReference();
|
|
115
|
+
joins.push({ kind: "cross", table, range: table.range });
|
|
116
|
+
}
|
|
117
|
+
else if (this.#isJoinStart())
|
|
118
|
+
joins.push(this.#parseJoin());
|
|
119
|
+
else
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (this.#matchKeyword("WHERE"))
|
|
124
|
+
where = this.#parseExpression();
|
|
125
|
+
if (this.#matchKeyword("GROUP")) {
|
|
126
|
+
this.#expectKeyword("BY");
|
|
127
|
+
groupBy.push(...this.#parseExpressionList());
|
|
128
|
+
}
|
|
129
|
+
if (this.#matchKeyword("HAVING"))
|
|
130
|
+
having = this.#parseExpression();
|
|
131
|
+
if (this.#matchKeyword("WINDOW")) {
|
|
132
|
+
do {
|
|
133
|
+
const name = this.#parseIdentifier();
|
|
134
|
+
this.#expectKeyword("AS");
|
|
135
|
+
const specification = this.#parseWindowSpecification();
|
|
136
|
+
windows.push({ name, specification, range: mergeRanges(name.range, specification.range) });
|
|
137
|
+
} while (this.#matchPunctuation(","));
|
|
138
|
+
}
|
|
139
|
+
if (this.#matchKeyword("ORDER")) {
|
|
140
|
+
this.#expectKeyword("BY");
|
|
141
|
+
orderBy = this.#parseOrderByList();
|
|
142
|
+
}
|
|
143
|
+
if (this.#matchKeyword("LIMIT")) {
|
|
144
|
+
limit = this.#parseExpression();
|
|
145
|
+
if (this.#syntax === "mysql" && this.#matchPunctuation(",")) {
|
|
146
|
+
offset = limit;
|
|
147
|
+
limit = this.#parseExpression();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (this.#matchKeyword("OFFSET"))
|
|
151
|
+
offset = this.#parseExpression();
|
|
152
|
+
const end = this.#previous().range;
|
|
153
|
+
return {
|
|
154
|
+
kind: "select",
|
|
155
|
+
...(withClause === undefined ? {} : { with: withClause }),
|
|
156
|
+
distinct,
|
|
157
|
+
distinctOn,
|
|
158
|
+
columns,
|
|
159
|
+
...(from === undefined ? {} : { from }),
|
|
160
|
+
joins,
|
|
161
|
+
...(where === undefined ? {} : { where }),
|
|
162
|
+
groupBy,
|
|
163
|
+
...(having === undefined ? {} : { having }),
|
|
164
|
+
windows,
|
|
165
|
+
orderBy,
|
|
166
|
+
...(limit === undefined ? {} : { limit }),
|
|
167
|
+
...(offset === undefined ? {} : { offset }),
|
|
168
|
+
range: mergeRanges(start, end),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
#parseInsert(withClause) {
|
|
172
|
+
const start = withClause?.range ?? this.#expectKeyword("INSERT").range;
|
|
173
|
+
if (withClause !== undefined)
|
|
174
|
+
this.#expectKeyword("INSERT");
|
|
175
|
+
this.#expectKeyword("INTO");
|
|
176
|
+
const table = this.#parseNamedTableReference(true);
|
|
177
|
+
const columns = [];
|
|
178
|
+
if (this.#matchPunctuation("(")) {
|
|
179
|
+
if (!this.#matchPunctuation(")")) {
|
|
180
|
+
do
|
|
181
|
+
columns.push(this.#parseIdentifier());
|
|
182
|
+
while (this.#matchPunctuation(","));
|
|
183
|
+
this.#expectPunctuation(")");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let source;
|
|
187
|
+
if (this.#matchKeyword("DEFAULT")) {
|
|
188
|
+
const sourceStart = this.#previous().range;
|
|
189
|
+
const end = this.#expectKeyword("VALUES").range;
|
|
190
|
+
source = { kind: "default-values", range: mergeRanges(sourceStart, end) };
|
|
191
|
+
}
|
|
192
|
+
else if (this.#matchKeyword("VALUES")) {
|
|
193
|
+
const sourceStart = this.#previous().range;
|
|
194
|
+
const rows = [];
|
|
195
|
+
do {
|
|
196
|
+
this.#expectPunctuation("(");
|
|
197
|
+
const row = this.#matchPunctuation(")") ? [] : [...this.#parseExpressionList()];
|
|
198
|
+
const close = this.#previous().value === ")" ? this.#previous() : this.#expectPunctuation(")");
|
|
199
|
+
rows.push(row);
|
|
200
|
+
if (close.value !== ")")
|
|
201
|
+
throw this.#error("Expected ) after VALUES row", close.range);
|
|
202
|
+
} while (this.#matchPunctuation(","));
|
|
203
|
+
source = { kind: "values", rows, range: mergeRanges(sourceStart, this.#previous().range) };
|
|
204
|
+
}
|
|
205
|
+
else if (this.#current().value === "SELECT" || this.#current().value === "WITH") {
|
|
206
|
+
const statement = this.#parseStatement();
|
|
207
|
+
if (statement.kind !== "select")
|
|
208
|
+
throw this.#error("INSERT source must be SELECT or VALUES", statement.range);
|
|
209
|
+
source = statement;
|
|
210
|
+
}
|
|
211
|
+
else
|
|
212
|
+
throw this.#error("Expected VALUES, DEFAULT VALUES, or SELECT after INSERT target", this.#current().range);
|
|
213
|
+
const returning = this.#matchKeyword("RETURNING") ? this.#parseSelectList() : [];
|
|
214
|
+
return {
|
|
215
|
+
kind: "insert",
|
|
216
|
+
...(withClause === undefined ? {} : { with: withClause }),
|
|
217
|
+
table,
|
|
218
|
+
columns,
|
|
219
|
+
source,
|
|
220
|
+
returning,
|
|
221
|
+
range: mergeRanges(start, this.#previous().range),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
#parseUpdate(withClause) {
|
|
225
|
+
const start = withClause?.range ?? this.#expectKeyword("UPDATE").range;
|
|
226
|
+
if (withClause !== undefined)
|
|
227
|
+
this.#expectKeyword("UPDATE");
|
|
228
|
+
const table = this.#parseNamedTableReference(true);
|
|
229
|
+
this.#expectKeyword("SET");
|
|
230
|
+
const assignments = [];
|
|
231
|
+
do {
|
|
232
|
+
const column = this.#parseIdentifier();
|
|
233
|
+
this.#expectOperator("=");
|
|
234
|
+
const value = this.#parseExpression();
|
|
235
|
+
assignments.push({ column, value, range: mergeRanges(column.range, value.range) });
|
|
236
|
+
} while (this.#matchPunctuation(","));
|
|
237
|
+
let from;
|
|
238
|
+
const joins = [];
|
|
239
|
+
if (this.#matchKeyword("FROM")) {
|
|
240
|
+
from = this.#parseTableReference();
|
|
241
|
+
while (this.#isJoinStart())
|
|
242
|
+
joins.push(this.#parseJoin());
|
|
243
|
+
}
|
|
244
|
+
const where = this.#matchKeyword("WHERE") ? this.#parseExpression() : undefined;
|
|
245
|
+
const returning = this.#matchKeyword("RETURNING") ? this.#parseSelectList() : [];
|
|
246
|
+
return {
|
|
247
|
+
kind: "update",
|
|
248
|
+
...(withClause === undefined ? {} : { with: withClause }),
|
|
249
|
+
table,
|
|
250
|
+
assignments,
|
|
251
|
+
...(from === undefined ? {} : { from }),
|
|
252
|
+
joins,
|
|
253
|
+
...(where === undefined ? {} : { where }),
|
|
254
|
+
returning,
|
|
255
|
+
range: mergeRanges(start, this.#previous().range),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
#parseDelete(withClause) {
|
|
259
|
+
const start = withClause?.range ?? this.#expectKeyword("DELETE").range;
|
|
260
|
+
if (withClause !== undefined)
|
|
261
|
+
this.#expectKeyword("DELETE");
|
|
262
|
+
this.#expectKeyword("FROM");
|
|
263
|
+
const table = this.#parseNamedTableReference(true);
|
|
264
|
+
const using = [];
|
|
265
|
+
if (this.#matchKeyword("USING")) {
|
|
266
|
+
do
|
|
267
|
+
using.push(this.#parseTableReference());
|
|
268
|
+
while (this.#matchPunctuation(","));
|
|
269
|
+
}
|
|
270
|
+
const where = this.#matchKeyword("WHERE") ? this.#parseExpression() : undefined;
|
|
271
|
+
const returning = this.#matchKeyword("RETURNING") ? this.#parseSelectList() : [];
|
|
272
|
+
return {
|
|
273
|
+
kind: "delete",
|
|
274
|
+
...(withClause === undefined ? {} : { with: withClause }),
|
|
275
|
+
table,
|
|
276
|
+
using,
|
|
277
|
+
...(where === undefined ? {} : { where }),
|
|
278
|
+
returning,
|
|
279
|
+
range: mergeRanges(start, this.#previous().range),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
#parseSelectList() {
|
|
283
|
+
const items = [];
|
|
284
|
+
do {
|
|
285
|
+
const expression = this.#parseExpression();
|
|
286
|
+
let alias;
|
|
287
|
+
if (this.#matchKeyword("AS"))
|
|
288
|
+
alias = this.#parseIdentifier();
|
|
289
|
+
else if (this.#current().kind === "identifier" || this.#current().kind === "quoted-identifier")
|
|
290
|
+
alias = this.#parseIdentifier();
|
|
291
|
+
items.push({ expression, ...(alias === undefined ? {} : { alias }), range: mergeRanges(expression.range, alias?.range ?? expression.range) });
|
|
292
|
+
} while (this.#matchPunctuation(","));
|
|
293
|
+
return items;
|
|
294
|
+
}
|
|
295
|
+
#parseTableReference() {
|
|
296
|
+
const lateral = this.#matchKeyword("LATERAL");
|
|
297
|
+
const start = lateral ? this.#previous().range : this.#current().range;
|
|
298
|
+
if (this.#matchPunctuation("(")) {
|
|
299
|
+
if (!this.#isStatementStart()) {
|
|
300
|
+
throw this.#error("A parenthesized FROM item must be a SELECT subquery", this.#current().range);
|
|
301
|
+
}
|
|
302
|
+
const statement = this.#parseStatement();
|
|
303
|
+
if (statement.kind !== "select")
|
|
304
|
+
throw this.#error("FROM subquery must be SELECT", statement.range);
|
|
305
|
+
this.#expectPunctuation(")");
|
|
306
|
+
this.#matchKeyword("AS");
|
|
307
|
+
const alias = this.#parseIdentifier();
|
|
308
|
+
return { kind: "subquery", query: statement, alias, lateral, range: mergeRanges(start, alias.range) };
|
|
309
|
+
}
|
|
310
|
+
return this.#parseNamedTableReference(true, lateral, start);
|
|
311
|
+
}
|
|
312
|
+
#parseNamedTableReference(allowAlias, lateral = false, start = this.#current().range) {
|
|
313
|
+
const first = this.#parseIdentifier();
|
|
314
|
+
let schema;
|
|
315
|
+
let name = first;
|
|
316
|
+
if (this.#matchPunctuation(".")) {
|
|
317
|
+
schema = first;
|
|
318
|
+
name = this.#parseIdentifier();
|
|
319
|
+
}
|
|
320
|
+
let alias;
|
|
321
|
+
if (allowAlias && this.#matchKeyword("AS"))
|
|
322
|
+
alias = this.#parseIdentifier();
|
|
323
|
+
else if (allowAlias && (this.#current().kind === "identifier" || this.#current().kind === "quoted-identifier"))
|
|
324
|
+
alias = this.#parseIdentifier();
|
|
325
|
+
return {
|
|
326
|
+
kind: "table",
|
|
327
|
+
name,
|
|
328
|
+
...(schema === undefined ? {} : { schema }),
|
|
329
|
+
...(alias === undefined ? {} : { alias }),
|
|
330
|
+
lateral,
|
|
331
|
+
range: mergeRanges(start, alias?.range ?? name.range),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
#isJoinStart() {
|
|
335
|
+
return ["JOIN", "INNER", "LEFT", "RIGHT", "FULL", "CROSS"].includes(this.#current().value);
|
|
336
|
+
}
|
|
337
|
+
#parseJoin() {
|
|
338
|
+
const start = this.#current().range;
|
|
339
|
+
let kind = "inner";
|
|
340
|
+
if (this.#matchKeyword("INNER"))
|
|
341
|
+
kind = "inner";
|
|
342
|
+
else if (this.#matchKeyword("LEFT"))
|
|
343
|
+
kind = "left";
|
|
344
|
+
else if (this.#matchKeyword("RIGHT"))
|
|
345
|
+
kind = "right";
|
|
346
|
+
else if (this.#matchKeyword("FULL"))
|
|
347
|
+
kind = "full";
|
|
348
|
+
else if (this.#matchKeyword("CROSS"))
|
|
349
|
+
kind = "cross";
|
|
350
|
+
this.#matchKeyword("OUTER");
|
|
351
|
+
this.#expectKeyword("JOIN");
|
|
352
|
+
const table = this.#parseTableReference();
|
|
353
|
+
if (kind === "cross")
|
|
354
|
+
return { kind, table, range: mergeRanges(start, table.range) };
|
|
355
|
+
if (this.#matchKeyword("ON")) {
|
|
356
|
+
const on = this.#parseExpression();
|
|
357
|
+
return { kind, table, on, range: mergeRanges(start, on.range) };
|
|
358
|
+
}
|
|
359
|
+
if (this.#matchKeyword("USING")) {
|
|
360
|
+
this.#expectPunctuation("(");
|
|
361
|
+
const using = [];
|
|
362
|
+
do
|
|
363
|
+
using.push(this.#parseIdentifier());
|
|
364
|
+
while (this.#matchPunctuation(","));
|
|
365
|
+
const close = this.#expectPunctuation(")");
|
|
366
|
+
return { kind, table, using, range: mergeRanges(start, close.range) };
|
|
367
|
+
}
|
|
368
|
+
throw this.#error("JOIN requires ON or USING", this.#current().range);
|
|
369
|
+
}
|
|
370
|
+
#parseExpressionList() {
|
|
371
|
+
const expressions = [];
|
|
372
|
+
do
|
|
373
|
+
expressions.push(this.#parseExpression());
|
|
374
|
+
while (this.#matchPunctuation(","));
|
|
375
|
+
return expressions;
|
|
376
|
+
}
|
|
377
|
+
#parseOrderByList() {
|
|
378
|
+
const orderBy = [];
|
|
379
|
+
do {
|
|
380
|
+
const expression = this.#parseExpression();
|
|
381
|
+
const direction = this.#matchKeyword("ASC") ? "asc" : this.#matchKeyword("DESC") ? "desc" : undefined;
|
|
382
|
+
let nulls;
|
|
383
|
+
if (this.#matchKeyword("NULLS"))
|
|
384
|
+
nulls = this.#matchKeyword("FIRST") ? "first" : (this.#expectKeyword("LAST"), "last");
|
|
385
|
+
orderBy.push({
|
|
386
|
+
expression,
|
|
387
|
+
...(direction === undefined ? {} : { direction }),
|
|
388
|
+
...(nulls === undefined ? {} : { nulls }),
|
|
389
|
+
range: mergeRanges(expression.range, this.#previous().range),
|
|
390
|
+
});
|
|
391
|
+
} while (this.#matchPunctuation(","));
|
|
392
|
+
return orderBy;
|
|
393
|
+
}
|
|
394
|
+
#parseExpression(minimum = 0) {
|
|
395
|
+
return this.#nested(() => {
|
|
396
|
+
let left = this.#parseUnary();
|
|
397
|
+
while (true) {
|
|
398
|
+
if (this.#syntax === "postgres" && this.#matchOperator("::")) {
|
|
399
|
+
const databaseType = this.#parseTypeName(false);
|
|
400
|
+
left = { kind: "cast", expression: left, databaseType, syntax: "postgres", range: mergeRanges(left.range, databaseType.range) };
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const negated = this.#current().value === "NOT" && ["IN", "BETWEEN", "LIKE", "ILIKE", "SIMILAR"].includes(this.#peekToken(1).value);
|
|
404
|
+
const special = negated ? this.#peekToken(1).value : this.#current().value;
|
|
405
|
+
if (special === "IN") {
|
|
406
|
+
const strength = 3;
|
|
407
|
+
if (strength < minimum)
|
|
408
|
+
break;
|
|
409
|
+
if (negated)
|
|
410
|
+
this.#advance();
|
|
411
|
+
this.#advance();
|
|
412
|
+
const start = left.range;
|
|
413
|
+
this.#expectPunctuation("(");
|
|
414
|
+
let values;
|
|
415
|
+
if (this.#isStatementStart()) {
|
|
416
|
+
const statement = this.#parseStatement();
|
|
417
|
+
if (statement.kind !== "select")
|
|
418
|
+
throw this.#error("IN subquery must be SELECT", statement.range);
|
|
419
|
+
values = statement;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
if (this.#current().value === ")")
|
|
423
|
+
throw this.#error("IN requires at least one value", this.#current().range);
|
|
424
|
+
values = this.#parseExpressionList();
|
|
425
|
+
}
|
|
426
|
+
const close = this.#previous().value === ")" ? this.#previous() : this.#expectPunctuation(")");
|
|
427
|
+
left = { kind: "in", expression: left, values, negated, range: mergeRanges(start, close.range) };
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (special === "BETWEEN") {
|
|
431
|
+
const strength = 3;
|
|
432
|
+
if (strength < minimum)
|
|
433
|
+
break;
|
|
434
|
+
if (negated)
|
|
435
|
+
this.#advance();
|
|
436
|
+
this.#advance();
|
|
437
|
+
const lower = this.#parseExpression(strength + 1);
|
|
438
|
+
this.#expectKeyword("AND");
|
|
439
|
+
const upper = this.#parseExpression(strength + 1);
|
|
440
|
+
left = { kind: "between", expression: left, lower, upper, negated, range: mergeRanges(left.range, upper.range) };
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
let operator = this.#current().value.toUpperCase();
|
|
444
|
+
if (special === "SIMILAR")
|
|
445
|
+
operator = negated ? "NOT SIMILAR TO" : "SIMILAR TO";
|
|
446
|
+
else if (negated)
|
|
447
|
+
operator = `NOT ${special}`;
|
|
448
|
+
const strength = precedence.get(operator.replace(/^NOT /u, ""));
|
|
449
|
+
if (strength === undefined || strength < minimum)
|
|
450
|
+
break;
|
|
451
|
+
if (negated)
|
|
452
|
+
this.#advance();
|
|
453
|
+
this.#advance();
|
|
454
|
+
if (operator.endsWith("SIMILAR TO"))
|
|
455
|
+
this.#expectKeyword("TO");
|
|
456
|
+
if (operator === "IS") {
|
|
457
|
+
if (this.#matchKeyword("NOT"))
|
|
458
|
+
operator = "IS NOT";
|
|
459
|
+
if (this.#matchKeyword("DISTINCT")) {
|
|
460
|
+
this.#expectKeyword("FROM");
|
|
461
|
+
operator += " DISTINCT FROM";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const right = this.#parseExpression(strength + 1);
|
|
465
|
+
left = { kind: "binary", left, operator, right, range: mergeRanges(left.range, right.range) };
|
|
466
|
+
}
|
|
467
|
+
return left;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
#parseUnary() {
|
|
471
|
+
const token = this.#current();
|
|
472
|
+
if (this.#matchKeyword("NOT") || this.#matchOperator("+") || this.#matchOperator("-") || this.#matchOperator("~")) {
|
|
473
|
+
const expression = this.#parseUnary();
|
|
474
|
+
return { kind: "unary", operator: token.value.toUpperCase(), expression, range: mergeRanges(token.range, expression.range) };
|
|
475
|
+
}
|
|
476
|
+
if (this.#matchKeyword("EXISTS")) {
|
|
477
|
+
this.#expectPunctuation("(");
|
|
478
|
+
const statement = this.#parseStatement();
|
|
479
|
+
if (statement.kind !== "select")
|
|
480
|
+
throw this.#error("EXISTS requires a SELECT subquery", statement.range);
|
|
481
|
+
const close = this.#expectPunctuation(")");
|
|
482
|
+
return { kind: "exists", query: statement, range: mergeRanges(token.range, close.range) };
|
|
483
|
+
}
|
|
484
|
+
return this.#parsePrimary();
|
|
485
|
+
}
|
|
486
|
+
#parsePrimary() {
|
|
487
|
+
const token = this.#current();
|
|
488
|
+
if (this.#matchPunctuation("(")) {
|
|
489
|
+
if (this.#isStatementStart()) {
|
|
490
|
+
const statement = this.#parseStatement();
|
|
491
|
+
if (statement.kind !== "select")
|
|
492
|
+
throw this.#error("Scalar subquery must be SELECT", statement.range);
|
|
493
|
+
const close = this.#expectPunctuation(")");
|
|
494
|
+
return { kind: "subquery", query: statement, range: mergeRanges(token.range, close.range) };
|
|
495
|
+
}
|
|
496
|
+
const first = this.#parseExpression();
|
|
497
|
+
if (this.#matchPunctuation(",")) {
|
|
498
|
+
const elements = [first];
|
|
499
|
+
do
|
|
500
|
+
elements.push(this.#parseExpression());
|
|
501
|
+
while (this.#matchPunctuation(","));
|
|
502
|
+
const close = this.#expectPunctuation(")");
|
|
503
|
+
return { kind: "row", elements, range: mergeRanges(token.range, close.range) };
|
|
504
|
+
}
|
|
505
|
+
this.#expectPunctuation(")");
|
|
506
|
+
return first;
|
|
507
|
+
}
|
|
508
|
+
if (this.#matchKeyword("ARRAY")) {
|
|
509
|
+
this.#expectPunctuation("[");
|
|
510
|
+
const elements = this.#matchPunctuation("]") ? [] : [...this.#parseExpressionList()];
|
|
511
|
+
const close = this.#previous().value === "]" ? this.#previous() : this.#expectPunctuation("]");
|
|
512
|
+
return { kind: "array", elements, range: mergeRanges(token.range, close.range) };
|
|
513
|
+
}
|
|
514
|
+
if (this.#matchKeyword("ROW")) {
|
|
515
|
+
this.#expectPunctuation("(");
|
|
516
|
+
const elements = this.#matchPunctuation(")") ? [] : [...this.#parseExpressionList()];
|
|
517
|
+
const close = this.#previous().value === ")" ? this.#previous() : this.#expectPunctuation(")");
|
|
518
|
+
return { kind: "row", elements, range: mergeRanges(token.range, close.range) };
|
|
519
|
+
}
|
|
520
|
+
if (this.#matchKeyword("CAST"))
|
|
521
|
+
return this.#parseCast(token.range);
|
|
522
|
+
if (this.#matchKeyword("CASE"))
|
|
523
|
+
return this.#parseCase(token.range);
|
|
524
|
+
if (this.#matchKeyword("NULL"))
|
|
525
|
+
return { kind: "literal", value: null, range: token.range };
|
|
526
|
+
if (this.#matchKeyword("TRUE"))
|
|
527
|
+
return { kind: "literal", value: true, range: token.range };
|
|
528
|
+
if (this.#matchKeyword("FALSE"))
|
|
529
|
+
return { kind: "literal", value: false, range: token.range };
|
|
530
|
+
if (this.#matchKeyword("DEFAULT"))
|
|
531
|
+
return { kind: "column", column: { name: "DEFAULT", quoted: false, range: token.range }, range: token.range };
|
|
532
|
+
if (token.kind === "number") {
|
|
533
|
+
this.#advance();
|
|
534
|
+
return { kind: "literal", value: Number(token.value), range: token.range };
|
|
535
|
+
}
|
|
536
|
+
if (token.kind === "string") {
|
|
537
|
+
this.#advance();
|
|
538
|
+
return { kind: "literal", value: token.value, range: token.range };
|
|
539
|
+
}
|
|
540
|
+
if (token.kind === "parameter") {
|
|
541
|
+
this.#advance();
|
|
542
|
+
return { kind: "parameter", index: Number(token.value), range: token.range };
|
|
543
|
+
}
|
|
544
|
+
if (this.#matchOperator("*"))
|
|
545
|
+
return { kind: "star", range: token.range };
|
|
546
|
+
if (this.#isIdentifierLike(token)) {
|
|
547
|
+
const first = this.#parseIdentifier(true);
|
|
548
|
+
if (this.#matchPunctuation("("))
|
|
549
|
+
return this.#parseCall(undefined, first);
|
|
550
|
+
if (this.#matchPunctuation(".")) {
|
|
551
|
+
if (this.#matchOperator("*"))
|
|
552
|
+
return { kind: "star", relation: first, range: mergeRanges(first.range, this.#previous().range) };
|
|
553
|
+
const second = this.#parseIdentifier(true);
|
|
554
|
+
if (this.#matchPunctuation("("))
|
|
555
|
+
return this.#parseCall(first, second);
|
|
556
|
+
return { kind: "column", relation: first, column: second, range: mergeRanges(first.range, second.range) };
|
|
557
|
+
}
|
|
558
|
+
return { kind: "column", column: first, range: first.range };
|
|
559
|
+
}
|
|
560
|
+
throw this.#error(`Expected expression, found ${token.text || "end of query"}`, token.range);
|
|
561
|
+
}
|
|
562
|
+
#parseCall(schema, name) {
|
|
563
|
+
const distinct = this.#matchKeyword("DISTINCT");
|
|
564
|
+
const args = [];
|
|
565
|
+
let close;
|
|
566
|
+
if (this.#matchPunctuation(")"))
|
|
567
|
+
close = this.#previous();
|
|
568
|
+
else {
|
|
569
|
+
do
|
|
570
|
+
args.push(this.#parseExpression());
|
|
571
|
+
while (this.#matchPunctuation(","));
|
|
572
|
+
close = this.#expectPunctuation(")");
|
|
573
|
+
}
|
|
574
|
+
let filter;
|
|
575
|
+
if (this.#syntax === "postgres" && this.#matchKeyword("FILTER")) {
|
|
576
|
+
this.#expectPunctuation("(");
|
|
577
|
+
this.#expectKeyword("WHERE");
|
|
578
|
+
filter = this.#parseExpression();
|
|
579
|
+
close = this.#expectPunctuation(")");
|
|
580
|
+
}
|
|
581
|
+
let over;
|
|
582
|
+
if (this.#matchKeyword("OVER")) {
|
|
583
|
+
if (this.#current().kind === "identifier" || this.#current().kind === "quoted-identifier")
|
|
584
|
+
over = this.#parseIdentifier();
|
|
585
|
+
else
|
|
586
|
+
over = this.#parseWindowSpecification();
|
|
587
|
+
close = { ...close, range: over.range };
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
kind: "call",
|
|
591
|
+
name,
|
|
592
|
+
...(schema === undefined ? {} : { schema }),
|
|
593
|
+
arguments: args,
|
|
594
|
+
distinct,
|
|
595
|
+
...(filter === undefined ? {} : { filter }),
|
|
596
|
+
...(over === undefined ? {} : { over }),
|
|
597
|
+
range: mergeRanges(schema?.range ?? name.range, close.range),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
#parseWindowSpecification() {
|
|
601
|
+
const start = this.#expectPunctuation("(").range;
|
|
602
|
+
const partitionBy = [];
|
|
603
|
+
let orderBy = [];
|
|
604
|
+
if (this.#matchKeyword("PARTITION")) {
|
|
605
|
+
this.#expectKeyword("BY");
|
|
606
|
+
partitionBy.push(...this.#parseExpressionList());
|
|
607
|
+
}
|
|
608
|
+
if (this.#matchKeyword("ORDER")) {
|
|
609
|
+
this.#expectKeyword("BY");
|
|
610
|
+
orderBy = this.#parseOrderByList();
|
|
611
|
+
}
|
|
612
|
+
const close = this.#expectPunctuation(")");
|
|
613
|
+
return { partitionBy, orderBy, range: mergeRanges(start, close.range) };
|
|
614
|
+
}
|
|
615
|
+
#parseCast(start) {
|
|
616
|
+
this.#expectPunctuation("(");
|
|
617
|
+
const expression = this.#parseExpression();
|
|
618
|
+
this.#expectKeyword("AS");
|
|
619
|
+
const databaseType = this.#parseTypeName(true);
|
|
620
|
+
const close = this.#expectPunctuation(")");
|
|
621
|
+
return { kind: "cast", expression, databaseType, syntax: "cast", range: mergeRanges(start, close.range) };
|
|
622
|
+
}
|
|
623
|
+
#parseTypeName(stopAtClose) {
|
|
624
|
+
const start = this.#current();
|
|
625
|
+
if (!this.#isIdentifierLike(start))
|
|
626
|
+
throw this.#error(`Expected identifier, found ${start.text || "end of query"}`, start.range);
|
|
627
|
+
this.#advance();
|
|
628
|
+
let end = start;
|
|
629
|
+
let schemaSeparatorAllowed = true;
|
|
630
|
+
while (true) {
|
|
631
|
+
if (schemaSeparatorAllowed && this.#matchPunctuation(".")) {
|
|
632
|
+
this.#parseIdentifier(true);
|
|
633
|
+
end = this.#previous();
|
|
634
|
+
schemaSeparatorAllowed = false;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
schemaSeparatorAllowed = false;
|
|
638
|
+
if (this.#matchPunctuation("(")) {
|
|
639
|
+
let depth = 1;
|
|
640
|
+
while (depth > 0) {
|
|
641
|
+
const token = this.#current();
|
|
642
|
+
if (token.kind === "eof")
|
|
643
|
+
throw this.#error("Unterminated type modifier", token.range);
|
|
644
|
+
this.#advance();
|
|
645
|
+
if (token.value === "(")
|
|
646
|
+
depth += 1;
|
|
647
|
+
else if (token.value === ")")
|
|
648
|
+
depth -= 1;
|
|
649
|
+
end = token;
|
|
650
|
+
}
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (this.#isIdentifierLike(this.#current()) && typeContinuationWords.has(this.#current().value.toUpperCase())) {
|
|
654
|
+
end = this.#advance();
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (this.#matchPunctuation("[")) {
|
|
658
|
+
end = this.#expectPunctuation("]");
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
if (stopAtClose && this.#current().value !== ")")
|
|
664
|
+
throw this.#error(`Expected ) after type name, found ${this.#current().text}`, this.#current().range);
|
|
665
|
+
return { name: this.#source.slice(start.range.start, end.range.end).trim(), range: mergeRanges(start.range, end.range) };
|
|
666
|
+
}
|
|
667
|
+
#parseCase(start) {
|
|
668
|
+
const operand = this.#current().value === "WHEN" ? undefined : this.#parseExpression();
|
|
669
|
+
const branches = [];
|
|
670
|
+
while (this.#matchKeyword("WHEN")) {
|
|
671
|
+
const whenStart = this.#previous().range;
|
|
672
|
+
const when = this.#parseExpression();
|
|
673
|
+
this.#expectKeyword("THEN");
|
|
674
|
+
const then = this.#parseExpression();
|
|
675
|
+
branches.push({ when, then, range: mergeRanges(whenStart, then.range) });
|
|
676
|
+
}
|
|
677
|
+
if (branches.length === 0)
|
|
678
|
+
throw this.#error("CASE requires at least one WHEN branch", this.#current().range);
|
|
679
|
+
const elseExpression = this.#matchKeyword("ELSE") ? this.#parseExpression() : undefined;
|
|
680
|
+
const end = this.#expectKeyword("END");
|
|
681
|
+
return {
|
|
682
|
+
kind: "case",
|
|
683
|
+
...(operand === undefined ? {} : { operand }),
|
|
684
|
+
branches,
|
|
685
|
+
...(elseExpression === undefined ? {} : { elseExpression }),
|
|
686
|
+
range: mergeRanges(start, end.range),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
#parseIdentifier(allowKeyword = false) {
|
|
690
|
+
const token = this.#current();
|
|
691
|
+
if (!this.#isIdentifierLike(token) || (token.kind === "keyword" && !allowKeyword)) {
|
|
692
|
+
throw this.#error(`Expected identifier, found ${token.text || "end of query"}`, token.range);
|
|
693
|
+
}
|
|
694
|
+
this.#advance();
|
|
695
|
+
return { name: token.value, quoted: token.kind === "quoted-identifier", range: token.range };
|
|
696
|
+
}
|
|
697
|
+
#isIdentifierLike(token) {
|
|
698
|
+
return token.kind === "identifier" || token.kind === "quoted-identifier" || token.kind === "keyword";
|
|
699
|
+
}
|
|
700
|
+
#isStatementStart() {
|
|
701
|
+
return ["SELECT", "INSERT", "UPDATE", "DELETE", "WITH"].includes(this.#current().value);
|
|
702
|
+
}
|
|
703
|
+
#current() {
|
|
704
|
+
return this.#tokens[this.#index] ?? this.#tokens[this.#tokens.length - 1];
|
|
705
|
+
}
|
|
706
|
+
#peekToken(offset) {
|
|
707
|
+
return this.#tokens[this.#index + offset] ?? this.#tokens[this.#tokens.length - 1];
|
|
708
|
+
}
|
|
709
|
+
#previous() {
|
|
710
|
+
return this.#tokens[Math.max(0, this.#index - 1)];
|
|
711
|
+
}
|
|
712
|
+
#advance() {
|
|
713
|
+
const token = this.#current();
|
|
714
|
+
if (token.kind !== "eof")
|
|
715
|
+
this.#index += 1;
|
|
716
|
+
return token;
|
|
717
|
+
}
|
|
718
|
+
#matchKeyword(keyword) {
|
|
719
|
+
if (this.#current().kind === "keyword" && this.#current().value === keyword) {
|
|
720
|
+
this.#advance();
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
#matchOperator(operator) {
|
|
726
|
+
if (this.#current().kind === "operator" && this.#current().value === operator) {
|
|
727
|
+
this.#advance();
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
#matchPunctuation(value) {
|
|
733
|
+
if (this.#current().kind === "punctuation" && this.#current().value === value) {
|
|
734
|
+
this.#advance();
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
#expectKeyword(keyword) {
|
|
740
|
+
const token = this.#current();
|
|
741
|
+
if (!this.#matchKeyword(keyword))
|
|
742
|
+
throw this.#error(`Expected ${keyword}, found ${token.text || "end of query"}`, token.range);
|
|
743
|
+
return token;
|
|
744
|
+
}
|
|
745
|
+
#expectOperator(operator) {
|
|
746
|
+
const token = this.#current();
|
|
747
|
+
if (!this.#matchOperator(operator))
|
|
748
|
+
throw this.#error(`Expected ${operator}, found ${token.text || "end of query"}`, token.range);
|
|
749
|
+
return token;
|
|
750
|
+
}
|
|
751
|
+
#expectPunctuation(value) {
|
|
752
|
+
const token = this.#current();
|
|
753
|
+
if (!this.#matchPunctuation(value))
|
|
754
|
+
throw this.#error(`Expected ${value}, found ${token.text || "end of query"}`, token.range);
|
|
755
|
+
return token;
|
|
756
|
+
}
|
|
757
|
+
#expect(kind, label) {
|
|
758
|
+
const token = this.#current();
|
|
759
|
+
if (token.kind !== kind)
|
|
760
|
+
throw this.#error(`Expected ${label}, found ${token.text || "end of query"}`, token.range);
|
|
761
|
+
this.#advance();
|
|
762
|
+
return token;
|
|
763
|
+
}
|
|
764
|
+
#nested(fn) {
|
|
765
|
+
this.#depth += 1;
|
|
766
|
+
if (this.#depth > this.#maxDepth) {
|
|
767
|
+
this.#depth -= 1;
|
|
768
|
+
throw this.#error(`SQL exceeds the ${this.#maxDepth} level parser nesting limit`, this.#current().range, "TSQ002");
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
return fn();
|
|
772
|
+
}
|
|
773
|
+
finally {
|
|
774
|
+
this.#depth -= 1;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
#error(message, range, code = "TSQ001") {
|
|
778
|
+
return new SqlParseError(message, range, code);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function parse(source, options) {
|
|
782
|
+
try {
|
|
783
|
+
return new Parser(source, options).parse();
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
if (error instanceof SqlTokenizeError)
|
|
787
|
+
throw new SqlParseError(error.message, error.range, error.code);
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
export function parseStatement(source, options = {}) {
|
|
792
|
+
return parse(source, options);
|
|
793
|
+
}
|
|
794
|
+
export function parseSelect(source, options = {}) {
|
|
795
|
+
const statement = parse(source, options);
|
|
796
|
+
if (statement.kind !== "select")
|
|
797
|
+
throw new SqlParseError(`Expected SELECT, found ${statement.kind.toUpperCase()}`, statement.range);
|
|
798
|
+
return statement;
|
|
799
|
+
}
|
|
800
|
+
//# sourceMappingURL=parser.js.map
|