leangraph 1.0.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/LICENSE +21 -0
- package/README.md +456 -0
- package/dist/auth.d.ts +66 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +148 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup.d.ts +51 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +201 -0
- package/dist/backup.js.map +1 -0
- package/dist/cli-helpers.d.ts +17 -0
- package/dist/cli-helpers.d.ts.map +1 -0
- package/dist/cli-helpers.js +121 -0
- package/dist/cli-helpers.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +660 -0
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +118 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +720 -0
- package/dist/db.js.map +1 -0
- package/dist/executor.d.ts +663 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +8578 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/local.d.ts +7 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +119 -0
- package/dist/local.js.map +1 -0
- package/dist/parser.d.ts +365 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +2711 -0
- package/dist/parser.js.map +1 -0
- package/dist/property-value.d.ts +3 -0
- package/dist/property-value.d.ts.map +1 -0
- package/dist/property-value.js +30 -0
- package/dist/property-value.js.map +1 -0
- package/dist/remote.d.ts +6 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +93 -0
- package/dist/remote.js.map +1 -0
- package/dist/routes.d.ts +31 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +202 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/translator.d.ts +330 -0
- package/dist/translator.d.ts.map +1 -0
- package/dist/translator.js +13712 -0
- package/dist/translator.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,2711 @@
|
|
|
1
|
+
// Cypher Parser - Types and Implementation
|
|
2
|
+
const KEYWORDS = new Set([
|
|
3
|
+
"CREATE",
|
|
4
|
+
"MATCH",
|
|
5
|
+
"MERGE",
|
|
6
|
+
"SET",
|
|
7
|
+
"DELETE",
|
|
8
|
+
"DETACH",
|
|
9
|
+
"RETURN",
|
|
10
|
+
"WHERE",
|
|
11
|
+
"AND",
|
|
12
|
+
"OR",
|
|
13
|
+
"XOR",
|
|
14
|
+
"NOT",
|
|
15
|
+
"IN",
|
|
16
|
+
"LIMIT",
|
|
17
|
+
"SKIP",
|
|
18
|
+
"ORDER",
|
|
19
|
+
"BY",
|
|
20
|
+
"ASC",
|
|
21
|
+
"ASCENDING",
|
|
22
|
+
"DESC",
|
|
23
|
+
"DESCENDING",
|
|
24
|
+
"COUNT",
|
|
25
|
+
"ON",
|
|
26
|
+
"TRUE",
|
|
27
|
+
"FALSE",
|
|
28
|
+
"NULL",
|
|
29
|
+
"CONTAINS",
|
|
30
|
+
"STARTS",
|
|
31
|
+
"ENDS",
|
|
32
|
+
"WITH",
|
|
33
|
+
"AS",
|
|
34
|
+
"IS",
|
|
35
|
+
"DISTINCT",
|
|
36
|
+
"OPTIONAL",
|
|
37
|
+
"UNWIND",
|
|
38
|
+
"CASE",
|
|
39
|
+
"WHEN",
|
|
40
|
+
"THEN",
|
|
41
|
+
"ELSE",
|
|
42
|
+
"END",
|
|
43
|
+
"EXISTS",
|
|
44
|
+
"UNION",
|
|
45
|
+
"ALL",
|
|
46
|
+
"ANY",
|
|
47
|
+
"NONE",
|
|
48
|
+
"SINGLE",
|
|
49
|
+
"CALL",
|
|
50
|
+
"YIELD",
|
|
51
|
+
"REMOVE",
|
|
52
|
+
]);
|
|
53
|
+
class Tokenizer {
|
|
54
|
+
input;
|
|
55
|
+
pos = 0;
|
|
56
|
+
line = 1;
|
|
57
|
+
column = 1;
|
|
58
|
+
tokens = [];
|
|
59
|
+
constructor(input) {
|
|
60
|
+
this.input = input;
|
|
61
|
+
}
|
|
62
|
+
canStartNegativeNumber() {
|
|
63
|
+
const prev = this.tokens[this.tokens.length - 1];
|
|
64
|
+
if (!prev)
|
|
65
|
+
return true;
|
|
66
|
+
// Allow "-1" to be tokenized as a single NUMBER only where a new expression
|
|
67
|
+
// can start. Otherwise (e.g., "x-1") it must be DASH + NUMBER.
|
|
68
|
+
switch (prev.type) {
|
|
69
|
+
case "LPAREN":
|
|
70
|
+
case "COMMA":
|
|
71
|
+
case "LBRACKET":
|
|
72
|
+
case "LBRACE":
|
|
73
|
+
case "COLON":
|
|
74
|
+
case "EQUALS":
|
|
75
|
+
case "NOT_EQUALS":
|
|
76
|
+
case "LT":
|
|
77
|
+
case "LTE":
|
|
78
|
+
case "GT":
|
|
79
|
+
case "GTE":
|
|
80
|
+
case "PLUS":
|
|
81
|
+
case "DASH":
|
|
82
|
+
case "STAR":
|
|
83
|
+
case "SLASH":
|
|
84
|
+
case "PERCENT":
|
|
85
|
+
case "CARET":
|
|
86
|
+
case "PIPE":
|
|
87
|
+
case "KEYWORD":
|
|
88
|
+
return true;
|
|
89
|
+
default:
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
tokenize() {
|
|
94
|
+
while (this.pos < this.input.length) {
|
|
95
|
+
this.skipTrivia();
|
|
96
|
+
if (this.pos >= this.input.length)
|
|
97
|
+
break;
|
|
98
|
+
const token = this.nextToken();
|
|
99
|
+
if (token) {
|
|
100
|
+
this.tokens.push(token);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.tokens.push({
|
|
104
|
+
type: "EOF",
|
|
105
|
+
value: "",
|
|
106
|
+
position: this.pos,
|
|
107
|
+
line: this.line,
|
|
108
|
+
column: this.column,
|
|
109
|
+
});
|
|
110
|
+
return this.tokens;
|
|
111
|
+
}
|
|
112
|
+
skipTrivia() {
|
|
113
|
+
while (this.pos < this.input.length) {
|
|
114
|
+
this.skipWhitespace();
|
|
115
|
+
if (this.pos >= this.input.length)
|
|
116
|
+
return;
|
|
117
|
+
const char = this.input[this.pos];
|
|
118
|
+
const next = this.input[this.pos + 1];
|
|
119
|
+
// Line comment: // ... (until end of line)
|
|
120
|
+
if (char === "/" && next === "/") {
|
|
121
|
+
this.pos += 2;
|
|
122
|
+
this.column += 2;
|
|
123
|
+
while (this.pos < this.input.length) {
|
|
124
|
+
const c = this.input[this.pos];
|
|
125
|
+
if (c === "\n" || c === "\r")
|
|
126
|
+
break;
|
|
127
|
+
this.pos++;
|
|
128
|
+
this.column++;
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Block comment: /* ... */
|
|
133
|
+
if (char === "/" && next === "*") {
|
|
134
|
+
const commentStart = this.pos;
|
|
135
|
+
this.pos += 2;
|
|
136
|
+
this.column += 2;
|
|
137
|
+
while (this.pos < this.input.length) {
|
|
138
|
+
const c = this.input[this.pos];
|
|
139
|
+
const n = this.input[this.pos + 1];
|
|
140
|
+
if (c === "*" && n === "/") {
|
|
141
|
+
this.pos += 2;
|
|
142
|
+
this.column += 2;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (c === "\n") {
|
|
146
|
+
this.pos++;
|
|
147
|
+
this.line++;
|
|
148
|
+
this.column = 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (c === "\r") {
|
|
152
|
+
this.pos++;
|
|
153
|
+
if (this.input[this.pos] === "\n") {
|
|
154
|
+
this.pos++;
|
|
155
|
+
}
|
|
156
|
+
this.line++;
|
|
157
|
+
this.column = 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
this.pos++;
|
|
161
|
+
this.column++;
|
|
162
|
+
}
|
|
163
|
+
if (this.pos >= this.input.length && !(this.input[this.pos - 2] === "*" && this.input[this.pos - 1] === "/")) {
|
|
164
|
+
throw new Error(`Unterminated block comment at position ${commentStart}`);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
skipWhitespace() {
|
|
172
|
+
while (this.pos < this.input.length) {
|
|
173
|
+
const char = this.input[this.pos];
|
|
174
|
+
if (char === " " || char === "\t") {
|
|
175
|
+
this.pos++;
|
|
176
|
+
this.column++;
|
|
177
|
+
}
|
|
178
|
+
else if (char === "\n") {
|
|
179
|
+
this.pos++;
|
|
180
|
+
this.line++;
|
|
181
|
+
this.column = 1;
|
|
182
|
+
}
|
|
183
|
+
else if (char === "\r") {
|
|
184
|
+
this.pos++;
|
|
185
|
+
if (this.input[this.pos] === "\n") {
|
|
186
|
+
this.pos++;
|
|
187
|
+
}
|
|
188
|
+
this.line++;
|
|
189
|
+
this.column = 1;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
nextToken() {
|
|
197
|
+
const startPos = this.pos;
|
|
198
|
+
const startLine = this.line;
|
|
199
|
+
const startColumn = this.column;
|
|
200
|
+
const char = this.input[this.pos];
|
|
201
|
+
// Two-character operators
|
|
202
|
+
if (this.pos + 1 < this.input.length) {
|
|
203
|
+
const twoChars = this.input.slice(this.pos, this.pos + 2);
|
|
204
|
+
if (twoChars === "<-") {
|
|
205
|
+
this.pos += 2;
|
|
206
|
+
this.column += 2;
|
|
207
|
+
return { type: "ARROW_LEFT", value: "<-", position: startPos, line: startLine, column: startColumn };
|
|
208
|
+
}
|
|
209
|
+
if (twoChars === "->") {
|
|
210
|
+
this.pos += 2;
|
|
211
|
+
this.column += 2;
|
|
212
|
+
return { type: "ARROW_RIGHT", value: "->", position: startPos, line: startLine, column: startColumn };
|
|
213
|
+
}
|
|
214
|
+
if (twoChars === "<>") {
|
|
215
|
+
this.pos += 2;
|
|
216
|
+
this.column += 2;
|
|
217
|
+
return { type: "NOT_EQUALS", value: "<>", position: startPos, line: startLine, column: startColumn };
|
|
218
|
+
}
|
|
219
|
+
if (twoChars === "<=") {
|
|
220
|
+
this.pos += 2;
|
|
221
|
+
this.column += 2;
|
|
222
|
+
return { type: "LTE", value: "<=", position: startPos, line: startLine, column: startColumn };
|
|
223
|
+
}
|
|
224
|
+
if (twoChars === ">=") {
|
|
225
|
+
this.pos += 2;
|
|
226
|
+
this.column += 2;
|
|
227
|
+
return { type: "GTE", value: ">=", position: startPos, line: startLine, column: startColumn };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Single character tokens
|
|
231
|
+
const singleCharTokens = {
|
|
232
|
+
"(": "LPAREN",
|
|
233
|
+
")": "RPAREN",
|
|
234
|
+
"[": "LBRACKET",
|
|
235
|
+
"]": "RBRACKET",
|
|
236
|
+
"{": "LBRACE",
|
|
237
|
+
"}": "RBRACE",
|
|
238
|
+
":": "COLON",
|
|
239
|
+
",": "COMMA",
|
|
240
|
+
".": "DOT",
|
|
241
|
+
"-": "DASH",
|
|
242
|
+
"+": "PLUS",
|
|
243
|
+
"/": "SLASH",
|
|
244
|
+
"%": "PERCENT",
|
|
245
|
+
"^": "CARET",
|
|
246
|
+
"=": "EQUALS",
|
|
247
|
+
"<": "LT",
|
|
248
|
+
">": "GT",
|
|
249
|
+
"*": "STAR",
|
|
250
|
+
"|": "PIPE",
|
|
251
|
+
};
|
|
252
|
+
// Number - includes floats starting with . like .5
|
|
253
|
+
// Check this before single char tokens so ".5" is parsed as number not DOT
|
|
254
|
+
// But don't match "..3" as ".3" - only match if there's no preceding dot
|
|
255
|
+
if (this.isDigit(char) ||
|
|
256
|
+
(char === "-" && this.isDigit(this.input[this.pos + 1]) && this.canStartNegativeNumber()) ||
|
|
257
|
+
(char === "." && this.isDigit(this.input[this.pos + 1]) && (this.pos === 0 || this.input[this.pos - 1] !== "."))) {
|
|
258
|
+
return this.readNumber(startPos, startLine, startColumn);
|
|
259
|
+
}
|
|
260
|
+
if (singleCharTokens[char]) {
|
|
261
|
+
this.pos++;
|
|
262
|
+
this.column++;
|
|
263
|
+
return { type: singleCharTokens[char], value: char, position: startPos, line: startLine, column: startColumn };
|
|
264
|
+
}
|
|
265
|
+
// Parameter
|
|
266
|
+
if (char === "$") {
|
|
267
|
+
this.pos++;
|
|
268
|
+
this.column++;
|
|
269
|
+
const name = this.readIdentifier();
|
|
270
|
+
return { type: "PARAMETER", value: name, position: startPos, line: startLine, column: startColumn };
|
|
271
|
+
}
|
|
272
|
+
// String
|
|
273
|
+
if (char === "'" || char === '"') {
|
|
274
|
+
return this.readString(char, startPos, startLine, startColumn);
|
|
275
|
+
}
|
|
276
|
+
// Backtick-delimited identifier (escaped identifier)
|
|
277
|
+
if (char === "`") {
|
|
278
|
+
return this.readBacktickIdentifier(startPos, startLine, startColumn);
|
|
279
|
+
}
|
|
280
|
+
// Identifier or keyword
|
|
281
|
+
if (this.isIdentifierStart(char)) {
|
|
282
|
+
const value = this.readIdentifier();
|
|
283
|
+
const upperValue = value.toUpperCase();
|
|
284
|
+
const type = KEYWORDS.has(upperValue) ? "KEYWORD" : "IDENTIFIER";
|
|
285
|
+
// Keywords store uppercase for matching, but we also preserve original casing for when keywords are used as identifiers
|
|
286
|
+
return { type, value: type === "KEYWORD" ? upperValue : value, originalValue: value, position: startPos, line: startLine, column: startColumn };
|
|
287
|
+
}
|
|
288
|
+
throw new Error(`Unexpected character '${char}' at position ${this.pos}`);
|
|
289
|
+
}
|
|
290
|
+
readString(quote, startPos, startLine, startColumn) {
|
|
291
|
+
this.pos++;
|
|
292
|
+
this.column++;
|
|
293
|
+
let value = "";
|
|
294
|
+
while (this.pos < this.input.length) {
|
|
295
|
+
const char = this.input[this.pos];
|
|
296
|
+
if (char === quote) {
|
|
297
|
+
this.pos++;
|
|
298
|
+
this.column++;
|
|
299
|
+
return { type: "STRING", value, position: startPos, line: startLine, column: startColumn };
|
|
300
|
+
}
|
|
301
|
+
if (char === "\\") {
|
|
302
|
+
this.pos++;
|
|
303
|
+
this.column++;
|
|
304
|
+
const escaped = this.input[this.pos];
|
|
305
|
+
if (escaped === "n") {
|
|
306
|
+
value += "\n";
|
|
307
|
+
this.pos++;
|
|
308
|
+
this.column++;
|
|
309
|
+
}
|
|
310
|
+
else if (escaped === "t") {
|
|
311
|
+
value += "\t";
|
|
312
|
+
this.pos++;
|
|
313
|
+
this.column++;
|
|
314
|
+
}
|
|
315
|
+
else if (escaped === "r") {
|
|
316
|
+
value += "\r";
|
|
317
|
+
this.pos++;
|
|
318
|
+
this.column++;
|
|
319
|
+
}
|
|
320
|
+
else if (escaped === "b") {
|
|
321
|
+
value += "\b";
|
|
322
|
+
this.pos++;
|
|
323
|
+
this.column++;
|
|
324
|
+
}
|
|
325
|
+
else if (escaped === "f") {
|
|
326
|
+
value += "\f";
|
|
327
|
+
this.pos++;
|
|
328
|
+
this.column++;
|
|
329
|
+
}
|
|
330
|
+
else if (escaped === "\\") {
|
|
331
|
+
value += "\\";
|
|
332
|
+
this.pos++;
|
|
333
|
+
this.column++;
|
|
334
|
+
}
|
|
335
|
+
else if (escaped === quote) {
|
|
336
|
+
value += quote;
|
|
337
|
+
this.pos++;
|
|
338
|
+
this.column++;
|
|
339
|
+
}
|
|
340
|
+
else if (escaped === "u") {
|
|
341
|
+
// Unicode escape: \uXXXX (4 hex digits required)
|
|
342
|
+
this.pos++;
|
|
343
|
+
this.column++;
|
|
344
|
+
const hexStart = this.pos;
|
|
345
|
+
let hex = "";
|
|
346
|
+
for (let i = 0; i < 4; i++) {
|
|
347
|
+
if (this.pos >= this.input.length) {
|
|
348
|
+
throw new SyntaxError(`Invalid unicode escape sequence: incomplete \\u escape at position ${hexStart - 2}`);
|
|
349
|
+
}
|
|
350
|
+
const c = this.input[this.pos];
|
|
351
|
+
if (!/[0-9a-fA-F]/.test(c)) {
|
|
352
|
+
throw new SyntaxError(`Invalid unicode escape sequence: \\u${hex}${c}`);
|
|
353
|
+
}
|
|
354
|
+
hex += c;
|
|
355
|
+
this.pos++;
|
|
356
|
+
this.column++;
|
|
357
|
+
}
|
|
358
|
+
value += String.fromCharCode(parseInt(hex, 16));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
value += escaped;
|
|
362
|
+
this.pos++;
|
|
363
|
+
this.column++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
value += char;
|
|
368
|
+
this.pos++;
|
|
369
|
+
this.column++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
throw new Error(`Unterminated string at position ${startPos}`);
|
|
373
|
+
}
|
|
374
|
+
readNumber(startPos, startLine, startColumn) {
|
|
375
|
+
let value = "";
|
|
376
|
+
if (this.input[this.pos] === "-") {
|
|
377
|
+
value += "-";
|
|
378
|
+
this.pos++;
|
|
379
|
+
this.column++;
|
|
380
|
+
}
|
|
381
|
+
// Handle numbers starting with . like .5
|
|
382
|
+
if (this.input[this.pos] === ".") {
|
|
383
|
+
value += "0.";
|
|
384
|
+
this.pos++;
|
|
385
|
+
this.column++;
|
|
386
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
387
|
+
value += this.input[this.pos];
|
|
388
|
+
this.pos++;
|
|
389
|
+
this.column++;
|
|
390
|
+
}
|
|
391
|
+
// Handle scientific notation for numbers starting with . (e.g., .5e10)
|
|
392
|
+
if (this.input[this.pos] === "e" || this.input[this.pos] === "E") {
|
|
393
|
+
const nextChar = this.input[this.pos + 1];
|
|
394
|
+
if (this.isDigit(nextChar) ||
|
|
395
|
+
((nextChar === "+" || nextChar === "-") && this.isDigit(this.input[this.pos + 2]))) {
|
|
396
|
+
value += this.input[this.pos]; // 'e' or 'E'
|
|
397
|
+
this.pos++;
|
|
398
|
+
this.column++;
|
|
399
|
+
if (this.input[this.pos] === "+" || this.input[this.pos] === "-") {
|
|
400
|
+
value += this.input[this.pos];
|
|
401
|
+
this.pos++;
|
|
402
|
+
this.column++;
|
|
403
|
+
}
|
|
404
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
405
|
+
value += this.input[this.pos];
|
|
406
|
+
this.pos++;
|
|
407
|
+
this.column++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return { type: "NUMBER", value, position: startPos, line: startLine, column: startColumn };
|
|
412
|
+
}
|
|
413
|
+
// Check for hexadecimal: 0x or 0X
|
|
414
|
+
if (this.input[this.pos] === "0" && (this.input[this.pos + 1] === "x" || this.input[this.pos + 1] === "X")) {
|
|
415
|
+
value += "0x";
|
|
416
|
+
this.pos += 2;
|
|
417
|
+
this.column += 2;
|
|
418
|
+
// Must have at least one hex digit
|
|
419
|
+
if (!this.isHexDigit(this.input[this.pos])) {
|
|
420
|
+
throw new SyntaxError(`Invalid hexadecimal integer: incomplete hex literal`);
|
|
421
|
+
}
|
|
422
|
+
// Read hex digits
|
|
423
|
+
while (this.pos < this.input.length && this.isHexDigit(this.input[this.pos])) {
|
|
424
|
+
value += this.input[this.pos];
|
|
425
|
+
this.pos++;
|
|
426
|
+
this.column++;
|
|
427
|
+
}
|
|
428
|
+
// Check for invalid characters immediately following (e.g., 0x1g)
|
|
429
|
+
if (this.pos < this.input.length && this.isIdentifierChar(this.input[this.pos])) {
|
|
430
|
+
throw new SyntaxError(`Invalid hexadecimal integer: invalid character '${this.input[this.pos]}'`);
|
|
431
|
+
}
|
|
432
|
+
return { type: "NUMBER", value, position: startPos, line: startLine, column: startColumn };
|
|
433
|
+
}
|
|
434
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
435
|
+
value += this.input[this.pos];
|
|
436
|
+
this.pos++;
|
|
437
|
+
this.column++;
|
|
438
|
+
}
|
|
439
|
+
// Only read decimal part if . is followed by a digit (not another .)
|
|
440
|
+
// This prevents "1..2" from being tokenized as "1." + "." + "2"
|
|
441
|
+
if (this.input[this.pos] === "." && this.isDigit(this.input[this.pos + 1])) {
|
|
442
|
+
value += ".";
|
|
443
|
+
this.pos++;
|
|
444
|
+
this.column++;
|
|
445
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
446
|
+
value += this.input[this.pos];
|
|
447
|
+
this.pos++;
|
|
448
|
+
this.column++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Handle scientific notation (e.g., 1e9, 1E-9, 1.5e+10)
|
|
452
|
+
if (this.input[this.pos] === "e" || this.input[this.pos] === "E") {
|
|
453
|
+
const nextChar = this.input[this.pos + 1];
|
|
454
|
+
// Check if followed by digit, + digit, or - digit
|
|
455
|
+
if (this.isDigit(nextChar) ||
|
|
456
|
+
((nextChar === "+" || nextChar === "-") && this.isDigit(this.input[this.pos + 2]))) {
|
|
457
|
+
value += this.input[this.pos]; // 'e' or 'E'
|
|
458
|
+
this.pos++;
|
|
459
|
+
this.column++;
|
|
460
|
+
// Handle optional sign
|
|
461
|
+
if (this.input[this.pos] === "+" || this.input[this.pos] === "-") {
|
|
462
|
+
value += this.input[this.pos];
|
|
463
|
+
this.pos++;
|
|
464
|
+
this.column++;
|
|
465
|
+
}
|
|
466
|
+
// Read exponent digits
|
|
467
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
468
|
+
value += this.input[this.pos];
|
|
469
|
+
this.pos++;
|
|
470
|
+
this.column++;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return { type: "NUMBER", value, position: startPos, line: startLine, column: startColumn };
|
|
475
|
+
}
|
|
476
|
+
isHexDigit(char) {
|
|
477
|
+
return (char >= "0" && char <= "9") ||
|
|
478
|
+
(char >= "a" && char <= "f") ||
|
|
479
|
+
(char >= "A" && char <= "F");
|
|
480
|
+
}
|
|
481
|
+
readIdentifier() {
|
|
482
|
+
let value = "";
|
|
483
|
+
while (this.pos < this.input.length && this.isIdentifierChar(this.input[this.pos])) {
|
|
484
|
+
value += this.input[this.pos];
|
|
485
|
+
this.pos++;
|
|
486
|
+
this.column++;
|
|
487
|
+
}
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
readBacktickIdentifier(startPos, startLine, startColumn) {
|
|
491
|
+
this.pos++; // consume opening backtick
|
|
492
|
+
this.column++;
|
|
493
|
+
let value = "";
|
|
494
|
+
while (this.pos < this.input.length) {
|
|
495
|
+
const char = this.input[this.pos];
|
|
496
|
+
if (char === "`") {
|
|
497
|
+
// Check for escaped backtick (double backtick)
|
|
498
|
+
if (this.input[this.pos + 1] === "`") {
|
|
499
|
+
value += "`";
|
|
500
|
+
this.pos += 2;
|
|
501
|
+
this.column += 2;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
// End of identifier
|
|
505
|
+
this.pos++;
|
|
506
|
+
this.column++;
|
|
507
|
+
return { type: "IDENTIFIER", value, position: startPos, line: startLine, column: startColumn };
|
|
508
|
+
}
|
|
509
|
+
if (char === "\n") {
|
|
510
|
+
this.line++;
|
|
511
|
+
this.column = 0; // Will be incremented below
|
|
512
|
+
}
|
|
513
|
+
value += char;
|
|
514
|
+
this.pos++;
|
|
515
|
+
this.column++;
|
|
516
|
+
}
|
|
517
|
+
throw new Error(`Unterminated backtick identifier at position ${startPos}`);
|
|
518
|
+
}
|
|
519
|
+
isDigit(char) {
|
|
520
|
+
return char >= "0" && char <= "9";
|
|
521
|
+
}
|
|
522
|
+
isIdentifierStart(char) {
|
|
523
|
+
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z") || char === "_";
|
|
524
|
+
}
|
|
525
|
+
isIdentifierChar(char) {
|
|
526
|
+
return this.isIdentifierStart(char) || this.isDigit(char);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// ============================================================================
|
|
530
|
+
// Parser
|
|
531
|
+
// ============================================================================
|
|
532
|
+
// Int64 range constants
|
|
533
|
+
const INT64_MAX = BigInt("9223372036854775807");
|
|
534
|
+
const INT64_MIN = BigInt("-9223372036854775808");
|
|
535
|
+
export class Parser {
|
|
536
|
+
tokens = [];
|
|
537
|
+
pos = 0;
|
|
538
|
+
anonVarCounter = 0;
|
|
539
|
+
/**
|
|
540
|
+
* Parse a number string, validating that integers are within int64 range.
|
|
541
|
+
* Throws SyntaxError for overflow.
|
|
542
|
+
*/
|
|
543
|
+
parseNumber(numStr) {
|
|
544
|
+
// Check if it's a hexadecimal integer
|
|
545
|
+
const isHex = numStr.includes('0x') || numStr.includes('0X');
|
|
546
|
+
const isNegativeHex = isHex && numStr.startsWith('-');
|
|
547
|
+
// Check if it has scientific notation (e.g., 1e9, 1.5E-10)
|
|
548
|
+
const hasExponent = numStr.includes('e') || numStr.includes('E');
|
|
549
|
+
// Check if it's an integer (no decimal point, no exponent, and not hex, or is hex)
|
|
550
|
+
if ((!numStr.includes('.') && !hasExponent) || isHex) {
|
|
551
|
+
try {
|
|
552
|
+
let bigVal;
|
|
553
|
+
if (isHex) {
|
|
554
|
+
// For hex, we need to parse as unsigned first, then apply sign
|
|
555
|
+
const hexPart = isNegativeHex ? numStr.slice(1) : numStr; // Remove leading minus if present
|
|
556
|
+
const unsignedVal = BigInt(hexPart);
|
|
557
|
+
bigVal = isNegativeHex ? -unsignedVal : unsignedVal;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
bigVal = BigInt(numStr);
|
|
561
|
+
}
|
|
562
|
+
if (bigVal > INT64_MAX || bigVal < INT64_MIN) {
|
|
563
|
+
throw new SyntaxError(`integer is too large: ${numStr}`);
|
|
564
|
+
}
|
|
565
|
+
return Number(bigVal);
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
if (e instanceof SyntaxError)
|
|
569
|
+
throw e;
|
|
570
|
+
// BigInt parsing failed - could be too large even for BigInt
|
|
571
|
+
throw new SyntaxError(`integer is too large: ${numStr}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const floatVal = parseFloat(numStr);
|
|
575
|
+
// Check for overflow (Infinity) or underflow to 0 for very small exponents
|
|
576
|
+
if (!Number.isFinite(floatVal)) {
|
|
577
|
+
throw new SyntaxError(`floating point number is too large: ${numStr}`);
|
|
578
|
+
}
|
|
579
|
+
return floatVal;
|
|
580
|
+
}
|
|
581
|
+
parse(input) {
|
|
582
|
+
try {
|
|
583
|
+
const tokenizer = new Tokenizer(input);
|
|
584
|
+
this.tokens = tokenizer.tokenize();
|
|
585
|
+
this.pos = 0;
|
|
586
|
+
const query = this.parseQuery();
|
|
587
|
+
if (query.clauses.length === 0) {
|
|
588
|
+
return this.error("Empty query");
|
|
589
|
+
}
|
|
590
|
+
return { success: true, query };
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
const currentToken = this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
|
|
594
|
+
return {
|
|
595
|
+
success: false,
|
|
596
|
+
error: {
|
|
597
|
+
message: e instanceof Error ? e.message : String(e),
|
|
598
|
+
position: currentToken?.position ?? 0,
|
|
599
|
+
line: currentToken?.line ?? 1,
|
|
600
|
+
column: currentToken?.column ?? 1,
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
parseQuery() {
|
|
606
|
+
// Parse clauses until we hit UNION or end
|
|
607
|
+
const clauses = [];
|
|
608
|
+
while (!this.isAtEnd() && !this.checkKeyword("UNION")) {
|
|
609
|
+
const clause = this.parseClause();
|
|
610
|
+
if (clause) {
|
|
611
|
+
clauses.push(clause);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Check for UNION
|
|
615
|
+
if (this.checkKeyword("UNION")) {
|
|
616
|
+
this.advance(); // consume UNION
|
|
617
|
+
// Check for ALL
|
|
618
|
+
const all = this.checkKeyword("ALL");
|
|
619
|
+
if (all) {
|
|
620
|
+
this.advance();
|
|
621
|
+
}
|
|
622
|
+
// Parse the right side of the UNION
|
|
623
|
+
const rightQuery = this.parseQuery();
|
|
624
|
+
// Cypher disallows mixing `UNION` and `UNION ALL` within the same query.
|
|
625
|
+
// Since our grammar nests UNIONs on the right, we only need to compare the
|
|
626
|
+
// current UNION modifier with the next UNION (if present).
|
|
627
|
+
if (rightQuery.clauses.length === 1 && rightQuery.clauses[0]?.type === "UNION") {
|
|
628
|
+
const rightUnion = rightQuery.clauses[0];
|
|
629
|
+
if (rightUnion.all !== all) {
|
|
630
|
+
throw new SyntaxError("InvalidClauseComposition");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Create a UNION clause that wraps both queries
|
|
634
|
+
const unionClause = {
|
|
635
|
+
type: "UNION",
|
|
636
|
+
all,
|
|
637
|
+
left: { clauses },
|
|
638
|
+
right: rightQuery,
|
|
639
|
+
};
|
|
640
|
+
return { clauses: [unionClause] };
|
|
641
|
+
}
|
|
642
|
+
return { clauses };
|
|
643
|
+
}
|
|
644
|
+
error(message) {
|
|
645
|
+
const currentToken = this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
|
|
646
|
+
return {
|
|
647
|
+
success: false,
|
|
648
|
+
error: {
|
|
649
|
+
message,
|
|
650
|
+
position: currentToken?.position ?? 0,
|
|
651
|
+
line: currentToken?.line ?? 1,
|
|
652
|
+
column: currentToken?.column ?? 1,
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
parseClause() {
|
|
657
|
+
const token = this.peek();
|
|
658
|
+
if (token.type === "EOF")
|
|
659
|
+
return null;
|
|
660
|
+
if (token.type !== "KEYWORD") {
|
|
661
|
+
throw new Error(`Unexpected token '${token.value}', expected a clause keyword like CREATE, MATCH, MERGE, SET, DELETE, or RETURN`);
|
|
662
|
+
}
|
|
663
|
+
switch (token.value) {
|
|
664
|
+
case "CREATE":
|
|
665
|
+
return this.parseCreate();
|
|
666
|
+
case "MATCH":
|
|
667
|
+
return this.parseMatch(false);
|
|
668
|
+
case "OPTIONAL":
|
|
669
|
+
return this.parseOptionalMatch();
|
|
670
|
+
case "MERGE":
|
|
671
|
+
return this.parseMerge();
|
|
672
|
+
case "SET":
|
|
673
|
+
return this.parseSet();
|
|
674
|
+
case "DELETE":
|
|
675
|
+
case "DETACH":
|
|
676
|
+
return this.parseDelete();
|
|
677
|
+
case "REMOVE":
|
|
678
|
+
return this.parseRemove();
|
|
679
|
+
case "RETURN":
|
|
680
|
+
return this.parseReturn();
|
|
681
|
+
case "WITH":
|
|
682
|
+
return this.parseWith();
|
|
683
|
+
case "UNWIND":
|
|
684
|
+
return this.parseUnwind();
|
|
685
|
+
case "CALL":
|
|
686
|
+
return this.parseCall();
|
|
687
|
+
default:
|
|
688
|
+
throw new Error(`Unexpected keyword '${token.value}'`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
parseCreate() {
|
|
692
|
+
this.expect("KEYWORD", "CREATE");
|
|
693
|
+
const patterns = [];
|
|
694
|
+
patterns.push(...this.parsePatternChain());
|
|
695
|
+
while (this.check("COMMA")) {
|
|
696
|
+
this.advance();
|
|
697
|
+
patterns.push(...this.parsePatternChain());
|
|
698
|
+
}
|
|
699
|
+
// Validate: CREATE requires relationship type and direction
|
|
700
|
+
for (const pattern of patterns) {
|
|
701
|
+
if ("edge" in pattern) {
|
|
702
|
+
// This is a RelationshipPattern
|
|
703
|
+
if (!pattern.edge.type && !pattern.edge.types) {
|
|
704
|
+
throw new Error("A relationship type is required to create a relationship");
|
|
705
|
+
}
|
|
706
|
+
if (pattern.edge.direction === "none") {
|
|
707
|
+
throw new Error("Only directed relationships are supported in CREATE");
|
|
708
|
+
}
|
|
709
|
+
// Multiple relationship types are not allowed in CREATE
|
|
710
|
+
if (pattern.edge.types && pattern.edge.types.length > 1) {
|
|
711
|
+
throw new Error("A single relationship type must be specified for CREATE");
|
|
712
|
+
}
|
|
713
|
+
// Variable-length patterns are not allowed in CREATE
|
|
714
|
+
if (pattern.edge.minHops !== undefined || pattern.edge.maxHops !== undefined) {
|
|
715
|
+
throw new Error("Variable length relationship patterns are not supported in CREATE");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return { type: "CREATE", patterns };
|
|
720
|
+
}
|
|
721
|
+
parseMatch(optional = false) {
|
|
722
|
+
this.expect("KEYWORD", "MATCH");
|
|
723
|
+
const patterns = [];
|
|
724
|
+
const pathExpressions = [];
|
|
725
|
+
// Parse first pattern or path expression
|
|
726
|
+
const firstPattern = this.parsePatternOrPath();
|
|
727
|
+
if ("type" in firstPattern && firstPattern.type === "path") {
|
|
728
|
+
pathExpressions.push(firstPattern);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
patterns.push(...(Array.isArray(firstPattern) ? firstPattern : [firstPattern]));
|
|
732
|
+
}
|
|
733
|
+
while (this.check("COMMA")) {
|
|
734
|
+
this.advance();
|
|
735
|
+
const nextPattern = this.parsePatternOrPath();
|
|
736
|
+
if ("type" in nextPattern && nextPattern.type === "path") {
|
|
737
|
+
pathExpressions.push(nextPattern);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
patterns.push(...(Array.isArray(nextPattern) ? nextPattern : [nextPattern]));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Validate: same variable cannot be used as both node and relationship
|
|
744
|
+
this.validateNoNodeRelationshipVariableConflict(patterns, pathExpressions);
|
|
745
|
+
let where;
|
|
746
|
+
if (this.checkKeyword("WHERE")) {
|
|
747
|
+
this.advance();
|
|
748
|
+
where = this.parseWhereCondition();
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
type: optional ? "OPTIONAL_MATCH" : "MATCH",
|
|
752
|
+
patterns,
|
|
753
|
+
pathExpressions: pathExpressions.length > 0 ? pathExpressions : undefined,
|
|
754
|
+
where
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Parse either a regular pattern chain or a named path expression.
|
|
759
|
+
* Syntax: p = (a)-[r]->(b) or just (a)-[r]->(b)
|
|
760
|
+
*/
|
|
761
|
+
parsePatternOrPath() {
|
|
762
|
+
// Check for path expression syntax: identifier = pattern
|
|
763
|
+
if (this.check("IDENTIFIER")) {
|
|
764
|
+
const savedPos = this.pos;
|
|
765
|
+
const identifier = this.advance().value;
|
|
766
|
+
if (this.check("EQUALS")) {
|
|
767
|
+
// This is a path expression: p = (a)-[r]->(b)
|
|
768
|
+
this.advance(); // consume "="
|
|
769
|
+
const patterns = this.parsePatternChain();
|
|
770
|
+
return {
|
|
771
|
+
type: "path",
|
|
772
|
+
variable: identifier,
|
|
773
|
+
patterns
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
// Not a path expression, backtrack
|
|
778
|
+
this.pos = savedPos;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// Regular pattern chain
|
|
782
|
+
return this.parsePatternChain();
|
|
783
|
+
}
|
|
784
|
+
parseOptionalMatch() {
|
|
785
|
+
this.expect("KEYWORD", "OPTIONAL");
|
|
786
|
+
return this.parseMatch(true);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Validate that no variable is used as both a node and a relationship in the same MATCH clause,
|
|
790
|
+
* and that no relationship variable is used more than once in the same pattern.
|
|
791
|
+
* These are invalid Cypher syntax.
|
|
792
|
+
*/
|
|
793
|
+
validateNoNodeRelationshipVariableConflict(patterns, pathExpressions) {
|
|
794
|
+
const nodeVars = new Set();
|
|
795
|
+
const relVars = new Set();
|
|
796
|
+
const seenRelVars = new Set();
|
|
797
|
+
// Helper to collect variables from patterns
|
|
798
|
+
const collectFromPatterns = (pats) => {
|
|
799
|
+
for (const pattern of pats) {
|
|
800
|
+
if ("edge" in pattern) {
|
|
801
|
+
// RelationshipPattern
|
|
802
|
+
if (pattern.source.variable)
|
|
803
|
+
nodeVars.add(pattern.source.variable);
|
|
804
|
+
if (pattern.target.variable)
|
|
805
|
+
nodeVars.add(pattern.target.variable);
|
|
806
|
+
if (pattern.edge.variable) {
|
|
807
|
+
// Check for duplicate relationship variable in the same pattern
|
|
808
|
+
if (seenRelVars.has(pattern.edge.variable)) {
|
|
809
|
+
throw new Error(`Cannot use the same relationship variable '${pattern.edge.variable}' for multiple patterns`);
|
|
810
|
+
}
|
|
811
|
+
seenRelVars.add(pattern.edge.variable);
|
|
812
|
+
relVars.add(pattern.edge.variable);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
// NodePattern
|
|
817
|
+
if (pattern.variable)
|
|
818
|
+
nodeVars.add(pattern.variable);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
collectFromPatterns(patterns);
|
|
823
|
+
// Also check path expressions
|
|
824
|
+
if (pathExpressions) {
|
|
825
|
+
for (const pathExpr of pathExpressions) {
|
|
826
|
+
collectFromPatterns(pathExpr.patterns);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Check for conflicts
|
|
830
|
+
for (const v of nodeVars) {
|
|
831
|
+
if (relVars.has(v)) {
|
|
832
|
+
throw new Error(`Variable '${v}' already declared as relationship`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
for (const v of relVars) {
|
|
836
|
+
if (nodeVars.has(v)) {
|
|
837
|
+
throw new Error(`Variable '${v}' already declared as node`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
parseMerge() {
|
|
842
|
+
this.expect("KEYWORD", "MERGE");
|
|
843
|
+
// Parse pattern or path expression (handles p = (a)-[r]->(b) syntax)
|
|
844
|
+
const patternOrPath = this.parsePatternOrPath();
|
|
845
|
+
let patterns = [];
|
|
846
|
+
let pathExpressions;
|
|
847
|
+
if ("type" in patternOrPath && patternOrPath.type === "path") {
|
|
848
|
+
pathExpressions = [patternOrPath];
|
|
849
|
+
// Also add the patterns from the path expression to the patterns array
|
|
850
|
+
patterns = patternOrPath.patterns;
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
patterns = patternOrPath;
|
|
854
|
+
}
|
|
855
|
+
// Validate patterns - variable-length relationships are not allowed in MERGE
|
|
856
|
+
for (const pattern of patterns) {
|
|
857
|
+
if ("edge" in pattern) {
|
|
858
|
+
if (pattern.edge.minHops !== undefined || pattern.edge.maxHops !== undefined) {
|
|
859
|
+
throw new Error("Variable length relationship patterns are not supported in MERGE");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
let onCreateSet;
|
|
864
|
+
let onMatchSet;
|
|
865
|
+
while (this.checkKeyword("ON")) {
|
|
866
|
+
this.advance();
|
|
867
|
+
if (this.checkKeyword("CREATE")) {
|
|
868
|
+
this.advance();
|
|
869
|
+
this.expect("KEYWORD", "SET");
|
|
870
|
+
onCreateSet = this.parseSetAssignments();
|
|
871
|
+
}
|
|
872
|
+
else if (this.checkKeyword("MATCH")) {
|
|
873
|
+
this.advance();
|
|
874
|
+
this.expect("KEYWORD", "SET");
|
|
875
|
+
onMatchSet = this.parseSetAssignments();
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
throw new Error("Expected CREATE or MATCH after ON");
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return { type: "MERGE", patterns, pathExpressions, onCreateSet, onMatchSet };
|
|
882
|
+
}
|
|
883
|
+
parseSet() {
|
|
884
|
+
this.expect("KEYWORD", "SET");
|
|
885
|
+
const assignments = this.parseSetAssignments();
|
|
886
|
+
return { type: "SET", assignments };
|
|
887
|
+
}
|
|
888
|
+
parseSetAssignments() {
|
|
889
|
+
const assignments = [];
|
|
890
|
+
do {
|
|
891
|
+
if (assignments.length > 0) {
|
|
892
|
+
this.expect("COMMA");
|
|
893
|
+
}
|
|
894
|
+
// Handle parenthesized expression: SET (n).property = value
|
|
895
|
+
let variable;
|
|
896
|
+
if (this.check("LPAREN")) {
|
|
897
|
+
this.advance();
|
|
898
|
+
variable = this.expectIdentifier();
|
|
899
|
+
this.expect("RPAREN");
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
variable = this.expectIdentifier();
|
|
903
|
+
}
|
|
904
|
+
// Check for label assignment: SET n:Label or SET n :Label (with whitespace)
|
|
905
|
+
if (this.check("COLON")) {
|
|
906
|
+
// Label assignment: SET n:Label1:Label2
|
|
907
|
+
const labels = [];
|
|
908
|
+
while (this.check("COLON")) {
|
|
909
|
+
this.advance(); // consume ":"
|
|
910
|
+
labels.push(this.expectLabelOrType());
|
|
911
|
+
}
|
|
912
|
+
assignments.push({ variable, labels });
|
|
913
|
+
}
|
|
914
|
+
else if (this.check("PLUS")) {
|
|
915
|
+
// Property merge: SET n += {props}
|
|
916
|
+
this.advance(); // consume "+"
|
|
917
|
+
this.expect("EQUALS");
|
|
918
|
+
const value = this.parseExpression();
|
|
919
|
+
assignments.push({ variable, value, mergeProps: true });
|
|
920
|
+
}
|
|
921
|
+
else if (this.check("EQUALS")) {
|
|
922
|
+
// Property replace: SET n = {props}
|
|
923
|
+
this.advance(); // consume "="
|
|
924
|
+
const value = this.parseExpression();
|
|
925
|
+
assignments.push({ variable, value, replaceProps: true });
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
// Property assignment: SET n.property = value
|
|
929
|
+
this.expect("DOT");
|
|
930
|
+
const property = this.expectIdentifier();
|
|
931
|
+
this.expect("EQUALS");
|
|
932
|
+
const value = this.parseExpression();
|
|
933
|
+
assignments.push({ variable, property, value });
|
|
934
|
+
}
|
|
935
|
+
} while (this.check("COMMA"));
|
|
936
|
+
return assignments;
|
|
937
|
+
}
|
|
938
|
+
parseRemove() {
|
|
939
|
+
this.expect("KEYWORD", "REMOVE");
|
|
940
|
+
const items = [];
|
|
941
|
+
do {
|
|
942
|
+
if (items.length > 0) {
|
|
943
|
+
this.expect("COMMA");
|
|
944
|
+
}
|
|
945
|
+
const variable = this.expectIdentifier();
|
|
946
|
+
// Check for label removal: REMOVE n:Label or REMOVE n:Label1:Label2
|
|
947
|
+
if (this.check("COLON")) {
|
|
948
|
+
const labels = [];
|
|
949
|
+
while (this.check("COLON")) {
|
|
950
|
+
this.advance(); // consume ":"
|
|
951
|
+
labels.push(this.expectLabelOrType());
|
|
952
|
+
}
|
|
953
|
+
items.push({ variable, labels });
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
// Property removal: REMOVE n.prop
|
|
957
|
+
this.expect("DOT");
|
|
958
|
+
const property = this.expectIdentifier();
|
|
959
|
+
items.push({ variable, property });
|
|
960
|
+
}
|
|
961
|
+
} while (this.check("COMMA"));
|
|
962
|
+
return { type: "REMOVE", items };
|
|
963
|
+
}
|
|
964
|
+
parseDelete() {
|
|
965
|
+
let detach = false;
|
|
966
|
+
if (this.checkKeyword("DETACH")) {
|
|
967
|
+
this.advance();
|
|
968
|
+
detach = true;
|
|
969
|
+
}
|
|
970
|
+
this.expect("KEYWORD", "DELETE");
|
|
971
|
+
const variables = [];
|
|
972
|
+
const expressions = [];
|
|
973
|
+
// Parse first delete target (can be simple variable or complex expression)
|
|
974
|
+
this.parseDeleteTarget(variables, expressions);
|
|
975
|
+
while (this.check("COMMA")) {
|
|
976
|
+
this.advance();
|
|
977
|
+
this.parseDeleteTarget(variables, expressions);
|
|
978
|
+
}
|
|
979
|
+
const result = { type: "DELETE", variables, detach };
|
|
980
|
+
if (expressions.length > 0) {
|
|
981
|
+
result.expressions = expressions;
|
|
982
|
+
}
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
parseDeleteTarget(variables, expressions) {
|
|
986
|
+
// Check if this is a simple variable or a complex expression
|
|
987
|
+
// Look ahead to see if it's identifier followed by [ (list access) or . (property access)
|
|
988
|
+
const token = this.peek();
|
|
989
|
+
if (token.type === "IDENTIFIER") {
|
|
990
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
991
|
+
if (nextToken && (nextToken.type === "LBRACKET" || nextToken.type === "DOT")) {
|
|
992
|
+
// This is a list access expression like friends[$index]
|
|
993
|
+
// or a property access expression like nodes.key
|
|
994
|
+
const expr = this.parseExpression();
|
|
995
|
+
expressions.push(expr);
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
// Simple variable name
|
|
999
|
+
variables.push(this.advance().value);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
// DELETE requires a variable or variable-based expression (like list[index])
|
|
1004
|
+
// Other expression types (literals, arithmetic, etc.) are not valid DELETE targets
|
|
1005
|
+
throw new Error(`Type mismatch: expected Node or Relationship but was ${token.type === "NUMBER" ? "Integer" : token.type === "STRING" ? "String" : token.value}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
parseReturn() {
|
|
1009
|
+
this.expect("KEYWORD", "RETURN");
|
|
1010
|
+
// Check for DISTINCT after RETURN
|
|
1011
|
+
let distinct;
|
|
1012
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
1013
|
+
this.advance();
|
|
1014
|
+
distinct = true;
|
|
1015
|
+
}
|
|
1016
|
+
const items = [];
|
|
1017
|
+
// Check for RETURN * syntax (return all matched variables)
|
|
1018
|
+
if (this.check("STAR")) {
|
|
1019
|
+
this.advance();
|
|
1020
|
+
// Mark with special "*" variable to indicate return all
|
|
1021
|
+
items.push({ expression: { type: "variable", variable: "*" } });
|
|
1022
|
+
// After *, we might have additional items with comma (unlikely but possible)
|
|
1023
|
+
// e.g., RETURN *, count(*) AS cnt - but this is rare
|
|
1024
|
+
}
|
|
1025
|
+
if (items.length === 0 || this.check("COMMA")) {
|
|
1026
|
+
if (items.length > 0) {
|
|
1027
|
+
this.advance(); // consume comma after *
|
|
1028
|
+
}
|
|
1029
|
+
do {
|
|
1030
|
+
if (items.length > 0) {
|
|
1031
|
+
this.expect("COMMA");
|
|
1032
|
+
}
|
|
1033
|
+
// Use parseReturnExpression to allow comparisons in RETURN items
|
|
1034
|
+
const expression = this.parseReturnExpression();
|
|
1035
|
+
let alias;
|
|
1036
|
+
if (this.checkKeyword("AS")) {
|
|
1037
|
+
this.advance();
|
|
1038
|
+
alias = this.expectIdentifierOrKeyword();
|
|
1039
|
+
}
|
|
1040
|
+
items.push({ expression, alias });
|
|
1041
|
+
} while (this.check("COMMA"));
|
|
1042
|
+
}
|
|
1043
|
+
// Parse ORDER BY
|
|
1044
|
+
let orderBy;
|
|
1045
|
+
if (this.checkKeyword("ORDER")) {
|
|
1046
|
+
this.advance();
|
|
1047
|
+
this.expect("KEYWORD", "BY");
|
|
1048
|
+
orderBy = [];
|
|
1049
|
+
do {
|
|
1050
|
+
if (orderBy.length > 0) {
|
|
1051
|
+
this.expect("COMMA");
|
|
1052
|
+
}
|
|
1053
|
+
const expression = this.parseReturnExpression();
|
|
1054
|
+
let direction = "ASC"; // Default to ASC
|
|
1055
|
+
if (this.checkKeyword("ASC") || this.checkKeyword("ASCENDING")) {
|
|
1056
|
+
this.advance();
|
|
1057
|
+
}
|
|
1058
|
+
else if (this.checkKeyword("DESC") || this.checkKeyword("DESCENDING")) {
|
|
1059
|
+
this.advance();
|
|
1060
|
+
direction = "DESC";
|
|
1061
|
+
}
|
|
1062
|
+
orderBy.push({ expression, direction });
|
|
1063
|
+
} while (this.check("COMMA"));
|
|
1064
|
+
}
|
|
1065
|
+
// Parse SKIP
|
|
1066
|
+
let skip;
|
|
1067
|
+
if (this.checkKeyword("SKIP")) {
|
|
1068
|
+
this.advance();
|
|
1069
|
+
skip = this.parseExpression();
|
|
1070
|
+
// Validate if it's a literal number
|
|
1071
|
+
if (skip.type === "literal" && typeof skip.value === "number") {
|
|
1072
|
+
if (!Number.isInteger(skip.value)) {
|
|
1073
|
+
throw new Error("SKIP: InvalidArgumentType - expected an integer value");
|
|
1074
|
+
}
|
|
1075
|
+
if (skip.value < 0) {
|
|
1076
|
+
throw new Error("SKIP: NegativeIntegerArgument - cannot be negative");
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// Parse LIMIT
|
|
1081
|
+
let limit;
|
|
1082
|
+
if (this.checkKeyword("LIMIT")) {
|
|
1083
|
+
this.advance();
|
|
1084
|
+
limit = this.parseExpression();
|
|
1085
|
+
// Validate if it's a literal number
|
|
1086
|
+
if (limit.type === "literal" && typeof limit.value === "number") {
|
|
1087
|
+
if (!Number.isInteger(limit.value)) {
|
|
1088
|
+
throw new Error("LIMIT: InvalidArgumentType - expected an integer value");
|
|
1089
|
+
}
|
|
1090
|
+
if (limit.value < 0) {
|
|
1091
|
+
throw new Error("LIMIT: NegativeIntegerArgument - cannot be negative");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { type: "RETURN", distinct, items, orderBy, skip, limit };
|
|
1096
|
+
}
|
|
1097
|
+
parseWith() {
|
|
1098
|
+
this.expect("KEYWORD", "WITH");
|
|
1099
|
+
// Check for DISTINCT after WITH
|
|
1100
|
+
let distinct;
|
|
1101
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
1102
|
+
this.advance();
|
|
1103
|
+
distinct = true;
|
|
1104
|
+
}
|
|
1105
|
+
const items = [];
|
|
1106
|
+
let star = false;
|
|
1107
|
+
// Check for WITH * syntax (pass through all variables)
|
|
1108
|
+
if (this.check("STAR")) {
|
|
1109
|
+
this.advance();
|
|
1110
|
+
star = true;
|
|
1111
|
+
// After *, we might have additional items with comma
|
|
1112
|
+
// e.g., WITH *, count(n) AS cnt
|
|
1113
|
+
// For now, we'll mark this with a special expression
|
|
1114
|
+
items.push({ expression: { type: "variable", variable: "*" } });
|
|
1115
|
+
}
|
|
1116
|
+
if (!star || this.check("COMMA")) {
|
|
1117
|
+
if (star) {
|
|
1118
|
+
this.advance(); // consume comma after *
|
|
1119
|
+
}
|
|
1120
|
+
do {
|
|
1121
|
+
if (items.length > (star ? 1 : 0)) {
|
|
1122
|
+
this.expect("COMMA");
|
|
1123
|
+
}
|
|
1124
|
+
const expression = this.parseReturnExpression();
|
|
1125
|
+
let alias;
|
|
1126
|
+
if (this.checkKeyword("AS")) {
|
|
1127
|
+
this.advance();
|
|
1128
|
+
alias = this.expectIdentifierOrKeyword();
|
|
1129
|
+
}
|
|
1130
|
+
// In WITH, non-variable expressions must be aliased
|
|
1131
|
+
// e.g., WITH count(*) is invalid, but WITH count(*) AS c is valid
|
|
1132
|
+
// e.g., WITH a.name is invalid, but WITH a.name AS name is valid
|
|
1133
|
+
if (!alias && expression.type !== "variable") {
|
|
1134
|
+
throw new Error(`Expression in WITH must be aliased (use AS)`);
|
|
1135
|
+
}
|
|
1136
|
+
items.push({ expression, alias });
|
|
1137
|
+
} while (this.check("COMMA"));
|
|
1138
|
+
}
|
|
1139
|
+
// Parse ORDER BY
|
|
1140
|
+
let orderBy;
|
|
1141
|
+
if (this.checkKeyword("ORDER")) {
|
|
1142
|
+
this.advance();
|
|
1143
|
+
this.expect("KEYWORD", "BY");
|
|
1144
|
+
orderBy = [];
|
|
1145
|
+
do {
|
|
1146
|
+
if (orderBy.length > 0) {
|
|
1147
|
+
this.expect("COMMA");
|
|
1148
|
+
}
|
|
1149
|
+
const expression = this.parseReturnExpression();
|
|
1150
|
+
let direction = "ASC"; // Default to ASC
|
|
1151
|
+
if (this.checkKeyword("ASC") || this.checkKeyword("ASCENDING")) {
|
|
1152
|
+
this.advance();
|
|
1153
|
+
}
|
|
1154
|
+
else if (this.checkKeyword("DESC") || this.checkKeyword("DESCENDING")) {
|
|
1155
|
+
this.advance();
|
|
1156
|
+
direction = "DESC";
|
|
1157
|
+
}
|
|
1158
|
+
orderBy.push({ expression, direction });
|
|
1159
|
+
} while (this.check("COMMA"));
|
|
1160
|
+
}
|
|
1161
|
+
// Parse SKIP
|
|
1162
|
+
let skip;
|
|
1163
|
+
if (this.checkKeyword("SKIP")) {
|
|
1164
|
+
this.advance();
|
|
1165
|
+
skip = this.parseExpression();
|
|
1166
|
+
// Validate if it's a literal number
|
|
1167
|
+
if (skip.type === "literal" && typeof skip.value === "number") {
|
|
1168
|
+
if (!Number.isInteger(skip.value)) {
|
|
1169
|
+
throw new Error("SKIP: InvalidArgumentType - expected an integer value");
|
|
1170
|
+
}
|
|
1171
|
+
if (skip.value < 0) {
|
|
1172
|
+
throw new Error("SKIP: NegativeIntegerArgument - cannot be negative");
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// Parse LIMIT
|
|
1177
|
+
let limit;
|
|
1178
|
+
if (this.checkKeyword("LIMIT")) {
|
|
1179
|
+
this.advance();
|
|
1180
|
+
limit = this.parseExpression();
|
|
1181
|
+
// Validate if it's a literal number
|
|
1182
|
+
if (limit.type === "literal" && typeof limit.value === "number") {
|
|
1183
|
+
if (!Number.isInteger(limit.value)) {
|
|
1184
|
+
throw new Error("LIMIT: InvalidArgumentType - expected an integer value");
|
|
1185
|
+
}
|
|
1186
|
+
if (limit.value < 0) {
|
|
1187
|
+
throw new Error("LIMIT: NegativeIntegerArgument - cannot be negative");
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// Parse optional WHERE clause after WITH items
|
|
1192
|
+
let where;
|
|
1193
|
+
if (this.checkKeyword("WHERE")) {
|
|
1194
|
+
this.advance();
|
|
1195
|
+
where = this.parseWhereCondition();
|
|
1196
|
+
}
|
|
1197
|
+
return { type: "WITH", distinct, items, orderBy, skip, limit, where };
|
|
1198
|
+
}
|
|
1199
|
+
parseUnwind() {
|
|
1200
|
+
this.expect("KEYWORD", "UNWIND");
|
|
1201
|
+
const expression = this.parseUnwindExpression();
|
|
1202
|
+
this.expect("KEYWORD", "AS");
|
|
1203
|
+
const alias = this.expectIdentifier();
|
|
1204
|
+
return { type: "UNWIND", expression, alias };
|
|
1205
|
+
}
|
|
1206
|
+
parseUnwindExpression() {
|
|
1207
|
+
const token = this.peek();
|
|
1208
|
+
// NULL literal - UNWIND null produces empty result
|
|
1209
|
+
if (token.type === "KEYWORD" && token.value.toUpperCase() === "NULL") {
|
|
1210
|
+
this.advance();
|
|
1211
|
+
return { type: "literal", value: null };
|
|
1212
|
+
}
|
|
1213
|
+
// Parenthesized expression like (first + second)
|
|
1214
|
+
if (token.type === "LPAREN") {
|
|
1215
|
+
this.advance();
|
|
1216
|
+
const expr = this.parseExpression();
|
|
1217
|
+
this.expect("RPAREN");
|
|
1218
|
+
return expr;
|
|
1219
|
+
}
|
|
1220
|
+
// Array literal
|
|
1221
|
+
if (token.type === "LBRACKET") {
|
|
1222
|
+
// Use full list literal parsing so elements can be expressions (e.g. [date({year: 1910, ...}), ...])
|
|
1223
|
+
return this.parseListLiteralExpression();
|
|
1224
|
+
}
|
|
1225
|
+
// Parameter
|
|
1226
|
+
if (token.type === "PARAMETER") {
|
|
1227
|
+
this.advance();
|
|
1228
|
+
return { type: "parameter", name: token.value };
|
|
1229
|
+
}
|
|
1230
|
+
// Function call like range(1, 10)
|
|
1231
|
+
if ((token.type === "IDENTIFIER" || token.type === "KEYWORD") && this.tokens[this.pos + 1]?.type === "LPAREN") {
|
|
1232
|
+
return this.parseExpression();
|
|
1233
|
+
}
|
|
1234
|
+
// Variable, property access, or index access (e.g., qrows[p])
|
|
1235
|
+
if (token.type === "IDENTIFIER") {
|
|
1236
|
+
// Use parseExpression to handle postfix operations like property access and indexing
|
|
1237
|
+
return this.parseExpression();
|
|
1238
|
+
}
|
|
1239
|
+
throw new Error(`Expected array, parameter, or variable in UNWIND, got ${token.type} '${token.value}'`);
|
|
1240
|
+
}
|
|
1241
|
+
parseCall() {
|
|
1242
|
+
this.expect("KEYWORD", "CALL");
|
|
1243
|
+
// Parse procedure name (e.g., "db.labels" or "db.relationshipTypes")
|
|
1244
|
+
// Procedure names can have dots, so we parse identifier.identifier...
|
|
1245
|
+
let procedureName = this.expectIdentifier();
|
|
1246
|
+
while (this.check("DOT")) {
|
|
1247
|
+
this.advance();
|
|
1248
|
+
procedureName += "." + this.expectIdentifier();
|
|
1249
|
+
}
|
|
1250
|
+
// Parse arguments in parentheses
|
|
1251
|
+
this.expect("LPAREN");
|
|
1252
|
+
const args = [];
|
|
1253
|
+
if (!this.check("RPAREN")) {
|
|
1254
|
+
do {
|
|
1255
|
+
if (args.length > 0) {
|
|
1256
|
+
this.expect("COMMA");
|
|
1257
|
+
}
|
|
1258
|
+
args.push(this.parseExpression());
|
|
1259
|
+
} while (this.check("COMMA"));
|
|
1260
|
+
}
|
|
1261
|
+
this.expect("RPAREN");
|
|
1262
|
+
// Parse optional YIELD clause
|
|
1263
|
+
let yields;
|
|
1264
|
+
let where;
|
|
1265
|
+
if (this.checkKeyword("YIELD")) {
|
|
1266
|
+
this.advance();
|
|
1267
|
+
yields = [];
|
|
1268
|
+
// Parse yielded field names (can be identifiers or keywords like 'count')
|
|
1269
|
+
do {
|
|
1270
|
+
if (yields.length > 0) {
|
|
1271
|
+
this.expect("COMMA");
|
|
1272
|
+
}
|
|
1273
|
+
yields.push(this.expectIdentifierOrKeyword());
|
|
1274
|
+
} while (this.check("COMMA"));
|
|
1275
|
+
// Parse optional WHERE after YIELD
|
|
1276
|
+
if (this.checkKeyword("WHERE")) {
|
|
1277
|
+
this.advance();
|
|
1278
|
+
where = this.parseWhereCondition();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return { type: "CALL", procedure: procedureName, args, yields, where };
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Parse a pattern, which can be a single node or a chain of relationships.
|
|
1285
|
+
* For chained patterns like (a)-[:R1]->(b)-[:R2]->(c), this returns multiple
|
|
1286
|
+
* RelationshipPattern objects via parsePatternChain.
|
|
1287
|
+
*/
|
|
1288
|
+
parsePattern() {
|
|
1289
|
+
const firstNode = this.parseNodePattern();
|
|
1290
|
+
// Check for relationship
|
|
1291
|
+
if (this.check("DASH") || this.check("ARROW_LEFT")) {
|
|
1292
|
+
const edge = this.parseEdgePattern();
|
|
1293
|
+
const targetNode = this.parseNodePattern();
|
|
1294
|
+
return {
|
|
1295
|
+
source: firstNode,
|
|
1296
|
+
edge,
|
|
1297
|
+
target: targetNode,
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
return firstNode;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Parse a pattern chain, returning an array of patterns.
|
|
1304
|
+
* Handles multi-hop patterns like (a)-[:R1]->(b)-[:R2]->(c).
|
|
1305
|
+
*/
|
|
1306
|
+
parsePatternChain() {
|
|
1307
|
+
const patterns = [];
|
|
1308
|
+
const firstNode = this.parseNodePattern();
|
|
1309
|
+
// Check for relationship chain
|
|
1310
|
+
if (!this.check("DASH") && !this.check("ARROW_LEFT")) {
|
|
1311
|
+
// Just a single node
|
|
1312
|
+
return [firstNode];
|
|
1313
|
+
}
|
|
1314
|
+
// Parse first relationship
|
|
1315
|
+
let currentSource = firstNode;
|
|
1316
|
+
while (this.check("DASH") || this.check("ARROW_LEFT")) {
|
|
1317
|
+
const edge = this.parseEdgePattern();
|
|
1318
|
+
const targetNode = this.parseNodePattern();
|
|
1319
|
+
// Check if there's another relationship pattern coming after this one
|
|
1320
|
+
const hasMoreRelationships = this.check("DASH") || this.check("ARROW_LEFT");
|
|
1321
|
+
// If target is anonymous (no variable) AND there's more patterns coming,
|
|
1322
|
+
// assign a synthetic variable for chaining.
|
|
1323
|
+
// This ensures patterns like (:A)<-[:R]-(:B)-[:S]->(:C) share the (:B) node
|
|
1324
|
+
// But don't do this for standalone patterns like CREATE ()-[:R]->()
|
|
1325
|
+
if (!targetNode.variable && hasMoreRelationships) {
|
|
1326
|
+
targetNode.variable = `_anon${this.anonVarCounter++}`;
|
|
1327
|
+
}
|
|
1328
|
+
patterns.push({
|
|
1329
|
+
source: currentSource,
|
|
1330
|
+
edge,
|
|
1331
|
+
target: targetNode,
|
|
1332
|
+
});
|
|
1333
|
+
// For the next hop, the source is a reference to the previous target (variable only, no label)
|
|
1334
|
+
currentSource = { variable: targetNode.variable };
|
|
1335
|
+
}
|
|
1336
|
+
return patterns;
|
|
1337
|
+
}
|
|
1338
|
+
parseNodePattern() {
|
|
1339
|
+
this.expect("LPAREN");
|
|
1340
|
+
const pattern = {};
|
|
1341
|
+
// Variable name
|
|
1342
|
+
if (this.check("IDENTIFIER")) {
|
|
1343
|
+
pattern.variable = this.advance().value;
|
|
1344
|
+
}
|
|
1345
|
+
// Labels (can be multiple: :A:B:C)
|
|
1346
|
+
if (this.check("COLON")) {
|
|
1347
|
+
const labels = [];
|
|
1348
|
+
while (this.check("COLON")) {
|
|
1349
|
+
this.advance(); // consume ":"
|
|
1350
|
+
labels.push(this.expectLabelOrType());
|
|
1351
|
+
}
|
|
1352
|
+
// Store as array if multiple labels, string if single (for backward compatibility)
|
|
1353
|
+
pattern.label = labels.length === 1 ? labels[0] : labels;
|
|
1354
|
+
}
|
|
1355
|
+
// Properties
|
|
1356
|
+
if (this.check("LBRACE")) {
|
|
1357
|
+
pattern.properties = this.parseProperties();
|
|
1358
|
+
}
|
|
1359
|
+
this.expect("RPAREN");
|
|
1360
|
+
return pattern;
|
|
1361
|
+
}
|
|
1362
|
+
parseEdgePattern() {
|
|
1363
|
+
let direction = "none";
|
|
1364
|
+
// Left arrow or dash
|
|
1365
|
+
if (this.check("ARROW_LEFT")) {
|
|
1366
|
+
this.advance();
|
|
1367
|
+
direction = "left";
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
this.expect("DASH");
|
|
1371
|
+
}
|
|
1372
|
+
const edge = { direction };
|
|
1373
|
+
// Edge details in brackets
|
|
1374
|
+
if (this.check("LBRACKET")) {
|
|
1375
|
+
this.advance();
|
|
1376
|
+
// Variable name
|
|
1377
|
+
if (this.check("IDENTIFIER")) {
|
|
1378
|
+
edge.variable = this.advance().value;
|
|
1379
|
+
}
|
|
1380
|
+
// Type (can be identifier or keyword, or multiple types separated by |)
|
|
1381
|
+
if (this.check("COLON")) {
|
|
1382
|
+
this.advance();
|
|
1383
|
+
const firstType = this.expectLabelOrType();
|
|
1384
|
+
// Check for multiple types: [:TYPE1|TYPE2|TYPE3] or [:TYPE1|:TYPE2]
|
|
1385
|
+
if (this.check("PIPE")) {
|
|
1386
|
+
const types = [firstType];
|
|
1387
|
+
while (this.check("PIPE")) {
|
|
1388
|
+
this.advance();
|
|
1389
|
+
// Some Cypher dialects allow :TYPE after the pipe, consume the optional colon
|
|
1390
|
+
if (this.check("COLON")) {
|
|
1391
|
+
this.advance();
|
|
1392
|
+
}
|
|
1393
|
+
types.push(this.expectLabelOrType());
|
|
1394
|
+
}
|
|
1395
|
+
edge.types = types;
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
edge.type = firstType;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
// Variable-length pattern: *[min]..[max] or *n or *
|
|
1402
|
+
if (this.check("STAR")) {
|
|
1403
|
+
this.advance();
|
|
1404
|
+
this.parseVariableLengthSpec(edge);
|
|
1405
|
+
}
|
|
1406
|
+
// Properties
|
|
1407
|
+
if (this.check("LBRACE")) {
|
|
1408
|
+
edge.properties = this.parseProperties();
|
|
1409
|
+
}
|
|
1410
|
+
this.expect("RBRACKET");
|
|
1411
|
+
}
|
|
1412
|
+
// Right arrow or dash
|
|
1413
|
+
if (this.check("ARROW_RIGHT")) {
|
|
1414
|
+
this.advance();
|
|
1415
|
+
if (direction === "left") {
|
|
1416
|
+
// <--> pattern means "either direction" (bidirectional), same as --
|
|
1417
|
+
direction = "none";
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
direction = "right";
|
|
1421
|
+
}
|
|
1422
|
+
edge.direction = direction;
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
this.expect("DASH");
|
|
1426
|
+
}
|
|
1427
|
+
return edge;
|
|
1428
|
+
}
|
|
1429
|
+
parseVariableLengthSpec(edge) {
|
|
1430
|
+
// Patterns:
|
|
1431
|
+
// * -> min=1, max=undefined (any length >= 1)
|
|
1432
|
+
// *2 -> min=2, max=2 (fixed length)
|
|
1433
|
+
// *1..3 -> min=1, max=3 (range)
|
|
1434
|
+
// *2.. -> min=2, max=undefined (min only)
|
|
1435
|
+
// *..3 -> min=1, max=3 (max only)
|
|
1436
|
+
// *0..3 -> min=0, max=3 (can include zero-length)
|
|
1437
|
+
// Check for just * with no numbers or dots
|
|
1438
|
+
if (!this.check("NUMBER") && !this.check("DOT")) {
|
|
1439
|
+
edge.minHops = 1;
|
|
1440
|
+
edge.maxHops = undefined;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
// Check for ..N pattern (*..3) or just *.. (unbounded from 1)
|
|
1444
|
+
if (this.check("DOT")) {
|
|
1445
|
+
this.advance(); // first dot
|
|
1446
|
+
this.expect("DOT"); // second dot
|
|
1447
|
+
edge.minHops = 1;
|
|
1448
|
+
if (this.check("NUMBER")) {
|
|
1449
|
+
const maxVal = parseInt(this.advance().value, 10);
|
|
1450
|
+
if (maxVal < 0) {
|
|
1451
|
+
throw new Error("Negative bound in variable-length pattern is not allowed");
|
|
1452
|
+
}
|
|
1453
|
+
edge.maxHops = maxVal;
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
edge.maxHops = undefined; // unbounded
|
|
1457
|
+
}
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
// Parse first number
|
|
1461
|
+
const firstNum = parseInt(this.expect("NUMBER").value, 10);
|
|
1462
|
+
if (firstNum < 0) {
|
|
1463
|
+
throw new Error("Negative bound in variable-length pattern is not allowed");
|
|
1464
|
+
}
|
|
1465
|
+
// Check if this is a range or fixed
|
|
1466
|
+
if (this.check("DOT")) {
|
|
1467
|
+
this.advance(); // first dot
|
|
1468
|
+
// Need to check if next is DOT or if dots were consecutive
|
|
1469
|
+
if (this.check("DOT")) {
|
|
1470
|
+
this.advance(); // second dot
|
|
1471
|
+
}
|
|
1472
|
+
// If we just advanced past a DOT and the tokenizer gave us separate dots,
|
|
1473
|
+
// we need to handle this. Let's check the current token
|
|
1474
|
+
edge.minHops = firstNum;
|
|
1475
|
+
// Check for second number
|
|
1476
|
+
if (this.check("NUMBER")) {
|
|
1477
|
+
const maxVal = parseInt(this.advance().value, 10);
|
|
1478
|
+
if (maxVal < 0) {
|
|
1479
|
+
throw new Error("Negative bound in variable-length pattern is not allowed");
|
|
1480
|
+
}
|
|
1481
|
+
edge.maxHops = maxVal;
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
edge.maxHops = undefined; // unbounded
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
else {
|
|
1488
|
+
// Fixed length
|
|
1489
|
+
edge.minHops = firstNum;
|
|
1490
|
+
edge.maxHops = firstNum;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
parseProperties() {
|
|
1494
|
+
this.expect("LBRACE");
|
|
1495
|
+
const properties = {};
|
|
1496
|
+
if (!this.check("RBRACE")) {
|
|
1497
|
+
do {
|
|
1498
|
+
if (Object.keys(properties).length > 0) {
|
|
1499
|
+
this.expect("COMMA");
|
|
1500
|
+
}
|
|
1501
|
+
// Property keys can be identifiers OR keywords (like 'id', 'name', 'set', etc.)
|
|
1502
|
+
const key = this.expectIdentifierOrKeyword();
|
|
1503
|
+
this.expect("COLON");
|
|
1504
|
+
const value = this.parsePropertyValue();
|
|
1505
|
+
properties[key] = value;
|
|
1506
|
+
} while (this.check("COMMA"));
|
|
1507
|
+
}
|
|
1508
|
+
this.expect("RBRACE");
|
|
1509
|
+
return properties;
|
|
1510
|
+
}
|
|
1511
|
+
parsePropertyValue() {
|
|
1512
|
+
// Parse the primary property value first
|
|
1513
|
+
let left = this.parsePrimaryPropertyValue();
|
|
1514
|
+
// Check for binary operators: +, -, *, /, %
|
|
1515
|
+
while (this.check("PLUS") || this.check("DASH") || this.check("STAR") || this.check("SLASH") || this.check("PERCENT")) {
|
|
1516
|
+
const opToken = this.advance();
|
|
1517
|
+
let operator;
|
|
1518
|
+
if (opToken.type === "PLUS")
|
|
1519
|
+
operator = "+";
|
|
1520
|
+
else if (opToken.type === "DASH")
|
|
1521
|
+
operator = "-";
|
|
1522
|
+
else if (opToken.type === "STAR")
|
|
1523
|
+
operator = "*";
|
|
1524
|
+
else if (opToken.type === "SLASH")
|
|
1525
|
+
operator = "/";
|
|
1526
|
+
else
|
|
1527
|
+
operator = "%";
|
|
1528
|
+
const right = this.parsePrimaryPropertyValue();
|
|
1529
|
+
left = { type: "binary", operator, left, right };
|
|
1530
|
+
}
|
|
1531
|
+
return left;
|
|
1532
|
+
}
|
|
1533
|
+
parsePrimaryPropertyValue() {
|
|
1534
|
+
const token = this.peek();
|
|
1535
|
+
if (token.type === "STRING") {
|
|
1536
|
+
this.advance();
|
|
1537
|
+
return token.value;
|
|
1538
|
+
}
|
|
1539
|
+
if (token.type === "NUMBER") {
|
|
1540
|
+
this.advance();
|
|
1541
|
+
return this.parseNumber(token.value);
|
|
1542
|
+
}
|
|
1543
|
+
// Handle negative numbers: DASH followed by NUMBER
|
|
1544
|
+
if (token.type === "DASH") {
|
|
1545
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
1546
|
+
if (nextToken && nextToken.type === "NUMBER") {
|
|
1547
|
+
this.advance(); // consume DASH
|
|
1548
|
+
this.advance(); // consume NUMBER
|
|
1549
|
+
return -this.parseNumber(nextToken.value);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (token.type === "PARAMETER") {
|
|
1553
|
+
this.advance();
|
|
1554
|
+
return { type: "parameter", name: token.value };
|
|
1555
|
+
}
|
|
1556
|
+
if (token.type === "KEYWORD") {
|
|
1557
|
+
if (token.value === "TRUE") {
|
|
1558
|
+
this.advance();
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
if (token.value === "FALSE") {
|
|
1562
|
+
this.advance();
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
if (token.value === "NULL") {
|
|
1566
|
+
this.advance();
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (token.type === "LBRACKET") {
|
|
1571
|
+
return this.parseArray();
|
|
1572
|
+
}
|
|
1573
|
+
// Map literal (e.g., {year: 1980, month: 10, day: 24}) - used in temporal functions like date({..})
|
|
1574
|
+
if (token.type === "LBRACE") {
|
|
1575
|
+
return this.parseMapPropertyValue();
|
|
1576
|
+
}
|
|
1577
|
+
// Handle variable references (e.g., from UNWIND), property access (e.g., person.bornIn), or function calls (e.g., datetime())
|
|
1578
|
+
if (token.type === "IDENTIFIER") {
|
|
1579
|
+
this.advance();
|
|
1580
|
+
const varName = token.value;
|
|
1581
|
+
// Check for function call: identifier followed by LPAREN
|
|
1582
|
+
if (this.check("LPAREN")) {
|
|
1583
|
+
this.advance(); // consume LPAREN
|
|
1584
|
+
const args = [];
|
|
1585
|
+
// Parse function arguments
|
|
1586
|
+
if (!this.check("RPAREN")) {
|
|
1587
|
+
do {
|
|
1588
|
+
if (args.length > 0) {
|
|
1589
|
+
this.expect("COMMA");
|
|
1590
|
+
}
|
|
1591
|
+
args.push(this.parsePropertyValue());
|
|
1592
|
+
} while (this.check("COMMA"));
|
|
1593
|
+
}
|
|
1594
|
+
this.expect("RPAREN");
|
|
1595
|
+
return { type: "function", name: varName.toUpperCase(), args };
|
|
1596
|
+
}
|
|
1597
|
+
// Check for property access: variable.property
|
|
1598
|
+
if (this.check("DOT")) {
|
|
1599
|
+
this.advance(); // consume DOT
|
|
1600
|
+
const propToken = this.expect("IDENTIFIER");
|
|
1601
|
+
return { type: "property", variable: varName, property: propToken.value };
|
|
1602
|
+
}
|
|
1603
|
+
return { type: "variable", name: varName };
|
|
1604
|
+
}
|
|
1605
|
+
throw new Error(`Expected property value, got ${token.type} '${token.value}'`);
|
|
1606
|
+
}
|
|
1607
|
+
parseMapPropertyValue() {
|
|
1608
|
+
this.expect("LBRACE");
|
|
1609
|
+
const properties = {};
|
|
1610
|
+
if (!this.check("RBRACE")) {
|
|
1611
|
+
do {
|
|
1612
|
+
if (Object.keys(properties).length > 0) {
|
|
1613
|
+
this.expect("COMMA");
|
|
1614
|
+
}
|
|
1615
|
+
const key = this.expectIdentifierOrKeyword();
|
|
1616
|
+
this.expect("COLON");
|
|
1617
|
+
const value = this.parsePropertyValue();
|
|
1618
|
+
properties[key] = value;
|
|
1619
|
+
} while (this.check("COMMA"));
|
|
1620
|
+
}
|
|
1621
|
+
this.expect("RBRACE");
|
|
1622
|
+
return { type: "map", properties };
|
|
1623
|
+
}
|
|
1624
|
+
parseArray() {
|
|
1625
|
+
this.expect("LBRACKET");
|
|
1626
|
+
const values = [];
|
|
1627
|
+
if (!this.check("RBRACKET")) {
|
|
1628
|
+
do {
|
|
1629
|
+
if (values.length > 0) {
|
|
1630
|
+
this.expect("COMMA");
|
|
1631
|
+
}
|
|
1632
|
+
values.push(this.parsePropertyValue());
|
|
1633
|
+
} while (this.check("COMMA"));
|
|
1634
|
+
}
|
|
1635
|
+
this.expect("RBRACKET");
|
|
1636
|
+
return values;
|
|
1637
|
+
}
|
|
1638
|
+
parseWhereCondition() {
|
|
1639
|
+
return this.parseOrCondition();
|
|
1640
|
+
}
|
|
1641
|
+
parseOrCondition() {
|
|
1642
|
+
let left = this.parseAndCondition();
|
|
1643
|
+
while (this.checkKeyword("OR")) {
|
|
1644
|
+
this.advance();
|
|
1645
|
+
const right = this.parseAndCondition();
|
|
1646
|
+
left = { type: "or", conditions: [left, right] };
|
|
1647
|
+
}
|
|
1648
|
+
return left;
|
|
1649
|
+
}
|
|
1650
|
+
parseAndCondition() {
|
|
1651
|
+
let left = this.parseNotCondition();
|
|
1652
|
+
while (this.checkKeyword("AND")) {
|
|
1653
|
+
this.advance();
|
|
1654
|
+
const right = this.parseNotCondition();
|
|
1655
|
+
left = { type: "and", conditions: [left, right] };
|
|
1656
|
+
}
|
|
1657
|
+
return left;
|
|
1658
|
+
}
|
|
1659
|
+
parseNotCondition() {
|
|
1660
|
+
if (this.checkKeyword("NOT")) {
|
|
1661
|
+
this.advance();
|
|
1662
|
+
const condition = this.parseNotCondition();
|
|
1663
|
+
return { type: "not", condition };
|
|
1664
|
+
}
|
|
1665
|
+
return this.parsePrimaryCondition();
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Detect if we're looking at a pattern (e.g., (a)-[:R]->(b)).
|
|
1669
|
+
* A pattern starts with ( and after the matching ), we expect - or <- (not a keyword like AND, OR, etc).
|
|
1670
|
+
*/
|
|
1671
|
+
isPatternStart() {
|
|
1672
|
+
if (!this.check("LPAREN"))
|
|
1673
|
+
return false;
|
|
1674
|
+
// Find the matching closing paren
|
|
1675
|
+
let depth = 0;
|
|
1676
|
+
let pos = this.pos;
|
|
1677
|
+
while (pos < this.tokens.length) {
|
|
1678
|
+
const token = this.tokens[pos];
|
|
1679
|
+
if (token.type === "LPAREN")
|
|
1680
|
+
depth++;
|
|
1681
|
+
else if (token.type === "RPAREN") {
|
|
1682
|
+
depth--;
|
|
1683
|
+
if (depth === 0) {
|
|
1684
|
+
// Check what comes after the closing paren
|
|
1685
|
+
const nextPos = pos + 1;
|
|
1686
|
+
if (nextPos < this.tokens.length) {
|
|
1687
|
+
const nextToken = this.tokens[nextPos];
|
|
1688
|
+
// A pattern is followed by - or <- (never a keyword)
|
|
1689
|
+
return nextToken.type === "DASH" || nextToken.type === "ARROW_LEFT";
|
|
1690
|
+
}
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
pos++;
|
|
1695
|
+
}
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1698
|
+
parsePrimaryCondition() {
|
|
1699
|
+
// Handle EXISTS pattern
|
|
1700
|
+
if (this.checkKeyword("EXISTS")) {
|
|
1701
|
+
return this.parseExistsCondition();
|
|
1702
|
+
}
|
|
1703
|
+
// Handle list predicates: ALL, ANY, NONE, SINGLE
|
|
1704
|
+
const listPredicates = ["ALL", "ANY", "NONE", "SINGLE"];
|
|
1705
|
+
if (this.peek().type === "KEYWORD" && listPredicates.includes(this.peek().value)) {
|
|
1706
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
1707
|
+
if (nextToken && nextToken.type === "LPAREN") {
|
|
1708
|
+
return this.parseListPredicateCondition();
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
// Handle parenthesized conditions or patterns
|
|
1712
|
+
if (this.check("LPAREN")) {
|
|
1713
|
+
// Lookahead to determine if this is a pattern or a parenthesized condition
|
|
1714
|
+
if (this.isPatternStart()) {
|
|
1715
|
+
// Parse as pattern condition
|
|
1716
|
+
const patterns = this.parsePatternChain();
|
|
1717
|
+
return { type: "patternMatch", patterns };
|
|
1718
|
+
}
|
|
1719
|
+
// Otherwise parse as parenthesized condition
|
|
1720
|
+
this.advance(); // consume (
|
|
1721
|
+
const condition = this.parseOrCondition(); // parse the inner condition
|
|
1722
|
+
this.expect("RPAREN"); // consume )
|
|
1723
|
+
return condition;
|
|
1724
|
+
}
|
|
1725
|
+
return this.parseComparisonCondition();
|
|
1726
|
+
}
|
|
1727
|
+
parseListPredicateCondition() {
|
|
1728
|
+
// Parse list predicate as a condition (for use in WHERE clause)
|
|
1729
|
+
const predicateType = this.advance().value.toUpperCase();
|
|
1730
|
+
this.expect("LPAREN");
|
|
1731
|
+
// Expect variable followed by IN
|
|
1732
|
+
const variable = this.expectIdentifier();
|
|
1733
|
+
this.expect("KEYWORD", "IN");
|
|
1734
|
+
// Parse the source list expression
|
|
1735
|
+
const listExpr = this.parseExpression();
|
|
1736
|
+
// WHERE clause is required for list predicates
|
|
1737
|
+
if (!this.checkKeyword("WHERE")) {
|
|
1738
|
+
throw new Error(`Expected WHERE after list expression in ${predicateType}()`);
|
|
1739
|
+
}
|
|
1740
|
+
this.advance(); // consume WHERE
|
|
1741
|
+
// Parse the filter condition
|
|
1742
|
+
const filterCondition = this.parseListComprehensionCondition(variable);
|
|
1743
|
+
this.expect("RPAREN");
|
|
1744
|
+
return {
|
|
1745
|
+
type: "listPredicate",
|
|
1746
|
+
predicateType,
|
|
1747
|
+
variable,
|
|
1748
|
+
listExpr,
|
|
1749
|
+
filterCondition,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
parseExistsCondition() {
|
|
1753
|
+
this.expect("KEYWORD", "EXISTS");
|
|
1754
|
+
this.expect("LPAREN"); // outer (
|
|
1755
|
+
// Parse the pattern inside EXISTS((pattern))
|
|
1756
|
+
const patterns = this.parsePatternChain();
|
|
1757
|
+
const pattern = patterns.length === 1 ? patterns[0] : patterns[0]; // Use first pattern for now
|
|
1758
|
+
this.expect("RPAREN"); // outer )
|
|
1759
|
+
return { type: "exists", pattern };
|
|
1760
|
+
}
|
|
1761
|
+
parseComparisonCondition() {
|
|
1762
|
+
const left = this.parseExpression();
|
|
1763
|
+
// Check for label predicate: variable:Label or variable:Label1:Label2
|
|
1764
|
+
// After parsing the left expression (which should be a variable), check for COLON
|
|
1765
|
+
if (this.check("COLON") && left.type === "variable") {
|
|
1766
|
+
const variable = left.variable;
|
|
1767
|
+
const labelsList = [];
|
|
1768
|
+
while (this.check("COLON")) {
|
|
1769
|
+
this.advance(); // consume :
|
|
1770
|
+
labelsList.push(this.expectLabelOrType());
|
|
1771
|
+
}
|
|
1772
|
+
// Convert to a comparison that checks labels
|
|
1773
|
+
// Return as a comparison expression that the translator can handle
|
|
1774
|
+
const labelExpr = labelsList.length === 1
|
|
1775
|
+
? { type: "labelPredicate", variable, label: labelsList[0] }
|
|
1776
|
+
: { type: "labelPredicate", variable, labels: labelsList };
|
|
1777
|
+
// Wrap it in a comparison-like structure for WhereCondition
|
|
1778
|
+
return { type: "comparison", left: labelExpr, operator: "=", right: { type: "literal", value: true } };
|
|
1779
|
+
}
|
|
1780
|
+
// Check for IS NULL / IS NOT NULL
|
|
1781
|
+
if (this.checkKeyword("IS")) {
|
|
1782
|
+
this.advance();
|
|
1783
|
+
if (this.checkKeyword("NOT")) {
|
|
1784
|
+
this.advance();
|
|
1785
|
+
this.expect("KEYWORD", "NULL");
|
|
1786
|
+
return { type: "isNotNull", left };
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
this.expect("KEYWORD", "NULL");
|
|
1790
|
+
return { type: "isNull", left };
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
// Check for string operations
|
|
1794
|
+
if (this.checkKeyword("CONTAINS")) {
|
|
1795
|
+
this.advance();
|
|
1796
|
+
const right = this.parseExpression();
|
|
1797
|
+
return { type: "contains", left, right };
|
|
1798
|
+
}
|
|
1799
|
+
if (this.checkKeyword("STARTS")) {
|
|
1800
|
+
this.advance();
|
|
1801
|
+
this.expect("KEYWORD", "WITH");
|
|
1802
|
+
const right = this.parseExpression();
|
|
1803
|
+
return { type: "startsWith", left, right };
|
|
1804
|
+
}
|
|
1805
|
+
if (this.checkKeyword("ENDS")) {
|
|
1806
|
+
this.advance();
|
|
1807
|
+
this.expect("KEYWORD", "WITH");
|
|
1808
|
+
const right = this.parseExpression();
|
|
1809
|
+
return { type: "endsWith", left, right };
|
|
1810
|
+
}
|
|
1811
|
+
// Check for IN operator
|
|
1812
|
+
if (this.checkKeyword("IN")) {
|
|
1813
|
+
this.advance();
|
|
1814
|
+
// IN can be followed by a list literal [...] or a parameter $param
|
|
1815
|
+
const listExpr = this.parseInListExpression();
|
|
1816
|
+
return { type: "in", left, list: listExpr };
|
|
1817
|
+
}
|
|
1818
|
+
// Comparison operators - handle chained comparisons like 1 < n.num < 3
|
|
1819
|
+
const opToken = this.peek();
|
|
1820
|
+
let operator;
|
|
1821
|
+
if (opToken.type === "EQUALS")
|
|
1822
|
+
operator = "=";
|
|
1823
|
+
else if (opToken.type === "NOT_EQUALS")
|
|
1824
|
+
operator = "<>";
|
|
1825
|
+
else if (opToken.type === "LT")
|
|
1826
|
+
operator = "<";
|
|
1827
|
+
else if (opToken.type === "GT")
|
|
1828
|
+
operator = ">";
|
|
1829
|
+
else if (opToken.type === "LTE")
|
|
1830
|
+
operator = "<=";
|
|
1831
|
+
else if (opToken.type === "GTE")
|
|
1832
|
+
operator = ">=";
|
|
1833
|
+
if (operator) {
|
|
1834
|
+
this.advance();
|
|
1835
|
+
const middle = this.parseExpression();
|
|
1836
|
+
const firstComparison = { type: "comparison", left, right: middle, operator };
|
|
1837
|
+
// Check for chained comparison: 1 < n.num < 3 means (1 < n.num AND n.num < 3)
|
|
1838
|
+
const nextOpToken = this.peek();
|
|
1839
|
+
let secondOperator;
|
|
1840
|
+
if (nextOpToken.type === "EQUALS")
|
|
1841
|
+
secondOperator = "=";
|
|
1842
|
+
else if (nextOpToken.type === "NOT_EQUALS")
|
|
1843
|
+
secondOperator = "<>";
|
|
1844
|
+
else if (nextOpToken.type === "LT")
|
|
1845
|
+
secondOperator = "<";
|
|
1846
|
+
else if (nextOpToken.type === "GT")
|
|
1847
|
+
secondOperator = ">";
|
|
1848
|
+
else if (nextOpToken.type === "LTE")
|
|
1849
|
+
secondOperator = "<=";
|
|
1850
|
+
else if (nextOpToken.type === "GTE")
|
|
1851
|
+
secondOperator = ">=";
|
|
1852
|
+
if (secondOperator) {
|
|
1853
|
+
this.advance();
|
|
1854
|
+
const right = this.parseExpression();
|
|
1855
|
+
const secondComparison = { type: "comparison", left: middle, right, operator: secondOperator };
|
|
1856
|
+
// Return the chained comparison as an AND condition
|
|
1857
|
+
return { type: "and", conditions: [firstComparison, secondComparison] };
|
|
1858
|
+
}
|
|
1859
|
+
return firstComparison;
|
|
1860
|
+
}
|
|
1861
|
+
// No comparison operator - treat as standalone boolean expression
|
|
1862
|
+
// This handles cases like: WHERE false, WHERE n.active, WHERE true AND x = 1
|
|
1863
|
+
// Also handles boolean variables like: WHERE result (where result is a boolean)
|
|
1864
|
+
// However, bare variable references like WHERE (n) are invalid - that's checked at translation time
|
|
1865
|
+
return { type: "expression", left };
|
|
1866
|
+
}
|
|
1867
|
+
parseInListExpression() {
|
|
1868
|
+
const token = this.peek();
|
|
1869
|
+
// Array literal [...] - use parseListLiteralExpression which handles both
|
|
1870
|
+
// simple literals and list comprehensions like [x IN list | expr]
|
|
1871
|
+
if (token.type === "LBRACKET") {
|
|
1872
|
+
return this.parseListLiteralExpression();
|
|
1873
|
+
}
|
|
1874
|
+
// Parameter $param
|
|
1875
|
+
if (token.type === "PARAMETER") {
|
|
1876
|
+
this.advance();
|
|
1877
|
+
return { type: "parameter", name: token.value };
|
|
1878
|
+
}
|
|
1879
|
+
// Variable reference
|
|
1880
|
+
if (token.type === "IDENTIFIER") {
|
|
1881
|
+
const variable = this.advance().value;
|
|
1882
|
+
if (this.check("DOT")) {
|
|
1883
|
+
this.advance();
|
|
1884
|
+
const property = this.expectIdentifier();
|
|
1885
|
+
return { type: "property", variable, property };
|
|
1886
|
+
}
|
|
1887
|
+
return { type: "variable", variable };
|
|
1888
|
+
}
|
|
1889
|
+
throw new Error(`Expected array, parameter, or variable in IN clause, got ${token.type} '${token.value}'`);
|
|
1890
|
+
}
|
|
1891
|
+
parseExpression() {
|
|
1892
|
+
return this.parseAdditiveExpression();
|
|
1893
|
+
}
|
|
1894
|
+
// Parse expression that may include comparison and logical operators (for RETURN items)
|
|
1895
|
+
parseReturnExpression() {
|
|
1896
|
+
return this.parseOrExpression();
|
|
1897
|
+
}
|
|
1898
|
+
// Handle OR (lowest precedence for logical operators)
|
|
1899
|
+
parseOrExpression() {
|
|
1900
|
+
let left = this.parseXorExpression();
|
|
1901
|
+
while (this.checkKeyword("OR")) {
|
|
1902
|
+
this.advance();
|
|
1903
|
+
const right = this.parseXorExpression();
|
|
1904
|
+
left = { type: "binary", operator: "OR", left, right };
|
|
1905
|
+
}
|
|
1906
|
+
return left;
|
|
1907
|
+
}
|
|
1908
|
+
// Handle XOR (between OR and AND in precedence)
|
|
1909
|
+
parseXorExpression() {
|
|
1910
|
+
let left = this.parseAndExpression();
|
|
1911
|
+
while (this.checkKeyword("XOR")) {
|
|
1912
|
+
this.advance();
|
|
1913
|
+
const right = this.parseAndExpression();
|
|
1914
|
+
left = { type: "binary", operator: "XOR", left, right };
|
|
1915
|
+
}
|
|
1916
|
+
return left;
|
|
1917
|
+
}
|
|
1918
|
+
// Handle AND (higher precedence than XOR)
|
|
1919
|
+
parseAndExpression() {
|
|
1920
|
+
let left = this.parseNotExpression();
|
|
1921
|
+
while (this.checkKeyword("AND")) {
|
|
1922
|
+
this.advance();
|
|
1923
|
+
const right = this.parseNotExpression();
|
|
1924
|
+
left = { type: "binary", operator: "AND", left, right };
|
|
1925
|
+
}
|
|
1926
|
+
return left;
|
|
1927
|
+
}
|
|
1928
|
+
// Handle NOT (highest precedence for logical operators)
|
|
1929
|
+
parseNotExpression() {
|
|
1930
|
+
if (this.checkKeyword("NOT")) {
|
|
1931
|
+
this.advance();
|
|
1932
|
+
const operand = this.parseNotExpression();
|
|
1933
|
+
return { type: "unary", operator: "NOT", operand };
|
|
1934
|
+
}
|
|
1935
|
+
return this.parseComparisonExpression();
|
|
1936
|
+
}
|
|
1937
|
+
// Handle comparison operators
|
|
1938
|
+
parseComparisonExpression() {
|
|
1939
|
+
let left = this.parseIsNullExpression();
|
|
1940
|
+
// Check for comparison operators
|
|
1941
|
+
const opToken = this.peek();
|
|
1942
|
+
let comparisonOperator;
|
|
1943
|
+
if (opToken.type === "EQUALS")
|
|
1944
|
+
comparisonOperator = "=";
|
|
1945
|
+
else if (opToken.type === "NOT_EQUALS")
|
|
1946
|
+
comparisonOperator = "<>";
|
|
1947
|
+
else if (opToken.type === "LT")
|
|
1948
|
+
comparisonOperator = "<";
|
|
1949
|
+
else if (opToken.type === "GT")
|
|
1950
|
+
comparisonOperator = ">";
|
|
1951
|
+
else if (opToken.type === "LTE")
|
|
1952
|
+
comparisonOperator = "<=";
|
|
1953
|
+
else if (opToken.type === "GTE")
|
|
1954
|
+
comparisonOperator = ">=";
|
|
1955
|
+
if (comparisonOperator) {
|
|
1956
|
+
this.advance();
|
|
1957
|
+
const right = this.parseIsNullExpression();
|
|
1958
|
+
return { type: "comparison", comparisonOperator, left, right };
|
|
1959
|
+
}
|
|
1960
|
+
return left;
|
|
1961
|
+
}
|
|
1962
|
+
// Handle IS NULL / IS NOT NULL
|
|
1963
|
+
parseIsNullExpression() {
|
|
1964
|
+
let left = this.parseAdditiveExpression();
|
|
1965
|
+
// Check for label predicate: variable:Label or variable:Label1:Label2
|
|
1966
|
+
// This handles bare form `a:B` in RETURN expressions (without parentheses)
|
|
1967
|
+
if (this.check("COLON") && left.type === "variable") {
|
|
1968
|
+
const variable = left.variable;
|
|
1969
|
+
const labelsList = [];
|
|
1970
|
+
while (this.check("COLON")) {
|
|
1971
|
+
this.advance(); // consume :
|
|
1972
|
+
labelsList.push(this.expectLabelOrType());
|
|
1973
|
+
}
|
|
1974
|
+
if (labelsList.length === 1) {
|
|
1975
|
+
return { type: "labelPredicate", variable, label: labelsList[0] };
|
|
1976
|
+
}
|
|
1977
|
+
else {
|
|
1978
|
+
return { type: "labelPredicate", variable, labels: labelsList };
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (this.checkKeyword("IS")) {
|
|
1982
|
+
this.advance();
|
|
1983
|
+
if (this.checkKeyword("NOT")) {
|
|
1984
|
+
this.advance();
|
|
1985
|
+
this.expect("KEYWORD", "NULL");
|
|
1986
|
+
return { type: "comparison", comparisonOperator: "IS NOT NULL", left };
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
this.expect("KEYWORD", "NULL");
|
|
1990
|
+
return { type: "comparison", comparisonOperator: "IS NULL", left };
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
// Handle IN operator: value IN list
|
|
1994
|
+
// The list can be a function call like keys(n), array literal, parameter, or variable
|
|
1995
|
+
if (this.checkKeyword("IN")) {
|
|
1996
|
+
this.advance();
|
|
1997
|
+
// Use parseAdditiveExpression to allow function calls, array literals, etc.
|
|
1998
|
+
// but not operators that have lower precedence than IN
|
|
1999
|
+
const list = this.parseAdditiveExpression();
|
|
2000
|
+
return { type: "in", left, list };
|
|
2001
|
+
}
|
|
2002
|
+
// Handle string operators: CONTAINS, STARTS WITH, ENDS WITH
|
|
2003
|
+
if (this.checkKeyword("CONTAINS")) {
|
|
2004
|
+
this.advance();
|
|
2005
|
+
const right = this.parseAdditiveExpression();
|
|
2006
|
+
return { type: "stringOp", stringOperator: "CONTAINS", left, right };
|
|
2007
|
+
}
|
|
2008
|
+
if (this.checkKeyword("STARTS")) {
|
|
2009
|
+
this.advance();
|
|
2010
|
+
this.expect("KEYWORD", "WITH");
|
|
2011
|
+
const right = this.parseAdditiveExpression();
|
|
2012
|
+
return { type: "stringOp", stringOperator: "STARTS WITH", left, right };
|
|
2013
|
+
}
|
|
2014
|
+
if (this.checkKeyword("ENDS")) {
|
|
2015
|
+
this.advance();
|
|
2016
|
+
this.expect("KEYWORD", "WITH");
|
|
2017
|
+
const right = this.parseAdditiveExpression();
|
|
2018
|
+
return { type: "stringOp", stringOperator: "ENDS WITH", left, right };
|
|
2019
|
+
}
|
|
2020
|
+
return left;
|
|
2021
|
+
}
|
|
2022
|
+
// Handle + and - (lower precedence)
|
|
2023
|
+
parseAdditiveExpression() {
|
|
2024
|
+
let left = this.parseMultiplicativeExpression();
|
|
2025
|
+
while (this.check("PLUS") || this.check("DASH")) {
|
|
2026
|
+
const operatorToken = this.advance();
|
|
2027
|
+
const operator = operatorToken.type === "PLUS" ? "+" : "-";
|
|
2028
|
+
const right = this.parseMultiplicativeExpression();
|
|
2029
|
+
left = { type: "binary", operator: operator, left, right };
|
|
2030
|
+
}
|
|
2031
|
+
return left;
|
|
2032
|
+
}
|
|
2033
|
+
// Handle *, /, % (higher precedence than +, -)
|
|
2034
|
+
parseMultiplicativeExpression() {
|
|
2035
|
+
let left = this.parseExponentialExpression();
|
|
2036
|
+
while (this.check("STAR") || this.check("SLASH") || this.check("PERCENT")) {
|
|
2037
|
+
const operatorToken = this.advance();
|
|
2038
|
+
let operator;
|
|
2039
|
+
if (operatorToken.type === "STAR")
|
|
2040
|
+
operator = "*";
|
|
2041
|
+
else if (operatorToken.type === "SLASH")
|
|
2042
|
+
operator = "/";
|
|
2043
|
+
else
|
|
2044
|
+
operator = "%";
|
|
2045
|
+
const right = this.parseExponentialExpression();
|
|
2046
|
+
left = { type: "binary", operator, left, right };
|
|
2047
|
+
}
|
|
2048
|
+
return left;
|
|
2049
|
+
}
|
|
2050
|
+
// Handle ^ (exponentiation - highest precedence among arithmetic operators)
|
|
2051
|
+
parseExponentialExpression() {
|
|
2052
|
+
let left = this.parsePostfixExpression();
|
|
2053
|
+
while (this.check("CARET")) {
|
|
2054
|
+
this.advance(); // consume ^
|
|
2055
|
+
const right = this.parsePostfixExpression();
|
|
2056
|
+
left = { type: "binary", operator: "^", left, right };
|
|
2057
|
+
}
|
|
2058
|
+
return left;
|
|
2059
|
+
}
|
|
2060
|
+
// Handle postfix operations: list indexing like expr[0] or expr[1..3], and chained property access like a.b.c
|
|
2061
|
+
parsePostfixExpression() {
|
|
2062
|
+
let expr = this.parsePrimaryExpression();
|
|
2063
|
+
// Handle list/map indexing: expr[index] or expr[start..end], and chained property access: expr.prop
|
|
2064
|
+
while (this.check("LBRACKET") || this.check("DOT")) {
|
|
2065
|
+
if (this.check("DOT")) {
|
|
2066
|
+
this.advance(); // consume .
|
|
2067
|
+
// Property access - property names can be keywords too
|
|
2068
|
+
const property = this.expectIdentifierOrKeyword();
|
|
2069
|
+
// Check for namespaced function call: namespace.function(args)
|
|
2070
|
+
// e.g., duration.between(), duration.inMonths(), etc.
|
|
2071
|
+
if (this.check("LPAREN")) {
|
|
2072
|
+
this.advance(); // consume (
|
|
2073
|
+
// Build the namespaced function name (e.g., "duration.between")
|
|
2074
|
+
let namespace;
|
|
2075
|
+
if (expr.type === "variable") {
|
|
2076
|
+
namespace = expr.variable;
|
|
2077
|
+
}
|
|
2078
|
+
else if (expr.type === "propertyAccess") {
|
|
2079
|
+
// For deeper chains like a.b.c() - build full namespace path
|
|
2080
|
+
const parts = [];
|
|
2081
|
+
let current = expr;
|
|
2082
|
+
while (current.type === "propertyAccess") {
|
|
2083
|
+
parts.unshift(current.property);
|
|
2084
|
+
current = current.object;
|
|
2085
|
+
}
|
|
2086
|
+
if (current.type === "variable") {
|
|
2087
|
+
parts.unshift(current.variable);
|
|
2088
|
+
}
|
|
2089
|
+
namespace = parts.join(".");
|
|
2090
|
+
}
|
|
2091
|
+
else {
|
|
2092
|
+
// Can't form a namespace from this expression type
|
|
2093
|
+
throw new Error(`Invalid namespace function call syntax`);
|
|
2094
|
+
}
|
|
2095
|
+
// Convert to uppercase for consistency with other functions
|
|
2096
|
+
const functionName = `${namespace}.${property}`.toUpperCase();
|
|
2097
|
+
const args = [];
|
|
2098
|
+
// Check for DISTINCT keyword (for aggregation functions)
|
|
2099
|
+
let distinct;
|
|
2100
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
2101
|
+
this.advance();
|
|
2102
|
+
distinct = true;
|
|
2103
|
+
}
|
|
2104
|
+
if (!this.check("RPAREN")) {
|
|
2105
|
+
do {
|
|
2106
|
+
if (args.length > 0) {
|
|
2107
|
+
this.expect("COMMA");
|
|
2108
|
+
}
|
|
2109
|
+
args.push(this.parseReturnExpression());
|
|
2110
|
+
} while (this.check("COMMA"));
|
|
2111
|
+
}
|
|
2112
|
+
this.expect("RPAREN");
|
|
2113
|
+
expr = { type: "function", functionName, args, distinct };
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
// Regular property access
|
|
2117
|
+
expr = { type: "propertyAccess", object: expr, property };
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
else {
|
|
2121
|
+
// LBRACKET - parse index or slice
|
|
2122
|
+
this.advance(); // consume [
|
|
2123
|
+
// Check for slice syntax [start..end]
|
|
2124
|
+
if (this.check("DOT")) {
|
|
2125
|
+
// [..end] - from start (implicit start = 0)
|
|
2126
|
+
this.advance(); // consume first .
|
|
2127
|
+
this.expect("DOT"); // consume second .
|
|
2128
|
+
const endExpr = this.parseSliceBoundExpression();
|
|
2129
|
+
this.expect("RBRACKET");
|
|
2130
|
+
// Use SLICE_FROM_START to indicate implicit start
|
|
2131
|
+
expr = { type: "function", functionName: "SLICE_FROM_START", args: [expr, endExpr] };
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
// Parse start expression using slice-aware parsing
|
|
2135
|
+
const indexExpr = this.parseSliceBoundExpression();
|
|
2136
|
+
if (this.check("DOT")) {
|
|
2137
|
+
// Check for slice: [start..end] or [start..]
|
|
2138
|
+
this.advance(); // consume first .
|
|
2139
|
+
this.expect("DOT"); // consume second .
|
|
2140
|
+
if (this.check("RBRACKET")) {
|
|
2141
|
+
// [start..] - to end (implicit end = list length)
|
|
2142
|
+
this.expect("RBRACKET");
|
|
2143
|
+
// Use SLICE_TO_END to indicate implicit end
|
|
2144
|
+
expr = { type: "function", functionName: "SLICE_TO_END", args: [expr, indexExpr] };
|
|
2145
|
+
}
|
|
2146
|
+
else {
|
|
2147
|
+
const endExpr = this.parseSliceBoundExpression();
|
|
2148
|
+
this.expect("RBRACKET");
|
|
2149
|
+
// Both bounds are explicit
|
|
2150
|
+
expr = { type: "function", functionName: "SLICE", args: [expr, indexExpr, endExpr] };
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
// Simple index: [index]
|
|
2155
|
+
this.expect("RBRACKET");
|
|
2156
|
+
expr = { type: "function", functionName: "INDEX", args: [expr, indexExpr] };
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return expr;
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Parse an expression that can be used as a slice bound (start or end).
|
|
2165
|
+
* This is similar to parseExpression but stops at ".." to allow slice syntax.
|
|
2166
|
+
* It uses a modified postfix parser that detects ".." and stops before consuming it.
|
|
2167
|
+
*/
|
|
2168
|
+
parseSliceBoundExpression() {
|
|
2169
|
+
return this.parseSliceBoundAdditiveExpression();
|
|
2170
|
+
}
|
|
2171
|
+
parseSliceBoundAdditiveExpression() {
|
|
2172
|
+
let left = this.parseSliceBoundMultiplicativeExpression();
|
|
2173
|
+
while (this.check("PLUS") || this.check("DASH")) {
|
|
2174
|
+
const operatorToken = this.advance();
|
|
2175
|
+
const operator = operatorToken.type === "PLUS" ? "+" : "-";
|
|
2176
|
+
const right = this.parseSliceBoundMultiplicativeExpression();
|
|
2177
|
+
left = { type: "binary", operator: operator, left, right };
|
|
2178
|
+
}
|
|
2179
|
+
return left;
|
|
2180
|
+
}
|
|
2181
|
+
parseSliceBoundMultiplicativeExpression() {
|
|
2182
|
+
let left = this.parseSliceBoundExponentialExpression();
|
|
2183
|
+
while (this.check("STAR") || this.check("SLASH") || this.check("PERCENT")) {
|
|
2184
|
+
const operatorToken = this.advance();
|
|
2185
|
+
let operator;
|
|
2186
|
+
if (operatorToken.type === "STAR")
|
|
2187
|
+
operator = "*";
|
|
2188
|
+
else if (operatorToken.type === "SLASH")
|
|
2189
|
+
operator = "/";
|
|
2190
|
+
else
|
|
2191
|
+
operator = "%";
|
|
2192
|
+
const right = this.parseSliceBoundExponentialExpression();
|
|
2193
|
+
left = { type: "binary", operator, left, right };
|
|
2194
|
+
}
|
|
2195
|
+
return left;
|
|
2196
|
+
}
|
|
2197
|
+
parseSliceBoundExponentialExpression() {
|
|
2198
|
+
let left = this.parseSliceBoundPostfixExpression();
|
|
2199
|
+
while (this.check("CARET")) {
|
|
2200
|
+
this.advance();
|
|
2201
|
+
const right = this.parseSliceBoundPostfixExpression();
|
|
2202
|
+
left = { type: "binary", operator: "^", left, right };
|
|
2203
|
+
}
|
|
2204
|
+
return left;
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Parse postfix expression for slice bounds. This version detects ".."
|
|
2208
|
+
* and stops before consuming it, allowing the slice syntax to be parsed correctly.
|
|
2209
|
+
*/
|
|
2210
|
+
parseSliceBoundPostfixExpression() {
|
|
2211
|
+
let expr = this.parsePrimaryExpression();
|
|
2212
|
+
// Handle postfix operations, but stop at ".." for slice syntax
|
|
2213
|
+
while (this.check("LBRACKET") || this.check("DOT")) {
|
|
2214
|
+
if (this.check("DOT")) {
|
|
2215
|
+
// Check if this is ".." (slice syntax) - if so, stop here
|
|
2216
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
2217
|
+
if (nextToken && nextToken.type === "DOT") {
|
|
2218
|
+
// This is ".." - stop parsing, let the caller handle slice
|
|
2219
|
+
break;
|
|
2220
|
+
}
|
|
2221
|
+
// Single dot - property access
|
|
2222
|
+
this.advance(); // consume .
|
|
2223
|
+
const property = this.expectIdentifierOrKeyword();
|
|
2224
|
+
expr = { type: "propertyAccess", object: expr, property };
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
// LBRACKET - nested index access
|
|
2228
|
+
this.advance(); // consume [
|
|
2229
|
+
if (this.check("DOT")) {
|
|
2230
|
+
// [..end] - from start (implicit start = 0)
|
|
2231
|
+
this.advance();
|
|
2232
|
+
this.expect("DOT");
|
|
2233
|
+
const endExpr = this.parseSliceBoundExpression();
|
|
2234
|
+
this.expect("RBRACKET");
|
|
2235
|
+
expr = { type: "function", functionName: "SLICE_FROM_START", args: [expr, endExpr] };
|
|
2236
|
+
}
|
|
2237
|
+
else {
|
|
2238
|
+
const indexExpr = this.parseSliceBoundExpression();
|
|
2239
|
+
if (this.check("DOT")) {
|
|
2240
|
+
this.advance();
|
|
2241
|
+
this.expect("DOT");
|
|
2242
|
+
if (this.check("RBRACKET")) {
|
|
2243
|
+
// [start..] - to end (implicit end = list length)
|
|
2244
|
+
this.expect("RBRACKET");
|
|
2245
|
+
expr = { type: "function", functionName: "SLICE_TO_END", args: [expr, indexExpr] };
|
|
2246
|
+
}
|
|
2247
|
+
else {
|
|
2248
|
+
const endExpr = this.parseSliceBoundExpression();
|
|
2249
|
+
this.expect("RBRACKET");
|
|
2250
|
+
expr = { type: "function", functionName: "SLICE", args: [expr, indexExpr, endExpr] };
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
else {
|
|
2254
|
+
this.expect("RBRACKET");
|
|
2255
|
+
expr = { type: "function", functionName: "INDEX", args: [expr, indexExpr] };
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return expr;
|
|
2261
|
+
}
|
|
2262
|
+
// Parse primary expressions (atoms)
|
|
2263
|
+
parsePrimaryExpression() {
|
|
2264
|
+
const token = this.peek();
|
|
2265
|
+
// List literal [1, 2, 3]
|
|
2266
|
+
if (token.type === "LBRACKET") {
|
|
2267
|
+
return this.parseListLiteralExpression();
|
|
2268
|
+
}
|
|
2269
|
+
// Object literal { key: value, ... }
|
|
2270
|
+
if (token.type === "LBRACE") {
|
|
2271
|
+
return this.parseObjectLiteral();
|
|
2272
|
+
}
|
|
2273
|
+
// Parenthesized expression for grouping or label predicate (n:Label)
|
|
2274
|
+
if (token.type === "LPAREN") {
|
|
2275
|
+
// Check for label predicate: (n:Label) or (n:Label1:Label2)
|
|
2276
|
+
// Look ahead: ( IDENTIFIER COLON ...
|
|
2277
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
2278
|
+
const afterNext = this.tokens[this.pos + 2];
|
|
2279
|
+
if (nextToken?.type === "IDENTIFIER" && afterNext?.type === "COLON") {
|
|
2280
|
+
this.advance(); // consume (
|
|
2281
|
+
const variable = this.advance().value; // consume identifier
|
|
2282
|
+
// Parse one or more labels
|
|
2283
|
+
const labelsList = [];
|
|
2284
|
+
while (this.check("COLON")) {
|
|
2285
|
+
this.advance(); // consume :
|
|
2286
|
+
labelsList.push(this.expectLabelOrType());
|
|
2287
|
+
}
|
|
2288
|
+
this.expect("RPAREN");
|
|
2289
|
+
if (labelsList.length === 1) {
|
|
2290
|
+
return { type: "labelPredicate", variable, label: labelsList[0] };
|
|
2291
|
+
}
|
|
2292
|
+
else {
|
|
2293
|
+
return { type: "labelPredicate", variable, labels: labelsList };
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
// Regular parenthesized expression - use full expression parsing including AND/OR
|
|
2297
|
+
this.advance(); // consume (
|
|
2298
|
+
const expr = this.parseOrExpression();
|
|
2299
|
+
this.expect("RPAREN");
|
|
2300
|
+
return expr;
|
|
2301
|
+
}
|
|
2302
|
+
// CASE expression
|
|
2303
|
+
if (this.checkKeyword("CASE")) {
|
|
2304
|
+
return this.parseCaseExpression();
|
|
2305
|
+
}
|
|
2306
|
+
// Function call: COUNT(x), id(x), count(DISTINCT x), COUNT(*)
|
|
2307
|
+
// Also handles list predicates: ALL(x IN list WHERE cond), ANY(...), NONE(...), SINGLE(...)
|
|
2308
|
+
if (token.type === "KEYWORD" || token.type === "IDENTIFIER") {
|
|
2309
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
2310
|
+
if (nextToken && nextToken.type === "LPAREN") {
|
|
2311
|
+
const functionName = this.advance().value.toUpperCase();
|
|
2312
|
+
this.advance(); // LPAREN
|
|
2313
|
+
// Check if this is a list predicate: ALL, ANY, NONE, SINGLE
|
|
2314
|
+
const listPredicates = ["ALL", "ANY", "NONE", "SINGLE"];
|
|
2315
|
+
if (listPredicates.includes(functionName)) {
|
|
2316
|
+
// Check for list predicate syntax: PRED(var IN list WHERE cond)
|
|
2317
|
+
// Lookahead to see if next is identifier followed by IN
|
|
2318
|
+
if (this.check("IDENTIFIER")) {
|
|
2319
|
+
const savedPos = this.pos;
|
|
2320
|
+
const varToken = this.advance();
|
|
2321
|
+
if (this.checkKeyword("IN")) {
|
|
2322
|
+
// This is a list predicate
|
|
2323
|
+
this.advance(); // consume IN
|
|
2324
|
+
return this.parseListPredicate(functionName, varToken.value);
|
|
2325
|
+
}
|
|
2326
|
+
else {
|
|
2327
|
+
// Not a list predicate syntax, backtrack
|
|
2328
|
+
this.pos = savedPos;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
const args = [];
|
|
2333
|
+
// Check for DISTINCT keyword after opening paren (for aggregation functions)
|
|
2334
|
+
let distinct;
|
|
2335
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
2336
|
+
this.advance();
|
|
2337
|
+
distinct = true;
|
|
2338
|
+
}
|
|
2339
|
+
// Special case: COUNT(*) - handle STAR token as "count all"
|
|
2340
|
+
if (this.check("STAR")) {
|
|
2341
|
+
this.advance(); // consume STAR
|
|
2342
|
+
// COUNT(*) has no arguments - the * means "count all rows"
|
|
2343
|
+
this.expect("RPAREN");
|
|
2344
|
+
return { type: "function", functionName, args: [], distinct, star: true };
|
|
2345
|
+
}
|
|
2346
|
+
if (!this.check("RPAREN")) {
|
|
2347
|
+
do {
|
|
2348
|
+
if (args.length > 0) {
|
|
2349
|
+
this.expect("COMMA");
|
|
2350
|
+
}
|
|
2351
|
+
// Use parseReturnExpression to support comparisons and logical operators in function args
|
|
2352
|
+
args.push(this.parseReturnExpression());
|
|
2353
|
+
} while (this.check("COMMA"));
|
|
2354
|
+
}
|
|
2355
|
+
this.expect("RPAREN");
|
|
2356
|
+
return { type: "function", functionName, args, distinct };
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
// Parameter
|
|
2360
|
+
if (token.type === "PARAMETER") {
|
|
2361
|
+
this.advance();
|
|
2362
|
+
return { type: "parameter", name: token.value };
|
|
2363
|
+
}
|
|
2364
|
+
// Unary minus for negative numbers
|
|
2365
|
+
if (token.type === "DASH") {
|
|
2366
|
+
this.advance(); // consume the dash
|
|
2367
|
+
const nextToken = this.peek();
|
|
2368
|
+
if (nextToken.type === "NUMBER") {
|
|
2369
|
+
this.advance();
|
|
2370
|
+
return { type: "literal", value: -this.parseNumber(nextToken.value) };
|
|
2371
|
+
}
|
|
2372
|
+
// For more complex expressions, create a unary minus operation
|
|
2373
|
+
const operand = this.parsePrimaryExpression();
|
|
2374
|
+
return { type: "binary", operator: "-", left: { type: "literal", value: 0 }, right: operand };
|
|
2375
|
+
}
|
|
2376
|
+
// Literal values
|
|
2377
|
+
if (token.type === "STRING") {
|
|
2378
|
+
this.advance();
|
|
2379
|
+
return { type: "literal", value: token.value };
|
|
2380
|
+
}
|
|
2381
|
+
if (token.type === "NUMBER") {
|
|
2382
|
+
this.advance();
|
|
2383
|
+
const numberLiteralKind = token.value.includes(".") ? "float" : "integer";
|
|
2384
|
+
return { type: "literal", value: this.parseNumber(token.value), raw: token.value, numberLiteralKind };
|
|
2385
|
+
}
|
|
2386
|
+
if (token.type === "KEYWORD") {
|
|
2387
|
+
if (token.value === "TRUE") {
|
|
2388
|
+
this.advance();
|
|
2389
|
+
return { type: "literal", value: true };
|
|
2390
|
+
}
|
|
2391
|
+
if (token.value === "FALSE") {
|
|
2392
|
+
this.advance();
|
|
2393
|
+
return { type: "literal", value: false };
|
|
2394
|
+
}
|
|
2395
|
+
if (token.value === "NULL") {
|
|
2396
|
+
this.advance();
|
|
2397
|
+
return { type: "literal", value: null };
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
// Variable or property access
|
|
2401
|
+
// Allow keywords to be used as variable names when not in keyword position
|
|
2402
|
+
if (token.type === "IDENTIFIER" || (token.type === "KEYWORD" && !["TRUE", "FALSE", "NULL", "CASE"].includes(token.value))) {
|
|
2403
|
+
const tok = this.advance();
|
|
2404
|
+
// Use original casing for keywords used as identifiers
|
|
2405
|
+
const variable = tok.originalValue || tok.value;
|
|
2406
|
+
if (this.check("DOT")) {
|
|
2407
|
+
this.advance();
|
|
2408
|
+
// Property names can also be keywords (like 'count', 'order', etc.)
|
|
2409
|
+
const property = this.expectIdentifierOrKeyword();
|
|
2410
|
+
// Check for namespaced function call: namespace.function(args)
|
|
2411
|
+
// e.g., duration.between(), duration.inMonths(), etc.
|
|
2412
|
+
if (this.check("LPAREN")) {
|
|
2413
|
+
this.advance(); // consume (
|
|
2414
|
+
// Convert to uppercase for consistency with other functions
|
|
2415
|
+
const functionName = `${variable}.${property}`.toUpperCase();
|
|
2416
|
+
const args = [];
|
|
2417
|
+
// Check for DISTINCT keyword (for aggregation functions)
|
|
2418
|
+
let distinct;
|
|
2419
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
2420
|
+
this.advance();
|
|
2421
|
+
distinct = true;
|
|
2422
|
+
}
|
|
2423
|
+
if (!this.check("RPAREN")) {
|
|
2424
|
+
do {
|
|
2425
|
+
if (args.length > 0) {
|
|
2426
|
+
this.expect("COMMA");
|
|
2427
|
+
}
|
|
2428
|
+
args.push(this.parseReturnExpression());
|
|
2429
|
+
} while (this.check("COMMA"));
|
|
2430
|
+
}
|
|
2431
|
+
this.expect("RPAREN");
|
|
2432
|
+
return { type: "function", functionName, args, distinct };
|
|
2433
|
+
}
|
|
2434
|
+
return { type: "property", variable, property };
|
|
2435
|
+
}
|
|
2436
|
+
return { type: "variable", variable };
|
|
2437
|
+
}
|
|
2438
|
+
throw new Error(`Expected expression, got ${token.type} '${token.value}'`);
|
|
2439
|
+
}
|
|
2440
|
+
parseCaseExpression() {
|
|
2441
|
+
this.expect("KEYWORD", "CASE");
|
|
2442
|
+
// Check for simple form: CASE expr WHEN val THEN ...
|
|
2443
|
+
// vs searched form: CASE WHEN condition THEN ...
|
|
2444
|
+
let caseExpr;
|
|
2445
|
+
// If the next token is not WHEN, it's a simple form with an expression
|
|
2446
|
+
if (!this.checkKeyword("WHEN")) {
|
|
2447
|
+
caseExpr = this.parseExpression();
|
|
2448
|
+
}
|
|
2449
|
+
const whens = [];
|
|
2450
|
+
// Parse WHEN ... THEN ... clauses
|
|
2451
|
+
while (this.checkKeyword("WHEN")) {
|
|
2452
|
+
this.advance(); // consume WHEN
|
|
2453
|
+
let condition;
|
|
2454
|
+
if (caseExpr) {
|
|
2455
|
+
// Simple form: CASE expr WHEN value THEN ...
|
|
2456
|
+
// The value is compared for equality with caseExpr
|
|
2457
|
+
const whenValue = this.parseExpression();
|
|
2458
|
+
// Create an equality comparison condition
|
|
2459
|
+
condition = {
|
|
2460
|
+
type: "comparison",
|
|
2461
|
+
left: caseExpr,
|
|
2462
|
+
right: whenValue,
|
|
2463
|
+
operator: "="
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
else {
|
|
2467
|
+
// Searched form: CASE WHEN condition THEN ...
|
|
2468
|
+
condition = this.parseWhereCondition();
|
|
2469
|
+
}
|
|
2470
|
+
this.expect("KEYWORD", "THEN");
|
|
2471
|
+
const result = this.parseExpression();
|
|
2472
|
+
whens.push({ condition, result });
|
|
2473
|
+
}
|
|
2474
|
+
// Parse optional ELSE
|
|
2475
|
+
let elseExpr;
|
|
2476
|
+
if (this.checkKeyword("ELSE")) {
|
|
2477
|
+
this.advance();
|
|
2478
|
+
elseExpr = this.parseExpression();
|
|
2479
|
+
}
|
|
2480
|
+
this.expect("KEYWORD", "END");
|
|
2481
|
+
return {
|
|
2482
|
+
type: "case",
|
|
2483
|
+
expression: caseExpr,
|
|
2484
|
+
whens,
|
|
2485
|
+
elseExpr,
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
parseObjectLiteral() {
|
|
2489
|
+
this.expect("LBRACE");
|
|
2490
|
+
const properties = [];
|
|
2491
|
+
if (!this.check("RBRACE")) {
|
|
2492
|
+
do {
|
|
2493
|
+
if (properties.length > 0) {
|
|
2494
|
+
this.expect("COMMA");
|
|
2495
|
+
}
|
|
2496
|
+
// Property keys can be identifiers or keywords
|
|
2497
|
+
const key = this.expectIdentifierOrKeyword();
|
|
2498
|
+
this.expect("COLON");
|
|
2499
|
+
// Use parseReturnExpression to support comparisons like {foo: a.name='Andres'}
|
|
2500
|
+
const value = this.parseReturnExpression();
|
|
2501
|
+
properties.push({ key, value });
|
|
2502
|
+
} while (this.check("COMMA"));
|
|
2503
|
+
}
|
|
2504
|
+
this.expect("RBRACE");
|
|
2505
|
+
return { type: "object", properties };
|
|
2506
|
+
}
|
|
2507
|
+
parseListLiteralExpression() {
|
|
2508
|
+
this.expect("LBRACKET");
|
|
2509
|
+
// Check for pattern comprehension: [(pattern) WHERE cond | expr]
|
|
2510
|
+
// Pattern comprehensions start with a node pattern (parenthesis)
|
|
2511
|
+
if (this.check("LPAREN")) {
|
|
2512
|
+
return this.parsePatternComprehension();
|
|
2513
|
+
}
|
|
2514
|
+
// Check for list comprehension: [x IN list WHERE cond | expr]
|
|
2515
|
+
// Or named path in pattern comprehension: [p = (pattern) | p]
|
|
2516
|
+
// We need to look ahead to see which case this is
|
|
2517
|
+
if (this.check("IDENTIFIER")) {
|
|
2518
|
+
const savedPos = this.pos;
|
|
2519
|
+
const identifier = this.advance().value;
|
|
2520
|
+
if (this.checkKeyword("IN")) {
|
|
2521
|
+
// This is a list comprehension
|
|
2522
|
+
this.advance(); // consume "IN"
|
|
2523
|
+
return this.parseListComprehension(identifier);
|
|
2524
|
+
}
|
|
2525
|
+
else if (this.check("EQUALS")) {
|
|
2526
|
+
// This is a named path in pattern comprehension: [p = (pattern) | p]
|
|
2527
|
+
this.advance(); // consume "="
|
|
2528
|
+
return this.parsePatternComprehension(identifier);
|
|
2529
|
+
}
|
|
2530
|
+
else {
|
|
2531
|
+
// Not a list comprehension or named path, backtrack
|
|
2532
|
+
this.pos = savedPos;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
// Regular list literal - elements can be full expressions (including objects)
|
|
2536
|
+
const elements = [];
|
|
2537
|
+
if (!this.check("RBRACKET")) {
|
|
2538
|
+
do {
|
|
2539
|
+
if (elements.length > 0) {
|
|
2540
|
+
this.expect("COMMA");
|
|
2541
|
+
}
|
|
2542
|
+
elements.push(this.parseExpression());
|
|
2543
|
+
} while (this.check("COMMA"));
|
|
2544
|
+
}
|
|
2545
|
+
this.expect("RBRACKET");
|
|
2546
|
+
// If all elements are literals, return as literal list
|
|
2547
|
+
// Otherwise wrap in a function-like expression for arrays of expressions
|
|
2548
|
+
const allLiterals = elements.every(e => e.type === "literal");
|
|
2549
|
+
if (allLiterals) {
|
|
2550
|
+
return { type: "literal", value: elements.map(e => e.value) };
|
|
2551
|
+
}
|
|
2552
|
+
// For lists containing expressions, use a special function type
|
|
2553
|
+
return { type: "function", functionName: "LIST", args: elements };
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Parse a list comprehension after [variable IN has been consumed.
|
|
2557
|
+
* Full syntax: [variable IN listExpr WHERE filterCondition | mapExpr]
|
|
2558
|
+
* - WHERE and | are both optional
|
|
2559
|
+
*/
|
|
2560
|
+
parseListComprehension(variable) {
|
|
2561
|
+
// Parse the source list expression
|
|
2562
|
+
const listExpr = this.parseExpression();
|
|
2563
|
+
// Check for optional WHERE filter
|
|
2564
|
+
let filterCondition;
|
|
2565
|
+
if (this.checkKeyword("WHERE")) {
|
|
2566
|
+
this.advance();
|
|
2567
|
+
filterCondition = this.parseListComprehensionCondition(variable);
|
|
2568
|
+
}
|
|
2569
|
+
// Check for optional map projection (| expr)
|
|
2570
|
+
let mapExpr;
|
|
2571
|
+
if (this.check("PIPE")) {
|
|
2572
|
+
this.advance();
|
|
2573
|
+
mapExpr = this.parseListComprehensionExpression(variable);
|
|
2574
|
+
}
|
|
2575
|
+
this.expect("RBRACKET");
|
|
2576
|
+
return {
|
|
2577
|
+
type: "listComprehension",
|
|
2578
|
+
variable,
|
|
2579
|
+
listExpr,
|
|
2580
|
+
filterCondition,
|
|
2581
|
+
mapExpr,
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Parse a pattern comprehension after [ has been consumed and we see (.
|
|
2586
|
+
* Syntax: [(pattern) WHERE filterCondition | mapExpr]
|
|
2587
|
+
* Or with named path: [p = (pattern) WHERE filterCondition | p]
|
|
2588
|
+
* WHERE and | mapExpr are optional.
|
|
2589
|
+
*/
|
|
2590
|
+
parsePatternComprehension(pathVariable) {
|
|
2591
|
+
// Parse the pattern (reuse existing pattern parsing)
|
|
2592
|
+
const patterns = this.parsePatternChain();
|
|
2593
|
+
// Check for optional WHERE filter
|
|
2594
|
+
let filterCondition;
|
|
2595
|
+
if (this.checkKeyword("WHERE")) {
|
|
2596
|
+
this.advance();
|
|
2597
|
+
filterCondition = this.parseOrCondition();
|
|
2598
|
+
}
|
|
2599
|
+
// Check for optional map projection (| expr)
|
|
2600
|
+
let mapExpr;
|
|
2601
|
+
if (this.check("PIPE")) {
|
|
2602
|
+
this.advance();
|
|
2603
|
+
mapExpr = this.parseExpression();
|
|
2604
|
+
}
|
|
2605
|
+
this.expect("RBRACKET");
|
|
2606
|
+
return {
|
|
2607
|
+
type: "patternComprehension",
|
|
2608
|
+
patterns,
|
|
2609
|
+
filterCondition,
|
|
2610
|
+
mapExpr,
|
|
2611
|
+
pathVariable, // Named path variable (e.g., p in [p = (a)-->(b) | p])
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Parse a list predicate after PRED(variable IN has been consumed.
|
|
2616
|
+
* Syntax: ALL/ANY/NONE/SINGLE(variable IN listExpr WHERE filterCondition)
|
|
2617
|
+
* WHERE is required for list predicates.
|
|
2618
|
+
*/
|
|
2619
|
+
parseListPredicate(predicateType, variable) {
|
|
2620
|
+
// Parse the source list expression
|
|
2621
|
+
const listExpr = this.parseExpression();
|
|
2622
|
+
// WHERE clause is required for list predicates
|
|
2623
|
+
if (!this.checkKeyword("WHERE")) {
|
|
2624
|
+
throw new Error(`Expected WHERE after list expression in ${predicateType}()`);
|
|
2625
|
+
}
|
|
2626
|
+
this.advance(); // consume WHERE
|
|
2627
|
+
// Parse the filter condition
|
|
2628
|
+
const filterCondition = this.parseListComprehensionCondition(variable);
|
|
2629
|
+
this.expect("RPAREN");
|
|
2630
|
+
return {
|
|
2631
|
+
type: "listPredicate",
|
|
2632
|
+
predicateType,
|
|
2633
|
+
variable,
|
|
2634
|
+
listExpr,
|
|
2635
|
+
filterCondition,
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Parse a condition in a list comprehension, where the variable can be used.
|
|
2640
|
+
* Similar to parseWhereCondition but resolves variable references.
|
|
2641
|
+
*/
|
|
2642
|
+
parseListComprehensionCondition(variable) {
|
|
2643
|
+
return this.parseOrCondition();
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* Parse an expression in a list comprehension map projection.
|
|
2647
|
+
* Similar to parseExpression but the variable is in scope.
|
|
2648
|
+
*/
|
|
2649
|
+
parseListComprehensionExpression(variable) {
|
|
2650
|
+
return this.parseExpression();
|
|
2651
|
+
}
|
|
2652
|
+
// Token helpers
|
|
2653
|
+
peek() {
|
|
2654
|
+
return this.tokens[this.pos];
|
|
2655
|
+
}
|
|
2656
|
+
advance() {
|
|
2657
|
+
if (!this.isAtEnd()) {
|
|
2658
|
+
this.pos++;
|
|
2659
|
+
}
|
|
2660
|
+
return this.tokens[this.pos - 1];
|
|
2661
|
+
}
|
|
2662
|
+
isAtEnd() {
|
|
2663
|
+
return this.peek().type === "EOF";
|
|
2664
|
+
}
|
|
2665
|
+
check(type) {
|
|
2666
|
+
return this.peek().type === type;
|
|
2667
|
+
}
|
|
2668
|
+
checkKeyword(keyword) {
|
|
2669
|
+
const token = this.peek();
|
|
2670
|
+
return token.type === "KEYWORD" && token.value === keyword;
|
|
2671
|
+
}
|
|
2672
|
+
expect(type, value) {
|
|
2673
|
+
const token = this.peek();
|
|
2674
|
+
if (token.type !== type || (value !== undefined && token.value !== value)) {
|
|
2675
|
+
throw new Error(`Expected ${type}${value ? ` '${value}'` : ""}, got ${token.type} '${token.value}'`);
|
|
2676
|
+
}
|
|
2677
|
+
return this.advance();
|
|
2678
|
+
}
|
|
2679
|
+
expectIdentifier() {
|
|
2680
|
+
const token = this.peek();
|
|
2681
|
+
if (token.type !== "IDENTIFIER") {
|
|
2682
|
+
throw new Error(`Expected identifier, got ${token.type} '${token.value}'`);
|
|
2683
|
+
}
|
|
2684
|
+
return this.advance().value;
|
|
2685
|
+
}
|
|
2686
|
+
expectIdentifierOrKeyword() {
|
|
2687
|
+
const token = this.peek();
|
|
2688
|
+
if (token.type !== "IDENTIFIER" && token.type !== "KEYWORD") {
|
|
2689
|
+
throw new Error(`Expected identifier or keyword, got ${token.type} '${token.value}'`);
|
|
2690
|
+
}
|
|
2691
|
+
// Keywords preserve their original casing when used as identifiers (e.g., map keys)
|
|
2692
|
+
// originalValue stores the original casing before uppercasing for keyword matching
|
|
2693
|
+
this.advance();
|
|
2694
|
+
return token.originalValue || token.value;
|
|
2695
|
+
}
|
|
2696
|
+
expectLabelOrType() {
|
|
2697
|
+
const token = this.peek();
|
|
2698
|
+
if (token.type !== "IDENTIFIER" && token.type !== "KEYWORD") {
|
|
2699
|
+
throw new Error(`Expected label or type, got ${token.type} '${token.value}'`);
|
|
2700
|
+
}
|
|
2701
|
+
this.advance();
|
|
2702
|
+
// Labels and types preserve their original case from the query
|
|
2703
|
+
// Use originalValue for keywords (which stores the original casing before uppercasing)
|
|
2704
|
+
return token.originalValue || token.value;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
// Convenience function
|
|
2708
|
+
export function parse(input) {
|
|
2709
|
+
return new Parser().parse(input);
|
|
2710
|
+
}
|
|
2711
|
+
//# sourceMappingURL=parser.js.map
|