nicefox-graphdb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +417 -0
- package/package.json +78 -0
- package/packages/nicefox-graphdb/LICENSE +21 -0
- package/packages/nicefox-graphdb/README.md +417 -0
- package/packages/nicefox-graphdb/dist/auth.d.ts +66 -0
- package/packages/nicefox-graphdb/dist/auth.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/auth.js +148 -0
- package/packages/nicefox-graphdb/dist/auth.js.map +1 -0
- package/packages/nicefox-graphdb/dist/backup.d.ts +51 -0
- package/packages/nicefox-graphdb/dist/backup.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/backup.js +201 -0
- package/packages/nicefox-graphdb/dist/backup.js.map +1 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.d.ts +17 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.js +121 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.js.map +1 -0
- package/packages/nicefox-graphdb/dist/cli.d.ts +3 -0
- package/packages/nicefox-graphdb/dist/cli.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/cli.js +660 -0
- package/packages/nicefox-graphdb/dist/cli.js.map +1 -0
- package/packages/nicefox-graphdb/dist/db.d.ts +118 -0
- package/packages/nicefox-graphdb/dist/db.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/db.js +245 -0
- package/packages/nicefox-graphdb/dist/db.js.map +1 -0
- package/packages/nicefox-graphdb/dist/executor.d.ts +272 -0
- package/packages/nicefox-graphdb/dist/executor.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/executor.js +3579 -0
- package/packages/nicefox-graphdb/dist/executor.js.map +1 -0
- package/packages/nicefox-graphdb/dist/index.d.ts +54 -0
- package/packages/nicefox-graphdb/dist/index.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/index.js +74 -0
- package/packages/nicefox-graphdb/dist/index.js.map +1 -0
- package/packages/nicefox-graphdb/dist/local.d.ts +7 -0
- package/packages/nicefox-graphdb/dist/local.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/local.js +115 -0
- package/packages/nicefox-graphdb/dist/local.js.map +1 -0
- package/packages/nicefox-graphdb/dist/parser.d.ts +300 -0
- package/packages/nicefox-graphdb/dist/parser.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/parser.js +1891 -0
- package/packages/nicefox-graphdb/dist/parser.js.map +1 -0
- package/packages/nicefox-graphdb/dist/remote.d.ts +6 -0
- package/packages/nicefox-graphdb/dist/remote.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/remote.js +87 -0
- package/packages/nicefox-graphdb/dist/remote.js.map +1 -0
- package/packages/nicefox-graphdb/dist/routes.d.ts +31 -0
- package/packages/nicefox-graphdb/dist/routes.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/routes.js +202 -0
- package/packages/nicefox-graphdb/dist/routes.js.map +1 -0
- package/packages/nicefox-graphdb/dist/translator.d.ts +136 -0
- package/packages/nicefox-graphdb/dist/translator.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/translator.js +4849 -0
- package/packages/nicefox-graphdb/dist/translator.js.map +1 -0
- package/packages/nicefox-graphdb/dist/types.d.ts +133 -0
- package/packages/nicefox-graphdb/dist/types.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/types.js +21 -0
- package/packages/nicefox-graphdb/dist/types.js.map +1 -0
|
@@ -0,0 +1,1891 @@
|
|
|
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
|
+
"NOT",
|
|
14
|
+
"IN",
|
|
15
|
+
"LIMIT",
|
|
16
|
+
"SKIP",
|
|
17
|
+
"ORDER",
|
|
18
|
+
"BY",
|
|
19
|
+
"ASC",
|
|
20
|
+
"DESC",
|
|
21
|
+
"COUNT",
|
|
22
|
+
"ON",
|
|
23
|
+
"TRUE",
|
|
24
|
+
"FALSE",
|
|
25
|
+
"NULL",
|
|
26
|
+
"CONTAINS",
|
|
27
|
+
"STARTS",
|
|
28
|
+
"ENDS",
|
|
29
|
+
"WITH",
|
|
30
|
+
"AS",
|
|
31
|
+
"IS",
|
|
32
|
+
"DISTINCT",
|
|
33
|
+
"OPTIONAL",
|
|
34
|
+
"UNWIND",
|
|
35
|
+
"CASE",
|
|
36
|
+
"WHEN",
|
|
37
|
+
"THEN",
|
|
38
|
+
"ELSE",
|
|
39
|
+
"END",
|
|
40
|
+
"EXISTS",
|
|
41
|
+
"UNION",
|
|
42
|
+
"ALL",
|
|
43
|
+
"ANY",
|
|
44
|
+
"NONE",
|
|
45
|
+
"SINGLE",
|
|
46
|
+
"CALL",
|
|
47
|
+
"YIELD",
|
|
48
|
+
]);
|
|
49
|
+
class Tokenizer {
|
|
50
|
+
input;
|
|
51
|
+
pos = 0;
|
|
52
|
+
line = 1;
|
|
53
|
+
column = 1;
|
|
54
|
+
tokens = [];
|
|
55
|
+
constructor(input) {
|
|
56
|
+
this.input = input;
|
|
57
|
+
}
|
|
58
|
+
tokenize() {
|
|
59
|
+
while (this.pos < this.input.length) {
|
|
60
|
+
this.skipWhitespace();
|
|
61
|
+
if (this.pos >= this.input.length)
|
|
62
|
+
break;
|
|
63
|
+
const token = this.nextToken();
|
|
64
|
+
if (token) {
|
|
65
|
+
this.tokens.push(token);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.tokens.push({
|
|
69
|
+
type: "EOF",
|
|
70
|
+
value: "",
|
|
71
|
+
position: this.pos,
|
|
72
|
+
line: this.line,
|
|
73
|
+
column: this.column,
|
|
74
|
+
});
|
|
75
|
+
return this.tokens;
|
|
76
|
+
}
|
|
77
|
+
skipWhitespace() {
|
|
78
|
+
while (this.pos < this.input.length) {
|
|
79
|
+
const char = this.input[this.pos];
|
|
80
|
+
if (char === " " || char === "\t") {
|
|
81
|
+
this.pos++;
|
|
82
|
+
this.column++;
|
|
83
|
+
}
|
|
84
|
+
else if (char === "\n") {
|
|
85
|
+
this.pos++;
|
|
86
|
+
this.line++;
|
|
87
|
+
this.column = 1;
|
|
88
|
+
}
|
|
89
|
+
else if (char === "\r") {
|
|
90
|
+
this.pos++;
|
|
91
|
+
if (this.input[this.pos] === "\n") {
|
|
92
|
+
this.pos++;
|
|
93
|
+
}
|
|
94
|
+
this.line++;
|
|
95
|
+
this.column = 1;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
nextToken() {
|
|
103
|
+
const startPos = this.pos;
|
|
104
|
+
const startLine = this.line;
|
|
105
|
+
const startColumn = this.column;
|
|
106
|
+
const char = this.input[this.pos];
|
|
107
|
+
// Two-character operators
|
|
108
|
+
if (this.pos + 1 < this.input.length) {
|
|
109
|
+
const twoChars = this.input.slice(this.pos, this.pos + 2);
|
|
110
|
+
if (twoChars === "<-") {
|
|
111
|
+
this.pos += 2;
|
|
112
|
+
this.column += 2;
|
|
113
|
+
return { type: "ARROW_LEFT", value: "<-", position: startPos, line: startLine, column: startColumn };
|
|
114
|
+
}
|
|
115
|
+
if (twoChars === "->") {
|
|
116
|
+
this.pos += 2;
|
|
117
|
+
this.column += 2;
|
|
118
|
+
return { type: "ARROW_RIGHT", value: "->", position: startPos, line: startLine, column: startColumn };
|
|
119
|
+
}
|
|
120
|
+
if (twoChars === "<>") {
|
|
121
|
+
this.pos += 2;
|
|
122
|
+
this.column += 2;
|
|
123
|
+
return { type: "NOT_EQUALS", value: "<>", position: startPos, line: startLine, column: startColumn };
|
|
124
|
+
}
|
|
125
|
+
if (twoChars === "<=") {
|
|
126
|
+
this.pos += 2;
|
|
127
|
+
this.column += 2;
|
|
128
|
+
return { type: "LTE", value: "<=", position: startPos, line: startLine, column: startColumn };
|
|
129
|
+
}
|
|
130
|
+
if (twoChars === ">=") {
|
|
131
|
+
this.pos += 2;
|
|
132
|
+
this.column += 2;
|
|
133
|
+
return { type: "GTE", value: ">=", position: startPos, line: startLine, column: startColumn };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Single character tokens
|
|
137
|
+
const singleCharTokens = {
|
|
138
|
+
"(": "LPAREN",
|
|
139
|
+
")": "RPAREN",
|
|
140
|
+
"[": "LBRACKET",
|
|
141
|
+
"]": "RBRACKET",
|
|
142
|
+
"{": "LBRACE",
|
|
143
|
+
"}": "RBRACE",
|
|
144
|
+
":": "COLON",
|
|
145
|
+
",": "COMMA",
|
|
146
|
+
".": "DOT",
|
|
147
|
+
"-": "DASH",
|
|
148
|
+
"+": "PLUS",
|
|
149
|
+
"/": "SLASH",
|
|
150
|
+
"%": "PERCENT",
|
|
151
|
+
"^": "CARET",
|
|
152
|
+
"=": "EQUALS",
|
|
153
|
+
"<": "LT",
|
|
154
|
+
">": "GT",
|
|
155
|
+
"*": "STAR",
|
|
156
|
+
"|": "PIPE",
|
|
157
|
+
};
|
|
158
|
+
// Number - includes floats starting with . like .5
|
|
159
|
+
// Check this before single char tokens so ".5" is parsed as number not DOT
|
|
160
|
+
// But don't match "..3" as ".3" - only match if there's no preceding dot
|
|
161
|
+
if (this.isDigit(char) || (char === "-" && this.isDigit(this.input[this.pos + 1])) || (char === "." && this.isDigit(this.input[this.pos + 1]) && (this.pos === 0 || this.input[this.pos - 1] !== "."))) {
|
|
162
|
+
return this.readNumber(startPos, startLine, startColumn);
|
|
163
|
+
}
|
|
164
|
+
if (singleCharTokens[char]) {
|
|
165
|
+
this.pos++;
|
|
166
|
+
this.column++;
|
|
167
|
+
return { type: singleCharTokens[char], value: char, position: startPos, line: startLine, column: startColumn };
|
|
168
|
+
}
|
|
169
|
+
// Parameter
|
|
170
|
+
if (char === "$") {
|
|
171
|
+
this.pos++;
|
|
172
|
+
this.column++;
|
|
173
|
+
const name = this.readIdentifier();
|
|
174
|
+
return { type: "PARAMETER", value: name, position: startPos, line: startLine, column: startColumn };
|
|
175
|
+
}
|
|
176
|
+
// String
|
|
177
|
+
if (char === "'" || char === '"') {
|
|
178
|
+
return this.readString(char, startPos, startLine, startColumn);
|
|
179
|
+
}
|
|
180
|
+
// Identifier or keyword
|
|
181
|
+
if (this.isIdentifierStart(char)) {
|
|
182
|
+
const value = this.readIdentifier();
|
|
183
|
+
const upperValue = value.toUpperCase();
|
|
184
|
+
const type = KEYWORDS.has(upperValue) ? "KEYWORD" : "IDENTIFIER";
|
|
185
|
+
// Keywords store uppercase for matching, but we also preserve original casing for when keywords are used as identifiers
|
|
186
|
+
return { type, value: type === "KEYWORD" ? upperValue : value, originalValue: value, position: startPos, line: startLine, column: startColumn };
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`Unexpected character '${char}' at position ${this.pos}`);
|
|
189
|
+
}
|
|
190
|
+
readString(quote, startPos, startLine, startColumn) {
|
|
191
|
+
this.pos++;
|
|
192
|
+
this.column++;
|
|
193
|
+
let value = "";
|
|
194
|
+
while (this.pos < this.input.length) {
|
|
195
|
+
const char = this.input[this.pos];
|
|
196
|
+
if (char === quote) {
|
|
197
|
+
this.pos++;
|
|
198
|
+
this.column++;
|
|
199
|
+
return { type: "STRING", value, position: startPos, line: startLine, column: startColumn };
|
|
200
|
+
}
|
|
201
|
+
if (char === "\\") {
|
|
202
|
+
this.pos++;
|
|
203
|
+
this.column++;
|
|
204
|
+
const escaped = this.input[this.pos];
|
|
205
|
+
if (escaped === "n")
|
|
206
|
+
value += "\n";
|
|
207
|
+
else if (escaped === "t")
|
|
208
|
+
value += "\t";
|
|
209
|
+
else if (escaped === "\\")
|
|
210
|
+
value += "\\";
|
|
211
|
+
else if (escaped === quote)
|
|
212
|
+
value += quote;
|
|
213
|
+
else
|
|
214
|
+
value += escaped;
|
|
215
|
+
this.pos++;
|
|
216
|
+
this.column++;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
value += char;
|
|
220
|
+
this.pos++;
|
|
221
|
+
this.column++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`Unterminated string at position ${startPos}`);
|
|
225
|
+
}
|
|
226
|
+
readNumber(startPos, startLine, startColumn) {
|
|
227
|
+
let value = "";
|
|
228
|
+
if (this.input[this.pos] === "-") {
|
|
229
|
+
value += "-";
|
|
230
|
+
this.pos++;
|
|
231
|
+
this.column++;
|
|
232
|
+
}
|
|
233
|
+
// Handle numbers starting with . like .5
|
|
234
|
+
if (this.input[this.pos] === ".") {
|
|
235
|
+
value += "0.";
|
|
236
|
+
this.pos++;
|
|
237
|
+
this.column++;
|
|
238
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
239
|
+
value += this.input[this.pos];
|
|
240
|
+
this.pos++;
|
|
241
|
+
this.column++;
|
|
242
|
+
}
|
|
243
|
+
return { type: "NUMBER", value, position: startPos, line: startLine, column: startColumn };
|
|
244
|
+
}
|
|
245
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
246
|
+
value += this.input[this.pos];
|
|
247
|
+
this.pos++;
|
|
248
|
+
this.column++;
|
|
249
|
+
}
|
|
250
|
+
// Only read decimal part if . is followed by a digit (not another .)
|
|
251
|
+
// This prevents "1..2" from being tokenized as "1." + "." + "2"
|
|
252
|
+
if (this.input[this.pos] === "." && this.isDigit(this.input[this.pos + 1])) {
|
|
253
|
+
value += ".";
|
|
254
|
+
this.pos++;
|
|
255
|
+
this.column++;
|
|
256
|
+
while (this.pos < this.input.length && this.isDigit(this.input[this.pos])) {
|
|
257
|
+
value += this.input[this.pos];
|
|
258
|
+
this.pos++;
|
|
259
|
+
this.column++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { type: "NUMBER", value, position: startPos, line: startLine, column: startColumn };
|
|
263
|
+
}
|
|
264
|
+
readIdentifier() {
|
|
265
|
+
let value = "";
|
|
266
|
+
while (this.pos < this.input.length && this.isIdentifierChar(this.input[this.pos])) {
|
|
267
|
+
value += this.input[this.pos];
|
|
268
|
+
this.pos++;
|
|
269
|
+
this.column++;
|
|
270
|
+
}
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
isDigit(char) {
|
|
274
|
+
return char >= "0" && char <= "9";
|
|
275
|
+
}
|
|
276
|
+
isIdentifierStart(char) {
|
|
277
|
+
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z") || char === "_";
|
|
278
|
+
}
|
|
279
|
+
isIdentifierChar(char) {
|
|
280
|
+
return this.isIdentifierStart(char) || this.isDigit(char);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Parser
|
|
285
|
+
// ============================================================================
|
|
286
|
+
export class Parser {
|
|
287
|
+
tokens = [];
|
|
288
|
+
pos = 0;
|
|
289
|
+
anonVarCounter = 0;
|
|
290
|
+
parse(input) {
|
|
291
|
+
try {
|
|
292
|
+
const tokenizer = new Tokenizer(input);
|
|
293
|
+
this.tokens = tokenizer.tokenize();
|
|
294
|
+
this.pos = 0;
|
|
295
|
+
const query = this.parseQuery();
|
|
296
|
+
if (query.clauses.length === 0) {
|
|
297
|
+
return this.error("Empty query");
|
|
298
|
+
}
|
|
299
|
+
return { success: true, query };
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
const currentToken = this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
|
|
303
|
+
return {
|
|
304
|
+
success: false,
|
|
305
|
+
error: {
|
|
306
|
+
message: e instanceof Error ? e.message : String(e),
|
|
307
|
+
position: currentToken?.position ?? 0,
|
|
308
|
+
line: currentToken?.line ?? 1,
|
|
309
|
+
column: currentToken?.column ?? 1,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
parseQuery() {
|
|
315
|
+
// Parse clauses until we hit UNION or end
|
|
316
|
+
const clauses = [];
|
|
317
|
+
while (!this.isAtEnd() && !this.checkKeyword("UNION")) {
|
|
318
|
+
const clause = this.parseClause();
|
|
319
|
+
if (clause) {
|
|
320
|
+
clauses.push(clause);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Check for UNION
|
|
324
|
+
if (this.checkKeyword("UNION")) {
|
|
325
|
+
this.advance(); // consume UNION
|
|
326
|
+
// Check for ALL
|
|
327
|
+
const all = this.checkKeyword("ALL");
|
|
328
|
+
if (all) {
|
|
329
|
+
this.advance();
|
|
330
|
+
}
|
|
331
|
+
// Parse the right side of the UNION
|
|
332
|
+
const rightQuery = this.parseQuery();
|
|
333
|
+
// Create a UNION clause that wraps both queries
|
|
334
|
+
const unionClause = {
|
|
335
|
+
type: "UNION",
|
|
336
|
+
all,
|
|
337
|
+
left: { clauses },
|
|
338
|
+
right: rightQuery,
|
|
339
|
+
};
|
|
340
|
+
return { clauses: [unionClause] };
|
|
341
|
+
}
|
|
342
|
+
return { clauses };
|
|
343
|
+
}
|
|
344
|
+
error(message) {
|
|
345
|
+
const currentToken = this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: {
|
|
349
|
+
message,
|
|
350
|
+
position: currentToken?.position ?? 0,
|
|
351
|
+
line: currentToken?.line ?? 1,
|
|
352
|
+
column: currentToken?.column ?? 1,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
parseClause() {
|
|
357
|
+
const token = this.peek();
|
|
358
|
+
if (token.type === "EOF")
|
|
359
|
+
return null;
|
|
360
|
+
if (token.type !== "KEYWORD") {
|
|
361
|
+
throw new Error(`Unexpected token '${token.value}', expected a clause keyword like CREATE, MATCH, MERGE, SET, DELETE, or RETURN`);
|
|
362
|
+
}
|
|
363
|
+
switch (token.value) {
|
|
364
|
+
case "CREATE":
|
|
365
|
+
return this.parseCreate();
|
|
366
|
+
case "MATCH":
|
|
367
|
+
return this.parseMatch(false);
|
|
368
|
+
case "OPTIONAL":
|
|
369
|
+
return this.parseOptionalMatch();
|
|
370
|
+
case "MERGE":
|
|
371
|
+
return this.parseMerge();
|
|
372
|
+
case "SET":
|
|
373
|
+
return this.parseSet();
|
|
374
|
+
case "DELETE":
|
|
375
|
+
case "DETACH":
|
|
376
|
+
return this.parseDelete();
|
|
377
|
+
case "RETURN":
|
|
378
|
+
return this.parseReturn();
|
|
379
|
+
case "WITH":
|
|
380
|
+
return this.parseWith();
|
|
381
|
+
case "UNWIND":
|
|
382
|
+
return this.parseUnwind();
|
|
383
|
+
case "CALL":
|
|
384
|
+
return this.parseCall();
|
|
385
|
+
default:
|
|
386
|
+
throw new Error(`Unexpected keyword '${token.value}'`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
parseCreate() {
|
|
390
|
+
this.expect("KEYWORD", "CREATE");
|
|
391
|
+
const patterns = [];
|
|
392
|
+
patterns.push(...this.parsePatternChain());
|
|
393
|
+
while (this.check("COMMA")) {
|
|
394
|
+
this.advance();
|
|
395
|
+
patterns.push(...this.parsePatternChain());
|
|
396
|
+
}
|
|
397
|
+
// Validate: CREATE requires relationship type and direction
|
|
398
|
+
for (const pattern of patterns) {
|
|
399
|
+
if ("edge" in pattern) {
|
|
400
|
+
// This is a RelationshipPattern
|
|
401
|
+
if (!pattern.edge.type && !pattern.edge.types) {
|
|
402
|
+
throw new Error("A relationship type is required to create a relationship");
|
|
403
|
+
}
|
|
404
|
+
if (pattern.edge.direction === "none") {
|
|
405
|
+
throw new Error("Only directed relationships are supported in CREATE");
|
|
406
|
+
}
|
|
407
|
+
// Multiple relationship types are not allowed in CREATE
|
|
408
|
+
if (pattern.edge.types && pattern.edge.types.length > 1) {
|
|
409
|
+
throw new Error("A single relationship type must be specified for CREATE");
|
|
410
|
+
}
|
|
411
|
+
// Variable-length patterns are not allowed in CREATE
|
|
412
|
+
if (pattern.edge.minHops !== undefined || pattern.edge.maxHops !== undefined) {
|
|
413
|
+
throw new Error("Variable length relationship patterns are not supported in CREATE");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { type: "CREATE", patterns };
|
|
418
|
+
}
|
|
419
|
+
parseMatch(optional = false) {
|
|
420
|
+
this.expect("KEYWORD", "MATCH");
|
|
421
|
+
const patterns = [];
|
|
422
|
+
const pathExpressions = [];
|
|
423
|
+
// Parse first pattern or path expression
|
|
424
|
+
const firstPattern = this.parsePatternOrPath();
|
|
425
|
+
if ("type" in firstPattern && firstPattern.type === "path") {
|
|
426
|
+
pathExpressions.push(firstPattern);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
patterns.push(...(Array.isArray(firstPattern) ? firstPattern : [firstPattern]));
|
|
430
|
+
}
|
|
431
|
+
while (this.check("COMMA")) {
|
|
432
|
+
this.advance();
|
|
433
|
+
const nextPattern = this.parsePatternOrPath();
|
|
434
|
+
if ("type" in nextPattern && nextPattern.type === "path") {
|
|
435
|
+
pathExpressions.push(nextPattern);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
patterns.push(...(Array.isArray(nextPattern) ? nextPattern : [nextPattern]));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
let where;
|
|
442
|
+
if (this.checkKeyword("WHERE")) {
|
|
443
|
+
this.advance();
|
|
444
|
+
where = this.parseWhereCondition();
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
type: optional ? "OPTIONAL_MATCH" : "MATCH",
|
|
448
|
+
patterns,
|
|
449
|
+
pathExpressions: pathExpressions.length > 0 ? pathExpressions : undefined,
|
|
450
|
+
where
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Parse either a regular pattern chain or a named path expression.
|
|
455
|
+
* Syntax: p = (a)-[r]->(b) or just (a)-[r]->(b)
|
|
456
|
+
*/
|
|
457
|
+
parsePatternOrPath() {
|
|
458
|
+
// Check for path expression syntax: identifier = pattern
|
|
459
|
+
if (this.check("IDENTIFIER")) {
|
|
460
|
+
const savedPos = this.pos;
|
|
461
|
+
const identifier = this.advance().value;
|
|
462
|
+
if (this.check("EQUALS")) {
|
|
463
|
+
// This is a path expression: p = (a)-[r]->(b)
|
|
464
|
+
this.advance(); // consume "="
|
|
465
|
+
const patterns = this.parsePatternChain();
|
|
466
|
+
return {
|
|
467
|
+
type: "path",
|
|
468
|
+
variable: identifier,
|
|
469
|
+
patterns
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// Not a path expression, backtrack
|
|
474
|
+
this.pos = savedPos;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Regular pattern chain
|
|
478
|
+
return this.parsePatternChain();
|
|
479
|
+
}
|
|
480
|
+
parseOptionalMatch() {
|
|
481
|
+
this.expect("KEYWORD", "OPTIONAL");
|
|
482
|
+
return this.parseMatch(true);
|
|
483
|
+
}
|
|
484
|
+
parseMerge() {
|
|
485
|
+
this.expect("KEYWORD", "MERGE");
|
|
486
|
+
const patterns = this.parsePatternChain();
|
|
487
|
+
let onCreateSet;
|
|
488
|
+
let onMatchSet;
|
|
489
|
+
while (this.checkKeyword("ON")) {
|
|
490
|
+
this.advance();
|
|
491
|
+
if (this.checkKeyword("CREATE")) {
|
|
492
|
+
this.advance();
|
|
493
|
+
this.expect("KEYWORD", "SET");
|
|
494
|
+
onCreateSet = this.parseSetAssignments();
|
|
495
|
+
}
|
|
496
|
+
else if (this.checkKeyword("MATCH")) {
|
|
497
|
+
this.advance();
|
|
498
|
+
this.expect("KEYWORD", "SET");
|
|
499
|
+
onMatchSet = this.parseSetAssignments();
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
throw new Error("Expected CREATE or MATCH after ON");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return { type: "MERGE", patterns, onCreateSet, onMatchSet };
|
|
506
|
+
}
|
|
507
|
+
parseSet() {
|
|
508
|
+
this.expect("KEYWORD", "SET");
|
|
509
|
+
const assignments = this.parseSetAssignments();
|
|
510
|
+
return { type: "SET", assignments };
|
|
511
|
+
}
|
|
512
|
+
parseSetAssignments() {
|
|
513
|
+
const assignments = [];
|
|
514
|
+
do {
|
|
515
|
+
if (assignments.length > 0) {
|
|
516
|
+
this.expect("COMMA");
|
|
517
|
+
}
|
|
518
|
+
// Handle parenthesized expression: SET (n).property = value
|
|
519
|
+
let variable;
|
|
520
|
+
if (this.check("LPAREN")) {
|
|
521
|
+
this.advance();
|
|
522
|
+
variable = this.expectIdentifier();
|
|
523
|
+
this.expect("RPAREN");
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
variable = this.expectIdentifier();
|
|
527
|
+
}
|
|
528
|
+
// Check for label assignment: SET n:Label or SET n :Label (with whitespace)
|
|
529
|
+
if (this.check("COLON")) {
|
|
530
|
+
// Label assignment: SET n:Label1:Label2
|
|
531
|
+
const labels = [];
|
|
532
|
+
while (this.check("COLON")) {
|
|
533
|
+
this.advance(); // consume ":"
|
|
534
|
+
labels.push(this.expectLabelOrType());
|
|
535
|
+
}
|
|
536
|
+
assignments.push({ variable, labels });
|
|
537
|
+
}
|
|
538
|
+
else if (this.check("PLUS")) {
|
|
539
|
+
// Property merge: SET n += {props}
|
|
540
|
+
this.advance(); // consume "+"
|
|
541
|
+
this.expect("EQUALS");
|
|
542
|
+
const value = this.parseExpression();
|
|
543
|
+
assignments.push({ variable, value, mergeProps: true });
|
|
544
|
+
}
|
|
545
|
+
else if (this.check("EQUALS")) {
|
|
546
|
+
// Property replace: SET n = {props}
|
|
547
|
+
this.advance(); // consume "="
|
|
548
|
+
const value = this.parseExpression();
|
|
549
|
+
assignments.push({ variable, value, replaceProps: true });
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// Property assignment: SET n.property = value
|
|
553
|
+
this.expect("DOT");
|
|
554
|
+
const property = this.expectIdentifier();
|
|
555
|
+
this.expect("EQUALS");
|
|
556
|
+
const value = this.parseExpression();
|
|
557
|
+
assignments.push({ variable, property, value });
|
|
558
|
+
}
|
|
559
|
+
} while (this.check("COMMA"));
|
|
560
|
+
return assignments;
|
|
561
|
+
}
|
|
562
|
+
parseDelete() {
|
|
563
|
+
let detach = false;
|
|
564
|
+
if (this.checkKeyword("DETACH")) {
|
|
565
|
+
this.advance();
|
|
566
|
+
detach = true;
|
|
567
|
+
}
|
|
568
|
+
this.expect("KEYWORD", "DELETE");
|
|
569
|
+
const variables = [];
|
|
570
|
+
const expressions = [];
|
|
571
|
+
// Parse first delete target (can be simple variable or complex expression)
|
|
572
|
+
this.parseDeleteTarget(variables, expressions);
|
|
573
|
+
while (this.check("COMMA")) {
|
|
574
|
+
this.advance();
|
|
575
|
+
this.parseDeleteTarget(variables, expressions);
|
|
576
|
+
}
|
|
577
|
+
const result = { type: "DELETE", variables, detach };
|
|
578
|
+
if (expressions.length > 0) {
|
|
579
|
+
result.expressions = expressions;
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
parseDeleteTarget(variables, expressions) {
|
|
584
|
+
// Check if this is a simple variable or a complex expression
|
|
585
|
+
// Look ahead to see if it's identifier followed by [ (list access) or . (property access)
|
|
586
|
+
const token = this.peek();
|
|
587
|
+
if (token.type === "IDENTIFIER") {
|
|
588
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
589
|
+
if (nextToken && (nextToken.type === "LBRACKET" || nextToken.type === "DOT")) {
|
|
590
|
+
// This is a list access expression like friends[$index]
|
|
591
|
+
// or a property access expression like nodes.key
|
|
592
|
+
const expr = this.parseExpression();
|
|
593
|
+
expressions.push(expr);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Simple variable name
|
|
597
|
+
variables.push(this.advance().value);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
// DELETE requires a variable or variable-based expression (like list[index])
|
|
602
|
+
// Other expression types (literals, arithmetic, etc.) are not valid DELETE targets
|
|
603
|
+
throw new Error(`Type mismatch: expected Node or Relationship but was ${token.type === "NUMBER" ? "Integer" : token.type === "STRING" ? "String" : token.value}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
parseReturn() {
|
|
607
|
+
this.expect("KEYWORD", "RETURN");
|
|
608
|
+
// Check for DISTINCT after RETURN
|
|
609
|
+
let distinct;
|
|
610
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
611
|
+
this.advance();
|
|
612
|
+
distinct = true;
|
|
613
|
+
}
|
|
614
|
+
const items = [];
|
|
615
|
+
// Check for RETURN * syntax (return all matched variables)
|
|
616
|
+
if (this.check("STAR")) {
|
|
617
|
+
this.advance();
|
|
618
|
+
// Mark with special "*" variable to indicate return all
|
|
619
|
+
items.push({ expression: { type: "variable", variable: "*" } });
|
|
620
|
+
// After *, we might have additional items with comma (unlikely but possible)
|
|
621
|
+
// e.g., RETURN *, count(*) AS cnt - but this is rare
|
|
622
|
+
}
|
|
623
|
+
if (items.length === 0 || this.check("COMMA")) {
|
|
624
|
+
if (items.length > 0) {
|
|
625
|
+
this.advance(); // consume comma after *
|
|
626
|
+
}
|
|
627
|
+
do {
|
|
628
|
+
if (items.length > 0) {
|
|
629
|
+
this.expect("COMMA");
|
|
630
|
+
}
|
|
631
|
+
// Use parseReturnExpression to allow comparisons in RETURN items
|
|
632
|
+
const expression = this.parseReturnExpression();
|
|
633
|
+
let alias;
|
|
634
|
+
if (this.checkKeyword("AS")) {
|
|
635
|
+
this.advance();
|
|
636
|
+
alias = this.expectIdentifierOrKeyword();
|
|
637
|
+
}
|
|
638
|
+
items.push({ expression, alias });
|
|
639
|
+
} while (this.check("COMMA"));
|
|
640
|
+
}
|
|
641
|
+
// Parse ORDER BY
|
|
642
|
+
let orderBy;
|
|
643
|
+
if (this.checkKeyword("ORDER")) {
|
|
644
|
+
this.advance();
|
|
645
|
+
this.expect("KEYWORD", "BY");
|
|
646
|
+
orderBy = [];
|
|
647
|
+
do {
|
|
648
|
+
if (orderBy.length > 0) {
|
|
649
|
+
this.expect("COMMA");
|
|
650
|
+
}
|
|
651
|
+
const expression = this.parseExpression();
|
|
652
|
+
let direction = "ASC"; // Default to ASC
|
|
653
|
+
if (this.checkKeyword("ASC")) {
|
|
654
|
+
this.advance();
|
|
655
|
+
}
|
|
656
|
+
else if (this.checkKeyword("DESC")) {
|
|
657
|
+
this.advance();
|
|
658
|
+
direction = "DESC";
|
|
659
|
+
}
|
|
660
|
+
orderBy.push({ expression, direction });
|
|
661
|
+
} while (this.check("COMMA"));
|
|
662
|
+
}
|
|
663
|
+
// Parse SKIP
|
|
664
|
+
let skip;
|
|
665
|
+
if (this.checkKeyword("SKIP")) {
|
|
666
|
+
this.advance();
|
|
667
|
+
const skipToken = this.expect("NUMBER");
|
|
668
|
+
skip = parseInt(skipToken.value, 10);
|
|
669
|
+
}
|
|
670
|
+
// Parse LIMIT
|
|
671
|
+
let limit;
|
|
672
|
+
if (this.checkKeyword("LIMIT")) {
|
|
673
|
+
this.advance();
|
|
674
|
+
const limitToken = this.expect("NUMBER");
|
|
675
|
+
limit = parseInt(limitToken.value, 10);
|
|
676
|
+
}
|
|
677
|
+
return { type: "RETURN", distinct, items, orderBy, skip, limit };
|
|
678
|
+
}
|
|
679
|
+
parseWith() {
|
|
680
|
+
this.expect("KEYWORD", "WITH");
|
|
681
|
+
// Check for DISTINCT after WITH
|
|
682
|
+
let distinct;
|
|
683
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
684
|
+
this.advance();
|
|
685
|
+
distinct = true;
|
|
686
|
+
}
|
|
687
|
+
const items = [];
|
|
688
|
+
let star = false;
|
|
689
|
+
// Check for WITH * syntax (pass through all variables)
|
|
690
|
+
if (this.check("STAR")) {
|
|
691
|
+
this.advance();
|
|
692
|
+
star = true;
|
|
693
|
+
// After *, we might have additional items with comma
|
|
694
|
+
// e.g., WITH *, count(n) AS cnt
|
|
695
|
+
// For now, we'll mark this with a special expression
|
|
696
|
+
items.push({ expression: { type: "variable", variable: "*" } });
|
|
697
|
+
}
|
|
698
|
+
if (!star || this.check("COMMA")) {
|
|
699
|
+
if (star) {
|
|
700
|
+
this.advance(); // consume comma after *
|
|
701
|
+
}
|
|
702
|
+
do {
|
|
703
|
+
if (items.length > (star ? 1 : 0)) {
|
|
704
|
+
this.expect("COMMA");
|
|
705
|
+
}
|
|
706
|
+
const expression = this.parseExpression();
|
|
707
|
+
let alias;
|
|
708
|
+
if (this.checkKeyword("AS")) {
|
|
709
|
+
this.advance();
|
|
710
|
+
alias = this.expectIdentifierOrKeyword();
|
|
711
|
+
}
|
|
712
|
+
items.push({ expression, alias });
|
|
713
|
+
} while (this.check("COMMA"));
|
|
714
|
+
}
|
|
715
|
+
// Parse ORDER BY
|
|
716
|
+
let orderBy;
|
|
717
|
+
if (this.checkKeyword("ORDER")) {
|
|
718
|
+
this.advance();
|
|
719
|
+
this.expect("KEYWORD", "BY");
|
|
720
|
+
orderBy = [];
|
|
721
|
+
do {
|
|
722
|
+
if (orderBy.length > 0) {
|
|
723
|
+
this.expect("COMMA");
|
|
724
|
+
}
|
|
725
|
+
const expression = this.parseExpression();
|
|
726
|
+
let direction = "ASC"; // Default to ASC
|
|
727
|
+
if (this.checkKeyword("ASC")) {
|
|
728
|
+
this.advance();
|
|
729
|
+
}
|
|
730
|
+
else if (this.checkKeyword("DESC")) {
|
|
731
|
+
this.advance();
|
|
732
|
+
direction = "DESC";
|
|
733
|
+
}
|
|
734
|
+
orderBy.push({ expression, direction });
|
|
735
|
+
} while (this.check("COMMA"));
|
|
736
|
+
}
|
|
737
|
+
// Parse SKIP
|
|
738
|
+
let skip;
|
|
739
|
+
if (this.checkKeyword("SKIP")) {
|
|
740
|
+
this.advance();
|
|
741
|
+
const skipToken = this.expect("NUMBER");
|
|
742
|
+
skip = parseInt(skipToken.value, 10);
|
|
743
|
+
}
|
|
744
|
+
// Parse LIMIT
|
|
745
|
+
let limit;
|
|
746
|
+
if (this.checkKeyword("LIMIT")) {
|
|
747
|
+
this.advance();
|
|
748
|
+
const limitToken = this.expect("NUMBER");
|
|
749
|
+
limit = parseInt(limitToken.value, 10);
|
|
750
|
+
}
|
|
751
|
+
// Parse optional WHERE clause after WITH items
|
|
752
|
+
let where;
|
|
753
|
+
if (this.checkKeyword("WHERE")) {
|
|
754
|
+
this.advance();
|
|
755
|
+
where = this.parseWhereCondition();
|
|
756
|
+
}
|
|
757
|
+
return { type: "WITH", distinct, items, orderBy, skip, limit, where };
|
|
758
|
+
}
|
|
759
|
+
parseUnwind() {
|
|
760
|
+
this.expect("KEYWORD", "UNWIND");
|
|
761
|
+
const expression = this.parseUnwindExpression();
|
|
762
|
+
this.expect("KEYWORD", "AS");
|
|
763
|
+
const alias = this.expectIdentifier();
|
|
764
|
+
return { type: "UNWIND", expression, alias };
|
|
765
|
+
}
|
|
766
|
+
parseUnwindExpression() {
|
|
767
|
+
const token = this.peek();
|
|
768
|
+
// NULL literal - UNWIND null produces empty result
|
|
769
|
+
if (token.type === "KEYWORD" && token.value.toUpperCase() === "NULL") {
|
|
770
|
+
this.advance();
|
|
771
|
+
return { type: "literal", value: null };
|
|
772
|
+
}
|
|
773
|
+
// Parenthesized expression like (first + second)
|
|
774
|
+
if (token.type === "LPAREN") {
|
|
775
|
+
this.advance();
|
|
776
|
+
const expr = this.parseExpression();
|
|
777
|
+
this.expect("RPAREN");
|
|
778
|
+
return expr;
|
|
779
|
+
}
|
|
780
|
+
// Array literal
|
|
781
|
+
if (token.type === "LBRACKET") {
|
|
782
|
+
const values = this.parseArray();
|
|
783
|
+
return { type: "literal", value: values };
|
|
784
|
+
}
|
|
785
|
+
// Parameter
|
|
786
|
+
if (token.type === "PARAMETER") {
|
|
787
|
+
this.advance();
|
|
788
|
+
return { type: "parameter", name: token.value };
|
|
789
|
+
}
|
|
790
|
+
// Function call like range(1, 10)
|
|
791
|
+
if ((token.type === "IDENTIFIER" || token.type === "KEYWORD") && this.tokens[this.pos + 1]?.type === "LPAREN") {
|
|
792
|
+
return this.parseExpression();
|
|
793
|
+
}
|
|
794
|
+
// Variable or property access
|
|
795
|
+
if (token.type === "IDENTIFIER") {
|
|
796
|
+
const variable = this.advance().value;
|
|
797
|
+
if (this.check("DOT")) {
|
|
798
|
+
this.advance();
|
|
799
|
+
const property = this.expectIdentifier();
|
|
800
|
+
return { type: "property", variable, property };
|
|
801
|
+
}
|
|
802
|
+
return { type: "variable", variable };
|
|
803
|
+
}
|
|
804
|
+
throw new Error(`Expected array, parameter, or variable in UNWIND, got ${token.type} '${token.value}'`);
|
|
805
|
+
}
|
|
806
|
+
parseCall() {
|
|
807
|
+
this.expect("KEYWORD", "CALL");
|
|
808
|
+
// Parse procedure name (e.g., "db.labels" or "db.relationshipTypes")
|
|
809
|
+
// Procedure names can have dots, so we parse identifier.identifier...
|
|
810
|
+
let procedureName = this.expectIdentifier();
|
|
811
|
+
while (this.check("DOT")) {
|
|
812
|
+
this.advance();
|
|
813
|
+
procedureName += "." + this.expectIdentifier();
|
|
814
|
+
}
|
|
815
|
+
// Parse arguments in parentheses
|
|
816
|
+
this.expect("LPAREN");
|
|
817
|
+
const args = [];
|
|
818
|
+
if (!this.check("RPAREN")) {
|
|
819
|
+
do {
|
|
820
|
+
if (args.length > 0) {
|
|
821
|
+
this.expect("COMMA");
|
|
822
|
+
}
|
|
823
|
+
args.push(this.parseExpression());
|
|
824
|
+
} while (this.check("COMMA"));
|
|
825
|
+
}
|
|
826
|
+
this.expect("RPAREN");
|
|
827
|
+
// Parse optional YIELD clause
|
|
828
|
+
let yields;
|
|
829
|
+
let where;
|
|
830
|
+
if (this.checkKeyword("YIELD")) {
|
|
831
|
+
this.advance();
|
|
832
|
+
yields = [];
|
|
833
|
+
// Parse yielded field names (can be identifiers or keywords like 'count')
|
|
834
|
+
do {
|
|
835
|
+
if (yields.length > 0) {
|
|
836
|
+
this.expect("COMMA");
|
|
837
|
+
}
|
|
838
|
+
yields.push(this.expectIdentifierOrKeyword());
|
|
839
|
+
} while (this.check("COMMA"));
|
|
840
|
+
// Parse optional WHERE after YIELD
|
|
841
|
+
if (this.checkKeyword("WHERE")) {
|
|
842
|
+
this.advance();
|
|
843
|
+
where = this.parseWhereCondition();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return { type: "CALL", procedure: procedureName, args, yields, where };
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Parse a pattern, which can be a single node or a chain of relationships.
|
|
850
|
+
* For chained patterns like (a)-[:R1]->(b)-[:R2]->(c), this returns multiple
|
|
851
|
+
* RelationshipPattern objects via parsePatternChain.
|
|
852
|
+
*/
|
|
853
|
+
parsePattern() {
|
|
854
|
+
const firstNode = this.parseNodePattern();
|
|
855
|
+
// Check for relationship
|
|
856
|
+
if (this.check("DASH") || this.check("ARROW_LEFT")) {
|
|
857
|
+
const edge = this.parseEdgePattern();
|
|
858
|
+
const targetNode = this.parseNodePattern();
|
|
859
|
+
return {
|
|
860
|
+
source: firstNode,
|
|
861
|
+
edge,
|
|
862
|
+
target: targetNode,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
return firstNode;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Parse a pattern chain, returning an array of patterns.
|
|
869
|
+
* Handles multi-hop patterns like (a)-[:R1]->(b)-[:R2]->(c).
|
|
870
|
+
*/
|
|
871
|
+
parsePatternChain() {
|
|
872
|
+
const patterns = [];
|
|
873
|
+
const firstNode = this.parseNodePattern();
|
|
874
|
+
// Check for relationship chain
|
|
875
|
+
if (!this.check("DASH") && !this.check("ARROW_LEFT")) {
|
|
876
|
+
// Just a single node
|
|
877
|
+
return [firstNode];
|
|
878
|
+
}
|
|
879
|
+
// Parse first relationship
|
|
880
|
+
let currentSource = firstNode;
|
|
881
|
+
while (this.check("DASH") || this.check("ARROW_LEFT")) {
|
|
882
|
+
const edge = this.parseEdgePattern();
|
|
883
|
+
const targetNode = this.parseNodePattern();
|
|
884
|
+
// Check if there's another relationship pattern coming after this one
|
|
885
|
+
const hasMoreRelationships = this.check("DASH") || this.check("ARROW_LEFT");
|
|
886
|
+
// If target is anonymous (no variable) AND there's more patterns coming,
|
|
887
|
+
// assign a synthetic variable for chaining.
|
|
888
|
+
// This ensures patterns like (:A)<-[:R]-(:B)-[:S]->(:C) share the (:B) node
|
|
889
|
+
// But don't do this for standalone patterns like CREATE ()-[:R]->()
|
|
890
|
+
if (!targetNode.variable && hasMoreRelationships) {
|
|
891
|
+
targetNode.variable = `_anon${this.anonVarCounter++}`;
|
|
892
|
+
}
|
|
893
|
+
patterns.push({
|
|
894
|
+
source: currentSource,
|
|
895
|
+
edge,
|
|
896
|
+
target: targetNode,
|
|
897
|
+
});
|
|
898
|
+
// For the next hop, the source is a reference to the previous target (variable only, no label)
|
|
899
|
+
currentSource = { variable: targetNode.variable };
|
|
900
|
+
}
|
|
901
|
+
return patterns;
|
|
902
|
+
}
|
|
903
|
+
parseNodePattern() {
|
|
904
|
+
this.expect("LPAREN");
|
|
905
|
+
const pattern = {};
|
|
906
|
+
// Variable name
|
|
907
|
+
if (this.check("IDENTIFIER")) {
|
|
908
|
+
pattern.variable = this.advance().value;
|
|
909
|
+
}
|
|
910
|
+
// Labels (can be multiple: :A:B:C)
|
|
911
|
+
if (this.check("COLON")) {
|
|
912
|
+
const labels = [];
|
|
913
|
+
while (this.check("COLON")) {
|
|
914
|
+
this.advance(); // consume ":"
|
|
915
|
+
labels.push(this.expectLabelOrType());
|
|
916
|
+
}
|
|
917
|
+
// Store as array if multiple labels, string if single (for backward compatibility)
|
|
918
|
+
pattern.label = labels.length === 1 ? labels[0] : labels;
|
|
919
|
+
}
|
|
920
|
+
// Properties
|
|
921
|
+
if (this.check("LBRACE")) {
|
|
922
|
+
pattern.properties = this.parseProperties();
|
|
923
|
+
}
|
|
924
|
+
this.expect("RPAREN");
|
|
925
|
+
return pattern;
|
|
926
|
+
}
|
|
927
|
+
parseEdgePattern() {
|
|
928
|
+
let direction = "none";
|
|
929
|
+
// Left arrow or dash
|
|
930
|
+
if (this.check("ARROW_LEFT")) {
|
|
931
|
+
this.advance();
|
|
932
|
+
direction = "left";
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
this.expect("DASH");
|
|
936
|
+
}
|
|
937
|
+
const edge = { direction };
|
|
938
|
+
// Edge details in brackets
|
|
939
|
+
if (this.check("LBRACKET")) {
|
|
940
|
+
this.advance();
|
|
941
|
+
// Variable name
|
|
942
|
+
if (this.check("IDENTIFIER")) {
|
|
943
|
+
edge.variable = this.advance().value;
|
|
944
|
+
}
|
|
945
|
+
// Type (can be identifier or keyword, or multiple types separated by |)
|
|
946
|
+
if (this.check("COLON")) {
|
|
947
|
+
this.advance();
|
|
948
|
+
const firstType = this.expectLabelOrType();
|
|
949
|
+
// Check for multiple types: [:TYPE1|TYPE2|TYPE3] or [:TYPE1|:TYPE2]
|
|
950
|
+
if (this.check("PIPE")) {
|
|
951
|
+
const types = [firstType];
|
|
952
|
+
while (this.check("PIPE")) {
|
|
953
|
+
this.advance();
|
|
954
|
+
// Some Cypher dialects allow :TYPE after the pipe, consume the optional colon
|
|
955
|
+
if (this.check("COLON")) {
|
|
956
|
+
this.advance();
|
|
957
|
+
}
|
|
958
|
+
types.push(this.expectLabelOrType());
|
|
959
|
+
}
|
|
960
|
+
edge.types = types;
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
edge.type = firstType;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// Variable-length pattern: *[min]..[max] or *n or *
|
|
967
|
+
if (this.check("STAR")) {
|
|
968
|
+
this.advance();
|
|
969
|
+
this.parseVariableLengthSpec(edge);
|
|
970
|
+
}
|
|
971
|
+
// Properties
|
|
972
|
+
if (this.check("LBRACE")) {
|
|
973
|
+
edge.properties = this.parseProperties();
|
|
974
|
+
}
|
|
975
|
+
this.expect("RBRACKET");
|
|
976
|
+
}
|
|
977
|
+
// Right arrow or dash
|
|
978
|
+
if (this.check("ARROW_RIGHT")) {
|
|
979
|
+
this.advance();
|
|
980
|
+
if (direction === "left") {
|
|
981
|
+
// <--> pattern means "either direction" (bidirectional), same as --
|
|
982
|
+
direction = "none";
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
direction = "right";
|
|
986
|
+
}
|
|
987
|
+
edge.direction = direction;
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
this.expect("DASH");
|
|
991
|
+
}
|
|
992
|
+
return edge;
|
|
993
|
+
}
|
|
994
|
+
parseVariableLengthSpec(edge) {
|
|
995
|
+
// Patterns:
|
|
996
|
+
// * -> min=1, max=undefined (any length >= 1)
|
|
997
|
+
// *2 -> min=2, max=2 (fixed length)
|
|
998
|
+
// *1..3 -> min=1, max=3 (range)
|
|
999
|
+
// *2.. -> min=2, max=undefined (min only)
|
|
1000
|
+
// *..3 -> min=1, max=3 (max only)
|
|
1001
|
+
// *0..3 -> min=0, max=3 (can include zero-length)
|
|
1002
|
+
// Check for just * with no numbers or dots
|
|
1003
|
+
if (!this.check("NUMBER") && !this.check("DOT")) {
|
|
1004
|
+
edge.minHops = 1;
|
|
1005
|
+
edge.maxHops = undefined;
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
// Check for ..N pattern (*..3) or just *.. (unbounded from 1)
|
|
1009
|
+
if (this.check("DOT")) {
|
|
1010
|
+
this.advance(); // first dot
|
|
1011
|
+
this.expect("DOT"); // second dot
|
|
1012
|
+
edge.minHops = 1;
|
|
1013
|
+
if (this.check("NUMBER")) {
|
|
1014
|
+
edge.maxHops = parseInt(this.advance().value, 10);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
edge.maxHops = undefined; // unbounded
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
// Parse first number
|
|
1022
|
+
const firstNum = parseInt(this.expect("NUMBER").value, 10);
|
|
1023
|
+
// Check if this is a range or fixed
|
|
1024
|
+
if (this.check("DOT")) {
|
|
1025
|
+
this.advance(); // first dot
|
|
1026
|
+
// Need to check if next is DOT or if dots were consecutive
|
|
1027
|
+
if (this.check("DOT")) {
|
|
1028
|
+
this.advance(); // second dot
|
|
1029
|
+
}
|
|
1030
|
+
// If we just advanced past a DOT and the tokenizer gave us separate dots,
|
|
1031
|
+
// we need to handle this. Let's check the current token
|
|
1032
|
+
edge.minHops = firstNum;
|
|
1033
|
+
// Check for second number
|
|
1034
|
+
if (this.check("NUMBER")) {
|
|
1035
|
+
edge.maxHops = parseInt(this.advance().value, 10);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
edge.maxHops = undefined; // unbounded
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
// Fixed length
|
|
1043
|
+
edge.minHops = firstNum;
|
|
1044
|
+
edge.maxHops = firstNum;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
parseProperties() {
|
|
1048
|
+
this.expect("LBRACE");
|
|
1049
|
+
const properties = {};
|
|
1050
|
+
if (!this.check("RBRACE")) {
|
|
1051
|
+
do {
|
|
1052
|
+
if (Object.keys(properties).length > 0) {
|
|
1053
|
+
this.expect("COMMA");
|
|
1054
|
+
}
|
|
1055
|
+
// Property keys can be identifiers OR keywords (like 'id', 'name', 'set', etc.)
|
|
1056
|
+
const key = this.expectIdentifierOrKeyword();
|
|
1057
|
+
this.expect("COLON");
|
|
1058
|
+
const value = this.parsePropertyValue();
|
|
1059
|
+
properties[key] = value;
|
|
1060
|
+
} while (this.check("COMMA"));
|
|
1061
|
+
}
|
|
1062
|
+
this.expect("RBRACE");
|
|
1063
|
+
return properties;
|
|
1064
|
+
}
|
|
1065
|
+
parsePropertyValue() {
|
|
1066
|
+
// Parse the primary property value first
|
|
1067
|
+
let left = this.parsePrimaryPropertyValue();
|
|
1068
|
+
// Check for binary operators: +, -, *, /, %
|
|
1069
|
+
while (this.check("PLUS") || this.check("DASH") || this.check("STAR") || this.check("SLASH") || this.check("PERCENT")) {
|
|
1070
|
+
const opToken = this.advance();
|
|
1071
|
+
let operator;
|
|
1072
|
+
if (opToken.type === "PLUS")
|
|
1073
|
+
operator = "+";
|
|
1074
|
+
else if (opToken.type === "DASH")
|
|
1075
|
+
operator = "-";
|
|
1076
|
+
else if (opToken.type === "STAR")
|
|
1077
|
+
operator = "*";
|
|
1078
|
+
else if (opToken.type === "SLASH")
|
|
1079
|
+
operator = "/";
|
|
1080
|
+
else
|
|
1081
|
+
operator = "%";
|
|
1082
|
+
const right = this.parsePrimaryPropertyValue();
|
|
1083
|
+
left = { type: "binary", operator, left, right };
|
|
1084
|
+
}
|
|
1085
|
+
return left;
|
|
1086
|
+
}
|
|
1087
|
+
parsePrimaryPropertyValue() {
|
|
1088
|
+
const token = this.peek();
|
|
1089
|
+
if (token.type === "STRING") {
|
|
1090
|
+
this.advance();
|
|
1091
|
+
return token.value;
|
|
1092
|
+
}
|
|
1093
|
+
if (token.type === "NUMBER") {
|
|
1094
|
+
this.advance();
|
|
1095
|
+
return parseFloat(token.value);
|
|
1096
|
+
}
|
|
1097
|
+
// Handle negative numbers: DASH followed by NUMBER
|
|
1098
|
+
if (token.type === "DASH") {
|
|
1099
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
1100
|
+
if (nextToken && nextToken.type === "NUMBER") {
|
|
1101
|
+
this.advance(); // consume DASH
|
|
1102
|
+
this.advance(); // consume NUMBER
|
|
1103
|
+
return -parseFloat(nextToken.value);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (token.type === "PARAMETER") {
|
|
1107
|
+
this.advance();
|
|
1108
|
+
return { type: "parameter", name: token.value };
|
|
1109
|
+
}
|
|
1110
|
+
if (token.type === "KEYWORD") {
|
|
1111
|
+
if (token.value === "TRUE") {
|
|
1112
|
+
this.advance();
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
if (token.value === "FALSE") {
|
|
1116
|
+
this.advance();
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
if (token.value === "NULL") {
|
|
1120
|
+
this.advance();
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (token.type === "LBRACKET") {
|
|
1125
|
+
return this.parseArray();
|
|
1126
|
+
}
|
|
1127
|
+
// Handle variable references (e.g., from UNWIND), property access (e.g., person.bornIn), or function calls (e.g., datetime())
|
|
1128
|
+
if (token.type === "IDENTIFIER") {
|
|
1129
|
+
this.advance();
|
|
1130
|
+
const varName = token.value;
|
|
1131
|
+
// Check for function call: identifier followed by LPAREN
|
|
1132
|
+
if (this.check("LPAREN")) {
|
|
1133
|
+
this.advance(); // consume LPAREN
|
|
1134
|
+
const args = [];
|
|
1135
|
+
// Parse function arguments
|
|
1136
|
+
if (!this.check("RPAREN")) {
|
|
1137
|
+
do {
|
|
1138
|
+
if (args.length > 0) {
|
|
1139
|
+
this.expect("COMMA");
|
|
1140
|
+
}
|
|
1141
|
+
args.push(this.parsePropertyValue());
|
|
1142
|
+
} while (this.check("COMMA"));
|
|
1143
|
+
}
|
|
1144
|
+
this.expect("RPAREN");
|
|
1145
|
+
return { type: "function", name: varName.toUpperCase(), args };
|
|
1146
|
+
}
|
|
1147
|
+
// Check for property access: variable.property
|
|
1148
|
+
if (this.check("DOT")) {
|
|
1149
|
+
this.advance(); // consume DOT
|
|
1150
|
+
const propToken = this.expect("IDENTIFIER");
|
|
1151
|
+
return { type: "property", variable: varName, property: propToken.value };
|
|
1152
|
+
}
|
|
1153
|
+
return { type: "variable", name: varName };
|
|
1154
|
+
}
|
|
1155
|
+
throw new Error(`Expected property value, got ${token.type} '${token.value}'`);
|
|
1156
|
+
}
|
|
1157
|
+
parseArray() {
|
|
1158
|
+
this.expect("LBRACKET");
|
|
1159
|
+
const values = [];
|
|
1160
|
+
if (!this.check("RBRACKET")) {
|
|
1161
|
+
do {
|
|
1162
|
+
if (values.length > 0) {
|
|
1163
|
+
this.expect("COMMA");
|
|
1164
|
+
}
|
|
1165
|
+
values.push(this.parsePropertyValue());
|
|
1166
|
+
} while (this.check("COMMA"));
|
|
1167
|
+
}
|
|
1168
|
+
this.expect("RBRACKET");
|
|
1169
|
+
return values;
|
|
1170
|
+
}
|
|
1171
|
+
parseWhereCondition() {
|
|
1172
|
+
return this.parseOrCondition();
|
|
1173
|
+
}
|
|
1174
|
+
parseOrCondition() {
|
|
1175
|
+
let left = this.parseAndCondition();
|
|
1176
|
+
while (this.checkKeyword("OR")) {
|
|
1177
|
+
this.advance();
|
|
1178
|
+
const right = this.parseAndCondition();
|
|
1179
|
+
left = { type: "or", conditions: [left, right] };
|
|
1180
|
+
}
|
|
1181
|
+
return left;
|
|
1182
|
+
}
|
|
1183
|
+
parseAndCondition() {
|
|
1184
|
+
let left = this.parseNotCondition();
|
|
1185
|
+
while (this.checkKeyword("AND")) {
|
|
1186
|
+
this.advance();
|
|
1187
|
+
const right = this.parseNotCondition();
|
|
1188
|
+
left = { type: "and", conditions: [left, right] };
|
|
1189
|
+
}
|
|
1190
|
+
return left;
|
|
1191
|
+
}
|
|
1192
|
+
parseNotCondition() {
|
|
1193
|
+
if (this.checkKeyword("NOT")) {
|
|
1194
|
+
this.advance();
|
|
1195
|
+
const condition = this.parseNotCondition();
|
|
1196
|
+
return { type: "not", condition };
|
|
1197
|
+
}
|
|
1198
|
+
return this.parsePrimaryCondition();
|
|
1199
|
+
}
|
|
1200
|
+
parsePrimaryCondition() {
|
|
1201
|
+
// Handle EXISTS pattern
|
|
1202
|
+
if (this.checkKeyword("EXISTS")) {
|
|
1203
|
+
return this.parseExistsCondition();
|
|
1204
|
+
}
|
|
1205
|
+
// Handle list predicates: ALL, ANY, NONE, SINGLE
|
|
1206
|
+
const listPredicates = ["ALL", "ANY", "NONE", "SINGLE"];
|
|
1207
|
+
if (this.peek().type === "KEYWORD" && listPredicates.includes(this.peek().value)) {
|
|
1208
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
1209
|
+
if (nextToken && nextToken.type === "LPAREN") {
|
|
1210
|
+
return this.parseListPredicateCondition();
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Handle parenthesized conditions
|
|
1214
|
+
if (this.check("LPAREN")) {
|
|
1215
|
+
this.advance(); // consume (
|
|
1216
|
+
const condition = this.parseOrCondition(); // parse the inner condition
|
|
1217
|
+
this.expect("RPAREN"); // consume )
|
|
1218
|
+
return condition;
|
|
1219
|
+
}
|
|
1220
|
+
return this.parseComparisonCondition();
|
|
1221
|
+
}
|
|
1222
|
+
parseListPredicateCondition() {
|
|
1223
|
+
// Parse list predicate as a condition (for use in WHERE clause)
|
|
1224
|
+
const predicateType = this.advance().value.toUpperCase();
|
|
1225
|
+
this.expect("LPAREN");
|
|
1226
|
+
// Expect variable followed by IN
|
|
1227
|
+
const variable = this.expectIdentifier();
|
|
1228
|
+
this.expect("KEYWORD", "IN");
|
|
1229
|
+
// Parse the source list expression
|
|
1230
|
+
const listExpr = this.parseExpression();
|
|
1231
|
+
// WHERE clause is required for list predicates
|
|
1232
|
+
if (!this.checkKeyword("WHERE")) {
|
|
1233
|
+
throw new Error(`Expected WHERE after list expression in ${predicateType}()`);
|
|
1234
|
+
}
|
|
1235
|
+
this.advance(); // consume WHERE
|
|
1236
|
+
// Parse the filter condition
|
|
1237
|
+
const filterCondition = this.parseListComprehensionCondition(variable);
|
|
1238
|
+
this.expect("RPAREN");
|
|
1239
|
+
return {
|
|
1240
|
+
type: "listPredicate",
|
|
1241
|
+
predicateType,
|
|
1242
|
+
variable,
|
|
1243
|
+
listExpr,
|
|
1244
|
+
filterCondition,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
parseExistsCondition() {
|
|
1248
|
+
this.expect("KEYWORD", "EXISTS");
|
|
1249
|
+
this.expect("LPAREN"); // outer (
|
|
1250
|
+
// Parse the pattern inside EXISTS((pattern))
|
|
1251
|
+
const patterns = this.parsePatternChain();
|
|
1252
|
+
const pattern = patterns.length === 1 ? patterns[0] : patterns[0]; // Use first pattern for now
|
|
1253
|
+
this.expect("RPAREN"); // outer )
|
|
1254
|
+
return { type: "exists", pattern };
|
|
1255
|
+
}
|
|
1256
|
+
parseComparisonCondition() {
|
|
1257
|
+
const left = this.parseExpression();
|
|
1258
|
+
// Check for IS NULL / IS NOT NULL
|
|
1259
|
+
if (this.checkKeyword("IS")) {
|
|
1260
|
+
this.advance();
|
|
1261
|
+
if (this.checkKeyword("NOT")) {
|
|
1262
|
+
this.advance();
|
|
1263
|
+
this.expect("KEYWORD", "NULL");
|
|
1264
|
+
return { type: "isNotNull", left };
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
this.expect("KEYWORD", "NULL");
|
|
1268
|
+
return { type: "isNull", left };
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Check for string operations
|
|
1272
|
+
if (this.checkKeyword("CONTAINS")) {
|
|
1273
|
+
this.advance();
|
|
1274
|
+
const right = this.parseExpression();
|
|
1275
|
+
return { type: "contains", left, right };
|
|
1276
|
+
}
|
|
1277
|
+
if (this.checkKeyword("STARTS")) {
|
|
1278
|
+
this.advance();
|
|
1279
|
+
this.expect("KEYWORD", "WITH");
|
|
1280
|
+
const right = this.parseExpression();
|
|
1281
|
+
return { type: "startsWith", left, right };
|
|
1282
|
+
}
|
|
1283
|
+
if (this.checkKeyword("ENDS")) {
|
|
1284
|
+
this.advance();
|
|
1285
|
+
this.expect("KEYWORD", "WITH");
|
|
1286
|
+
const right = this.parseExpression();
|
|
1287
|
+
return { type: "endsWith", left, right };
|
|
1288
|
+
}
|
|
1289
|
+
// Check for IN operator
|
|
1290
|
+
if (this.checkKeyword("IN")) {
|
|
1291
|
+
this.advance();
|
|
1292
|
+
// IN can be followed by a list literal [...] or a parameter $param
|
|
1293
|
+
const listExpr = this.parseInListExpression();
|
|
1294
|
+
return { type: "in", left, list: listExpr };
|
|
1295
|
+
}
|
|
1296
|
+
// Comparison operators
|
|
1297
|
+
const opToken = this.peek();
|
|
1298
|
+
let operator;
|
|
1299
|
+
if (opToken.type === "EQUALS")
|
|
1300
|
+
operator = "=";
|
|
1301
|
+
else if (opToken.type === "NOT_EQUALS")
|
|
1302
|
+
operator = "<>";
|
|
1303
|
+
else if (opToken.type === "LT")
|
|
1304
|
+
operator = "<";
|
|
1305
|
+
else if (opToken.type === "GT")
|
|
1306
|
+
operator = ">";
|
|
1307
|
+
else if (opToken.type === "LTE")
|
|
1308
|
+
operator = "<=";
|
|
1309
|
+
else if (opToken.type === "GTE")
|
|
1310
|
+
operator = ">=";
|
|
1311
|
+
if (operator) {
|
|
1312
|
+
this.advance();
|
|
1313
|
+
const right = this.parseExpression();
|
|
1314
|
+
return { type: "comparison", left, right, operator };
|
|
1315
|
+
}
|
|
1316
|
+
throw new Error(`Expected comparison operator, got ${opToken.type}`);
|
|
1317
|
+
}
|
|
1318
|
+
parseInListExpression() {
|
|
1319
|
+
const token = this.peek();
|
|
1320
|
+
// Array literal [...]
|
|
1321
|
+
if (token.type === "LBRACKET") {
|
|
1322
|
+
const values = this.parseArray();
|
|
1323
|
+
return { type: "literal", value: values };
|
|
1324
|
+
}
|
|
1325
|
+
// Parameter $param
|
|
1326
|
+
if (token.type === "PARAMETER") {
|
|
1327
|
+
this.advance();
|
|
1328
|
+
return { type: "parameter", name: token.value };
|
|
1329
|
+
}
|
|
1330
|
+
// Variable reference
|
|
1331
|
+
if (token.type === "IDENTIFIER") {
|
|
1332
|
+
const variable = this.advance().value;
|
|
1333
|
+
if (this.check("DOT")) {
|
|
1334
|
+
this.advance();
|
|
1335
|
+
const property = this.expectIdentifier();
|
|
1336
|
+
return { type: "property", variable, property };
|
|
1337
|
+
}
|
|
1338
|
+
return { type: "variable", variable };
|
|
1339
|
+
}
|
|
1340
|
+
throw new Error(`Expected array, parameter, or variable in IN clause, got ${token.type} '${token.value}'`);
|
|
1341
|
+
}
|
|
1342
|
+
parseExpression() {
|
|
1343
|
+
return this.parseAdditiveExpression();
|
|
1344
|
+
}
|
|
1345
|
+
// Parse expression that may include comparison and logical operators (for RETURN items)
|
|
1346
|
+
parseReturnExpression() {
|
|
1347
|
+
return this.parseOrExpression();
|
|
1348
|
+
}
|
|
1349
|
+
// Handle OR (lowest precedence for logical operators)
|
|
1350
|
+
parseOrExpression() {
|
|
1351
|
+
let left = this.parseAndExpression();
|
|
1352
|
+
while (this.checkKeyword("OR")) {
|
|
1353
|
+
this.advance();
|
|
1354
|
+
const right = this.parseAndExpression();
|
|
1355
|
+
left = { type: "binary", operator: "OR", left, right };
|
|
1356
|
+
}
|
|
1357
|
+
return left;
|
|
1358
|
+
}
|
|
1359
|
+
// Handle AND (higher precedence than OR)
|
|
1360
|
+
parseAndExpression() {
|
|
1361
|
+
let left = this.parseNotExpression();
|
|
1362
|
+
while (this.checkKeyword("AND")) {
|
|
1363
|
+
this.advance();
|
|
1364
|
+
const right = this.parseNotExpression();
|
|
1365
|
+
left = { type: "binary", operator: "AND", left, right };
|
|
1366
|
+
}
|
|
1367
|
+
return left;
|
|
1368
|
+
}
|
|
1369
|
+
// Handle NOT (highest precedence for logical operators)
|
|
1370
|
+
parseNotExpression() {
|
|
1371
|
+
if (this.checkKeyword("NOT")) {
|
|
1372
|
+
this.advance();
|
|
1373
|
+
const operand = this.parseNotExpression();
|
|
1374
|
+
return { type: "unary", operator: "NOT", operand };
|
|
1375
|
+
}
|
|
1376
|
+
return this.parseComparisonExpression();
|
|
1377
|
+
}
|
|
1378
|
+
// Handle comparison operators
|
|
1379
|
+
parseComparisonExpression() {
|
|
1380
|
+
let left = this.parseAdditiveExpression();
|
|
1381
|
+
// Check for IS NULL / IS NOT NULL
|
|
1382
|
+
if (this.checkKeyword("IS")) {
|
|
1383
|
+
this.advance();
|
|
1384
|
+
if (this.checkKeyword("NOT")) {
|
|
1385
|
+
this.advance();
|
|
1386
|
+
this.expect("KEYWORD", "NULL");
|
|
1387
|
+
return { type: "comparison", comparisonOperator: "IS NOT NULL", left };
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
this.expect("KEYWORD", "NULL");
|
|
1391
|
+
return { type: "comparison", comparisonOperator: "IS NULL", left };
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
// Check for comparison operators
|
|
1395
|
+
const opToken = this.peek();
|
|
1396
|
+
let comparisonOperator;
|
|
1397
|
+
if (opToken.type === "EQUALS")
|
|
1398
|
+
comparisonOperator = "=";
|
|
1399
|
+
else if (opToken.type === "NOT_EQUALS")
|
|
1400
|
+
comparisonOperator = "<>";
|
|
1401
|
+
else if (opToken.type === "LT")
|
|
1402
|
+
comparisonOperator = "<";
|
|
1403
|
+
else if (opToken.type === "GT")
|
|
1404
|
+
comparisonOperator = ">";
|
|
1405
|
+
else if (opToken.type === "LTE")
|
|
1406
|
+
comparisonOperator = "<=";
|
|
1407
|
+
else if (opToken.type === "GTE")
|
|
1408
|
+
comparisonOperator = ">=";
|
|
1409
|
+
if (comparisonOperator) {
|
|
1410
|
+
this.advance();
|
|
1411
|
+
const right = this.parseAdditiveExpression();
|
|
1412
|
+
return { type: "comparison", comparisonOperator, left, right };
|
|
1413
|
+
}
|
|
1414
|
+
return left;
|
|
1415
|
+
}
|
|
1416
|
+
// Handle + and - (lower precedence)
|
|
1417
|
+
parseAdditiveExpression() {
|
|
1418
|
+
let left = this.parseMultiplicativeExpression();
|
|
1419
|
+
while (this.check("PLUS") || this.check("DASH")) {
|
|
1420
|
+
const operatorToken = this.advance();
|
|
1421
|
+
const operator = operatorToken.type === "PLUS" ? "+" : "-";
|
|
1422
|
+
const right = this.parseMultiplicativeExpression();
|
|
1423
|
+
left = { type: "binary", operator: operator, left, right };
|
|
1424
|
+
}
|
|
1425
|
+
return left;
|
|
1426
|
+
}
|
|
1427
|
+
// Handle *, /, % (higher precedence than +, -)
|
|
1428
|
+
parseMultiplicativeExpression() {
|
|
1429
|
+
let left = this.parseExponentialExpression();
|
|
1430
|
+
while (this.check("STAR") || this.check("SLASH") || this.check("PERCENT")) {
|
|
1431
|
+
const operatorToken = this.advance();
|
|
1432
|
+
let operator;
|
|
1433
|
+
if (operatorToken.type === "STAR")
|
|
1434
|
+
operator = "*";
|
|
1435
|
+
else if (operatorToken.type === "SLASH")
|
|
1436
|
+
operator = "/";
|
|
1437
|
+
else
|
|
1438
|
+
operator = "%";
|
|
1439
|
+
const right = this.parseExponentialExpression();
|
|
1440
|
+
left = { type: "binary", operator, left, right };
|
|
1441
|
+
}
|
|
1442
|
+
return left;
|
|
1443
|
+
}
|
|
1444
|
+
// Handle ^ (exponentiation - highest precedence among arithmetic operators)
|
|
1445
|
+
parseExponentialExpression() {
|
|
1446
|
+
let left = this.parsePostfixExpression();
|
|
1447
|
+
while (this.check("CARET")) {
|
|
1448
|
+
this.advance(); // consume ^
|
|
1449
|
+
const right = this.parsePostfixExpression();
|
|
1450
|
+
left = { type: "binary", operator: "^", left, right };
|
|
1451
|
+
}
|
|
1452
|
+
return left;
|
|
1453
|
+
}
|
|
1454
|
+
// Handle postfix operations: list indexing like expr[0] or expr[1..3], and chained property access like a.b.c
|
|
1455
|
+
parsePostfixExpression() {
|
|
1456
|
+
let expr = this.parsePrimaryExpression();
|
|
1457
|
+
// Handle list/map indexing: expr[index] or expr[start..end], and chained property access: expr.prop
|
|
1458
|
+
while (this.check("LBRACKET") || this.check("DOT")) {
|
|
1459
|
+
if (this.check("DOT")) {
|
|
1460
|
+
this.advance(); // consume .
|
|
1461
|
+
// Property access - property names can be keywords too
|
|
1462
|
+
const property = this.expectIdentifierOrKeyword();
|
|
1463
|
+
// Convert to propertyAccess expression for chained access
|
|
1464
|
+
expr = { type: "propertyAccess", object: expr, property };
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
// LBRACKET
|
|
1468
|
+
this.advance(); // consume [
|
|
1469
|
+
// Check for slice syntax [start..end]
|
|
1470
|
+
if (this.check("DOT")) {
|
|
1471
|
+
// [..end] - from start
|
|
1472
|
+
this.advance(); // consume first .
|
|
1473
|
+
this.expect("DOT"); // consume second .
|
|
1474
|
+
const endExpr = this.parseExpression();
|
|
1475
|
+
this.expect("RBRACKET");
|
|
1476
|
+
expr = { type: "function", functionName: "SLICE", args: [expr, { type: "literal", value: null }, endExpr] };
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
const indexExpr = this.parseExpression();
|
|
1480
|
+
if (this.check("DOT")) {
|
|
1481
|
+
// Check for slice: [start..end]
|
|
1482
|
+
this.advance(); // consume first .
|
|
1483
|
+
if (this.check("DOT")) {
|
|
1484
|
+
this.advance(); // consume second .
|
|
1485
|
+
if (this.check("RBRACKET")) {
|
|
1486
|
+
// [start..] - to end
|
|
1487
|
+
this.expect("RBRACKET");
|
|
1488
|
+
expr = { type: "function", functionName: "SLICE", args: [expr, indexExpr, { type: "literal", value: null }] };
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
const endExpr = this.parseExpression();
|
|
1492
|
+
this.expect("RBRACKET");
|
|
1493
|
+
expr = { type: "function", functionName: "SLICE", args: [expr, indexExpr, endExpr] };
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
else {
|
|
1497
|
+
throw new Error("Expected '..' for slice syntax");
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
// Simple index: [index]
|
|
1502
|
+
this.expect("RBRACKET");
|
|
1503
|
+
expr = { type: "function", functionName: "INDEX", args: [expr, indexExpr] };
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return expr;
|
|
1509
|
+
}
|
|
1510
|
+
// Parse primary expressions (atoms)
|
|
1511
|
+
parsePrimaryExpression() {
|
|
1512
|
+
const token = this.peek();
|
|
1513
|
+
// List literal [1, 2, 3]
|
|
1514
|
+
if (token.type === "LBRACKET") {
|
|
1515
|
+
return this.parseListLiteralExpression();
|
|
1516
|
+
}
|
|
1517
|
+
// Object literal { key: value, ... }
|
|
1518
|
+
if (token.type === "LBRACE") {
|
|
1519
|
+
return this.parseObjectLiteral();
|
|
1520
|
+
}
|
|
1521
|
+
// Parenthesized expression for grouping or label predicate (n:Label)
|
|
1522
|
+
if (token.type === "LPAREN") {
|
|
1523
|
+
// Check for label predicate: (n:Label) or (n:Label1:Label2)
|
|
1524
|
+
// Look ahead: ( IDENTIFIER COLON ...
|
|
1525
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
1526
|
+
const afterNext = this.tokens[this.pos + 2];
|
|
1527
|
+
if (nextToken?.type === "IDENTIFIER" && afterNext?.type === "COLON") {
|
|
1528
|
+
this.advance(); // consume (
|
|
1529
|
+
const variable = this.advance().value; // consume identifier
|
|
1530
|
+
// Parse one or more labels
|
|
1531
|
+
const labelsList = [];
|
|
1532
|
+
while (this.check("COLON")) {
|
|
1533
|
+
this.advance(); // consume :
|
|
1534
|
+
labelsList.push(this.expectLabelOrType());
|
|
1535
|
+
}
|
|
1536
|
+
this.expect("RPAREN");
|
|
1537
|
+
if (labelsList.length === 1) {
|
|
1538
|
+
return { type: "labelPredicate", variable, label: labelsList[0] };
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
return { type: "labelPredicate", variable, labels: labelsList };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// Regular parenthesized expression - use full expression parsing including AND/OR
|
|
1545
|
+
this.advance(); // consume (
|
|
1546
|
+
const expr = this.parseOrExpression();
|
|
1547
|
+
this.expect("RPAREN");
|
|
1548
|
+
return expr;
|
|
1549
|
+
}
|
|
1550
|
+
// CASE expression
|
|
1551
|
+
if (this.checkKeyword("CASE")) {
|
|
1552
|
+
return this.parseCaseExpression();
|
|
1553
|
+
}
|
|
1554
|
+
// Function call: COUNT(x), id(x), count(DISTINCT x), COUNT(*)
|
|
1555
|
+
// Also handles list predicates: ALL(x IN list WHERE cond), ANY(...), NONE(...), SINGLE(...)
|
|
1556
|
+
if (token.type === "KEYWORD" || token.type === "IDENTIFIER") {
|
|
1557
|
+
const nextToken = this.tokens[this.pos + 1];
|
|
1558
|
+
if (nextToken && nextToken.type === "LPAREN") {
|
|
1559
|
+
const functionName = this.advance().value.toUpperCase();
|
|
1560
|
+
this.advance(); // LPAREN
|
|
1561
|
+
// Check if this is a list predicate: ALL, ANY, NONE, SINGLE
|
|
1562
|
+
const listPredicates = ["ALL", "ANY", "NONE", "SINGLE"];
|
|
1563
|
+
if (listPredicates.includes(functionName)) {
|
|
1564
|
+
// Check for list predicate syntax: PRED(var IN list WHERE cond)
|
|
1565
|
+
// Lookahead to see if next is identifier followed by IN
|
|
1566
|
+
if (this.check("IDENTIFIER")) {
|
|
1567
|
+
const savedPos = this.pos;
|
|
1568
|
+
const varToken = this.advance();
|
|
1569
|
+
if (this.checkKeyword("IN")) {
|
|
1570
|
+
// This is a list predicate
|
|
1571
|
+
this.advance(); // consume IN
|
|
1572
|
+
return this.parseListPredicate(functionName, varToken.value);
|
|
1573
|
+
}
|
|
1574
|
+
else {
|
|
1575
|
+
// Not a list predicate syntax, backtrack
|
|
1576
|
+
this.pos = savedPos;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
const args = [];
|
|
1581
|
+
// Check for DISTINCT keyword after opening paren (for aggregation functions)
|
|
1582
|
+
let distinct;
|
|
1583
|
+
if (this.checkKeyword("DISTINCT")) {
|
|
1584
|
+
this.advance();
|
|
1585
|
+
distinct = true;
|
|
1586
|
+
}
|
|
1587
|
+
// Special case: COUNT(*) - handle STAR token as "count all"
|
|
1588
|
+
if (this.check("STAR")) {
|
|
1589
|
+
this.advance(); // consume STAR
|
|
1590
|
+
// COUNT(*) has no arguments - the * means "count all rows"
|
|
1591
|
+
this.expect("RPAREN");
|
|
1592
|
+
return { type: "function", functionName, args: [], distinct };
|
|
1593
|
+
}
|
|
1594
|
+
if (!this.check("RPAREN")) {
|
|
1595
|
+
do {
|
|
1596
|
+
if (args.length > 0) {
|
|
1597
|
+
this.expect("COMMA");
|
|
1598
|
+
}
|
|
1599
|
+
args.push(this.parseExpression());
|
|
1600
|
+
} while (this.check("COMMA"));
|
|
1601
|
+
}
|
|
1602
|
+
this.expect("RPAREN");
|
|
1603
|
+
return { type: "function", functionName, args, distinct };
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
// Parameter
|
|
1607
|
+
if (token.type === "PARAMETER") {
|
|
1608
|
+
this.advance();
|
|
1609
|
+
return { type: "parameter", name: token.value };
|
|
1610
|
+
}
|
|
1611
|
+
// Unary minus for negative numbers
|
|
1612
|
+
if (token.type === "DASH") {
|
|
1613
|
+
this.advance(); // consume the dash
|
|
1614
|
+
const nextToken = this.peek();
|
|
1615
|
+
if (nextToken.type === "NUMBER") {
|
|
1616
|
+
this.advance();
|
|
1617
|
+
return { type: "literal", value: -parseFloat(nextToken.value) };
|
|
1618
|
+
}
|
|
1619
|
+
// For more complex expressions, create a unary minus operation
|
|
1620
|
+
const operand = this.parsePrimaryExpression();
|
|
1621
|
+
return { type: "binary", operator: "-", left: { type: "literal", value: 0 }, right: operand };
|
|
1622
|
+
}
|
|
1623
|
+
// Literal values
|
|
1624
|
+
if (token.type === "STRING") {
|
|
1625
|
+
this.advance();
|
|
1626
|
+
return { type: "literal", value: token.value };
|
|
1627
|
+
}
|
|
1628
|
+
if (token.type === "NUMBER") {
|
|
1629
|
+
this.advance();
|
|
1630
|
+
return { type: "literal", value: parseFloat(token.value) };
|
|
1631
|
+
}
|
|
1632
|
+
if (token.type === "KEYWORD") {
|
|
1633
|
+
if (token.value === "TRUE") {
|
|
1634
|
+
this.advance();
|
|
1635
|
+
return { type: "literal", value: true };
|
|
1636
|
+
}
|
|
1637
|
+
if (token.value === "FALSE") {
|
|
1638
|
+
this.advance();
|
|
1639
|
+
return { type: "literal", value: false };
|
|
1640
|
+
}
|
|
1641
|
+
if (token.value === "NULL") {
|
|
1642
|
+
this.advance();
|
|
1643
|
+
return { type: "literal", value: null };
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
// Variable or property access
|
|
1647
|
+
// Allow keywords to be used as variable names when not in keyword position
|
|
1648
|
+
if (token.type === "IDENTIFIER" || (token.type === "KEYWORD" && !["TRUE", "FALSE", "NULL", "CASE"].includes(token.value))) {
|
|
1649
|
+
const tok = this.advance();
|
|
1650
|
+
// Use original casing for keywords used as identifiers
|
|
1651
|
+
const variable = tok.originalValue || tok.value;
|
|
1652
|
+
if (this.check("DOT")) {
|
|
1653
|
+
this.advance();
|
|
1654
|
+
// Property names can also be keywords (like 'count', 'order', etc.)
|
|
1655
|
+
const property = this.expectIdentifierOrKeyword();
|
|
1656
|
+
return { type: "property", variable, property };
|
|
1657
|
+
}
|
|
1658
|
+
return { type: "variable", variable };
|
|
1659
|
+
}
|
|
1660
|
+
throw new Error(`Expected expression, got ${token.type} '${token.value}'`);
|
|
1661
|
+
}
|
|
1662
|
+
parseCaseExpression() {
|
|
1663
|
+
this.expect("KEYWORD", "CASE");
|
|
1664
|
+
// Check for simple form: CASE expr WHEN val THEN ...
|
|
1665
|
+
// vs searched form: CASE WHEN condition THEN ...
|
|
1666
|
+
let caseExpr;
|
|
1667
|
+
// If the next token is not WHEN, it's a simple form with an expression
|
|
1668
|
+
if (!this.checkKeyword("WHEN")) {
|
|
1669
|
+
caseExpr = this.parseExpression();
|
|
1670
|
+
}
|
|
1671
|
+
const whens = [];
|
|
1672
|
+
// Parse WHEN ... THEN ... clauses
|
|
1673
|
+
while (this.checkKeyword("WHEN")) {
|
|
1674
|
+
this.advance(); // consume WHEN
|
|
1675
|
+
let condition;
|
|
1676
|
+
if (caseExpr) {
|
|
1677
|
+
// Simple form: CASE expr WHEN value THEN ...
|
|
1678
|
+
// The value is compared for equality with caseExpr
|
|
1679
|
+
const whenValue = this.parseExpression();
|
|
1680
|
+
// Create an equality comparison condition
|
|
1681
|
+
condition = {
|
|
1682
|
+
type: "comparison",
|
|
1683
|
+
left: caseExpr,
|
|
1684
|
+
right: whenValue,
|
|
1685
|
+
operator: "="
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
// Searched form: CASE WHEN condition THEN ...
|
|
1690
|
+
condition = this.parseWhereCondition();
|
|
1691
|
+
}
|
|
1692
|
+
this.expect("KEYWORD", "THEN");
|
|
1693
|
+
const result = this.parseExpression();
|
|
1694
|
+
whens.push({ condition, result });
|
|
1695
|
+
}
|
|
1696
|
+
// Parse optional ELSE
|
|
1697
|
+
let elseExpr;
|
|
1698
|
+
if (this.checkKeyword("ELSE")) {
|
|
1699
|
+
this.advance();
|
|
1700
|
+
elseExpr = this.parseExpression();
|
|
1701
|
+
}
|
|
1702
|
+
this.expect("KEYWORD", "END");
|
|
1703
|
+
return {
|
|
1704
|
+
type: "case",
|
|
1705
|
+
expression: caseExpr,
|
|
1706
|
+
whens,
|
|
1707
|
+
elseExpr,
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
parseObjectLiteral() {
|
|
1711
|
+
this.expect("LBRACE");
|
|
1712
|
+
const properties = [];
|
|
1713
|
+
if (!this.check("RBRACE")) {
|
|
1714
|
+
do {
|
|
1715
|
+
if (properties.length > 0) {
|
|
1716
|
+
this.expect("COMMA");
|
|
1717
|
+
}
|
|
1718
|
+
// Property keys can be identifiers or keywords
|
|
1719
|
+
const key = this.expectIdentifierOrKeyword();
|
|
1720
|
+
this.expect("COLON");
|
|
1721
|
+
// Use parseReturnExpression to support comparisons like {foo: a.name='Andres'}
|
|
1722
|
+
const value = this.parseReturnExpression();
|
|
1723
|
+
properties.push({ key, value });
|
|
1724
|
+
} while (this.check("COMMA"));
|
|
1725
|
+
}
|
|
1726
|
+
this.expect("RBRACE");
|
|
1727
|
+
return { type: "object", properties };
|
|
1728
|
+
}
|
|
1729
|
+
parseListLiteralExpression() {
|
|
1730
|
+
this.expect("LBRACKET");
|
|
1731
|
+
// Check for list comprehension: [x IN list WHERE cond | expr]
|
|
1732
|
+
// We need to look ahead to see if this is a list comprehension
|
|
1733
|
+
if (this.check("IDENTIFIER")) {
|
|
1734
|
+
const savedPos = this.pos;
|
|
1735
|
+
const identifier = this.advance().value;
|
|
1736
|
+
if (this.checkKeyword("IN")) {
|
|
1737
|
+
// This is a list comprehension
|
|
1738
|
+
this.advance(); // consume "IN"
|
|
1739
|
+
return this.parseListComprehension(identifier);
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
// Not a list comprehension, backtrack
|
|
1743
|
+
this.pos = savedPos;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
// Regular list literal - elements can be full expressions (including objects)
|
|
1747
|
+
const elements = [];
|
|
1748
|
+
if (!this.check("RBRACKET")) {
|
|
1749
|
+
do {
|
|
1750
|
+
if (elements.length > 0) {
|
|
1751
|
+
this.expect("COMMA");
|
|
1752
|
+
}
|
|
1753
|
+
elements.push(this.parseExpression());
|
|
1754
|
+
} while (this.check("COMMA"));
|
|
1755
|
+
}
|
|
1756
|
+
this.expect("RBRACKET");
|
|
1757
|
+
// If all elements are literals, return as literal list
|
|
1758
|
+
// Otherwise wrap in a function-like expression for arrays of expressions
|
|
1759
|
+
const allLiterals = elements.every(e => e.type === "literal");
|
|
1760
|
+
if (allLiterals) {
|
|
1761
|
+
return { type: "literal", value: elements.map(e => e.value) };
|
|
1762
|
+
}
|
|
1763
|
+
// For lists containing expressions, use a special function type
|
|
1764
|
+
return { type: "function", functionName: "LIST", args: elements };
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Parse a list comprehension after [variable IN has been consumed.
|
|
1768
|
+
* Full syntax: [variable IN listExpr WHERE filterCondition | mapExpr]
|
|
1769
|
+
* - WHERE and | are both optional
|
|
1770
|
+
*/
|
|
1771
|
+
parseListComprehension(variable) {
|
|
1772
|
+
// Parse the source list expression
|
|
1773
|
+
const listExpr = this.parseExpression();
|
|
1774
|
+
// Check for optional WHERE filter
|
|
1775
|
+
let filterCondition;
|
|
1776
|
+
if (this.checkKeyword("WHERE")) {
|
|
1777
|
+
this.advance();
|
|
1778
|
+
filterCondition = this.parseListComprehensionCondition(variable);
|
|
1779
|
+
}
|
|
1780
|
+
// Check for optional map projection (| expr)
|
|
1781
|
+
let mapExpr;
|
|
1782
|
+
if (this.check("PIPE")) {
|
|
1783
|
+
this.advance();
|
|
1784
|
+
mapExpr = this.parseListComprehensionExpression(variable);
|
|
1785
|
+
}
|
|
1786
|
+
this.expect("RBRACKET");
|
|
1787
|
+
return {
|
|
1788
|
+
type: "listComprehension",
|
|
1789
|
+
variable,
|
|
1790
|
+
listExpr,
|
|
1791
|
+
filterCondition,
|
|
1792
|
+
mapExpr,
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Parse a list predicate after PRED(variable IN has been consumed.
|
|
1797
|
+
* Syntax: ALL/ANY/NONE/SINGLE(variable IN listExpr WHERE filterCondition)
|
|
1798
|
+
* WHERE is required for list predicates.
|
|
1799
|
+
*/
|
|
1800
|
+
parseListPredicate(predicateType, variable) {
|
|
1801
|
+
// Parse the source list expression
|
|
1802
|
+
const listExpr = this.parseExpression();
|
|
1803
|
+
// WHERE clause is required for list predicates
|
|
1804
|
+
if (!this.checkKeyword("WHERE")) {
|
|
1805
|
+
throw new Error(`Expected WHERE after list expression in ${predicateType}()`);
|
|
1806
|
+
}
|
|
1807
|
+
this.advance(); // consume WHERE
|
|
1808
|
+
// Parse the filter condition
|
|
1809
|
+
const filterCondition = this.parseListComprehensionCondition(variable);
|
|
1810
|
+
this.expect("RPAREN");
|
|
1811
|
+
return {
|
|
1812
|
+
type: "listPredicate",
|
|
1813
|
+
predicateType,
|
|
1814
|
+
variable,
|
|
1815
|
+
listExpr,
|
|
1816
|
+
filterCondition,
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Parse a condition in a list comprehension, where the variable can be used.
|
|
1821
|
+
* Similar to parseWhereCondition but resolves variable references.
|
|
1822
|
+
*/
|
|
1823
|
+
parseListComprehensionCondition(variable) {
|
|
1824
|
+
return this.parseOrCondition();
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Parse an expression in a list comprehension map projection.
|
|
1828
|
+
* Similar to parseExpression but the variable is in scope.
|
|
1829
|
+
*/
|
|
1830
|
+
parseListComprehensionExpression(variable) {
|
|
1831
|
+
return this.parseExpression();
|
|
1832
|
+
}
|
|
1833
|
+
// Token helpers
|
|
1834
|
+
peek() {
|
|
1835
|
+
return this.tokens[this.pos];
|
|
1836
|
+
}
|
|
1837
|
+
advance() {
|
|
1838
|
+
if (!this.isAtEnd()) {
|
|
1839
|
+
this.pos++;
|
|
1840
|
+
}
|
|
1841
|
+
return this.tokens[this.pos - 1];
|
|
1842
|
+
}
|
|
1843
|
+
isAtEnd() {
|
|
1844
|
+
return this.peek().type === "EOF";
|
|
1845
|
+
}
|
|
1846
|
+
check(type) {
|
|
1847
|
+
return this.peek().type === type;
|
|
1848
|
+
}
|
|
1849
|
+
checkKeyword(keyword) {
|
|
1850
|
+
const token = this.peek();
|
|
1851
|
+
return token.type === "KEYWORD" && token.value === keyword;
|
|
1852
|
+
}
|
|
1853
|
+
expect(type, value) {
|
|
1854
|
+
const token = this.peek();
|
|
1855
|
+
if (token.type !== type || (value !== undefined && token.value !== value)) {
|
|
1856
|
+
throw new Error(`Expected ${type}${value ? ` '${value}'` : ""}, got ${token.type} '${token.value}'`);
|
|
1857
|
+
}
|
|
1858
|
+
return this.advance();
|
|
1859
|
+
}
|
|
1860
|
+
expectIdentifier() {
|
|
1861
|
+
const token = this.peek();
|
|
1862
|
+
if (token.type !== "IDENTIFIER") {
|
|
1863
|
+
throw new Error(`Expected identifier, got ${token.type} '${token.value}'`);
|
|
1864
|
+
}
|
|
1865
|
+
return this.advance().value;
|
|
1866
|
+
}
|
|
1867
|
+
expectIdentifierOrKeyword() {
|
|
1868
|
+
const token = this.peek();
|
|
1869
|
+
if (token.type !== "IDENTIFIER" && token.type !== "KEYWORD") {
|
|
1870
|
+
throw new Error(`Expected identifier or keyword, got ${token.type} '${token.value}'`);
|
|
1871
|
+
}
|
|
1872
|
+
// Keywords are stored uppercase, but property keys should be lowercase
|
|
1873
|
+
const value = this.advance().value;
|
|
1874
|
+
return token.type === "KEYWORD" ? value.toLowerCase() : value;
|
|
1875
|
+
}
|
|
1876
|
+
expectLabelOrType() {
|
|
1877
|
+
const token = this.peek();
|
|
1878
|
+
if (token.type !== "IDENTIFIER" && token.type !== "KEYWORD") {
|
|
1879
|
+
throw new Error(`Expected label or type, got ${token.type} '${token.value}'`);
|
|
1880
|
+
}
|
|
1881
|
+
this.advance();
|
|
1882
|
+
// Labels and types preserve their original case from the query
|
|
1883
|
+
// Use originalValue for keywords (which stores the original casing before uppercasing)
|
|
1884
|
+
return token.originalValue || token.value;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
// Convenience function
|
|
1888
|
+
export function parse(input) {
|
|
1889
|
+
return new Parser().parse(input);
|
|
1890
|
+
}
|
|
1891
|
+
//# sourceMappingURL=parser.js.map
|