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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +456 -0
  3. package/dist/auth.d.ts +66 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +148 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/backup.d.ts +51 -0
  8. package/dist/backup.d.ts.map +1 -0
  9. package/dist/backup.js +201 -0
  10. package/dist/backup.js.map +1 -0
  11. package/dist/cli-helpers.d.ts +17 -0
  12. package/dist/cli-helpers.d.ts.map +1 -0
  13. package/dist/cli-helpers.js +121 -0
  14. package/dist/cli-helpers.js.map +1 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +660 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/db.d.ts +118 -0
  20. package/dist/db.d.ts.map +1 -0
  21. package/dist/db.js +720 -0
  22. package/dist/db.js.map +1 -0
  23. package/dist/executor.d.ts +663 -0
  24. package/dist/executor.d.ts.map +1 -0
  25. package/dist/executor.js +8578 -0
  26. package/dist/executor.js.map +1 -0
  27. package/dist/index.d.ts +62 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +86 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/local.d.ts +7 -0
  32. package/dist/local.d.ts.map +1 -0
  33. package/dist/local.js +119 -0
  34. package/dist/local.js.map +1 -0
  35. package/dist/parser.d.ts +365 -0
  36. package/dist/parser.d.ts.map +1 -0
  37. package/dist/parser.js +2711 -0
  38. package/dist/parser.js.map +1 -0
  39. package/dist/property-value.d.ts +3 -0
  40. package/dist/property-value.d.ts.map +1 -0
  41. package/dist/property-value.js +30 -0
  42. package/dist/property-value.js.map +1 -0
  43. package/dist/remote.d.ts +6 -0
  44. package/dist/remote.d.ts.map +1 -0
  45. package/dist/remote.js +93 -0
  46. package/dist/remote.js.map +1 -0
  47. package/dist/routes.d.ts +31 -0
  48. package/dist/routes.d.ts.map +1 -0
  49. package/dist/routes.js +202 -0
  50. package/dist/routes.js.map +1 -0
  51. package/dist/server.d.ts +16 -0
  52. package/dist/server.d.ts.map +1 -0
  53. package/dist/server.js +25 -0
  54. package/dist/server.js.map +1 -0
  55. package/dist/translator.d.ts +330 -0
  56. package/dist/translator.d.ts.map +1 -0
  57. package/dist/translator.js +13712 -0
  58. package/dist/translator.js.map +1 -0
  59. package/dist/types.d.ts +136 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +21 -0
  62. package/dist/types.js.map +1 -0
  63. 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