gcyphrq 0.12.6

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/dist/lib.js ADDED
@@ -0,0 +1,1455 @@
1
+ // src/engine/cypher-parser.ts
2
+ import { createRequire } from "module";
3
+ var _require = createRequire(import.meta.url);
4
+ var antlr4 = _require("antlr4").default;
5
+ var { CypherLexer, CypherParser } = _require("@neo4j-cypher/antlr4");
6
+ var Ctx = {
7
+ Atom: "AtomContext",
8
+ AddOrSubtractExpression: "AddOrSubtractExpressionContext",
9
+ BooleanLiteral: "BooleanLiteralContext",
10
+ AnonymousPatternPart: "AnonymousPatternPartContext",
11
+ Clause: "ClauseContext",
12
+ ComparisonExpression: "ComparisonExpressionContext",
13
+ CreateClause: "CreateClauseContext",
14
+ CypherQuery: "CypherQueryContext",
15
+ DeleteClause: "DeleteClauseContext",
16
+ Expression: "ExpressionContext",
17
+ FloatLiteral: "FloatLiteralContext",
18
+ FunctionInvocation: "FunctionInvocationContext",
19
+ FunctionInvocationBody: "FunctionInvocationBodyContext",
20
+ FunctionName: "FunctionNameContext",
21
+ IntegerLiteral: "IntegerLiteralContext",
22
+ Keyword: "KeywordContext",
23
+ LabelName: "LabelNameContext",
24
+ LeftArrowHead: "LeftArrowHeadContext",
25
+ Literal: "LiteralContext",
26
+ LiteralEntry: "LiteralEntryContext",
27
+ MapLiteral: "MapLiteralContext",
28
+ MatchClause: "MatchClauseContext",
29
+ NodeLabel: "NodeLabelContext",
30
+ NodeLabels: "NodeLabelsContext",
31
+ NodePattern: "NodePatternContext",
32
+ NumberLiteral: "NumberLiteralContext",
33
+ PartialComparisonExpression: "PartialComparisonExpressionContext",
34
+ Pattern: "PatternContext",
35
+ PatternElement: "PatternElementContext",
36
+ PatternElementChain: "PatternElementChainContext",
37
+ PatternPart: "PatternPartContext",
38
+ Properties: "PropertiesContext",
39
+ PropertyExpression: "PropertyExpressionContext",
40
+ PropertyKey: "PropertyKeyNameContext",
41
+ PropertyLookup: "PropertyLookupContext",
42
+ RangeLiteral: "RangeLiteralContext",
43
+ RelTypeName: "RelTypeNameContext",
44
+ RelationshipDetail: "RelationshipDetailContext",
45
+ RelationshipPattern: "RelationshipPatternContext",
46
+ RelationshipPatternEnd: "RelationshipPatternEndContext",
47
+ RelationshipPatternStart: "RelationshipPatternStartContext",
48
+ RelationshipType: "RelationshipTypeContext",
49
+ RelationshipTypes: "RelationshipTypesContext",
50
+ RegularQuery: "RegularQueryContext",
51
+ Query: "QueryContext",
52
+ RightArrowHead: "RightArrowHeadContext",
53
+ Limit: "LimitContext",
54
+ Order: "OrderContext",
55
+ Skip: "SkipContext",
56
+ ReturnBody: "ReturnBodyContext",
57
+ ReturnClause: "ReturnClauseContext",
58
+ ReturnItem: "ReturnItemContext",
59
+ ReturnItems: "ReturnItemsContext",
60
+ SortItem: "SortItemContext",
61
+ SetClause: "SetClauseContext",
62
+ SetItem: "SetItemContext",
63
+ SingleQuery: "SingleQueryContext",
64
+ Statement: "StatementContext",
65
+ StringLiteral: "StringLiteralContext",
66
+ SymbolicName: "SymbolicNameContext",
67
+ TerminalNode: "TerminalNodeImpl",
68
+ Variable: "VariableContext",
69
+ Where: "WhereContext",
70
+ WithClause: "WithClauseContext"
71
+ };
72
+ function validateContextNames() {
73
+ const richQuery = 'MATCH (a:User {name: "Alice"})-[r:FRIEND*1..2]->(b:User) WITH a.name AS name, count(b) AS cnt WHERE cnt > 0 RETURN name, cnt';
74
+ const chars = antlr4.CharStreams.fromString(richQuery);
75
+ const lexer = new CypherLexer(chars);
76
+ const tokens = new antlr4.CommonTokenStream(lexer);
77
+ const parser = new CypherParser(tokens);
78
+ const tree = parser.cypher();
79
+ const allNames = /* @__PURE__ */ new Set();
80
+ const collect = (ctx) => {
81
+ allNames.add(ctx.constructor.name);
82
+ if (ctx.children) {
83
+ for (const child of ctx.children) collect(child);
84
+ }
85
+ };
86
+ collect(tree);
87
+ const required = [
88
+ Ctx.CypherQuery,
89
+ Ctx.Statement,
90
+ Ctx.Query,
91
+ Ctx.RegularQuery,
92
+ Ctx.SingleQuery,
93
+ Ctx.Clause,
94
+ Ctx.MatchClause,
95
+ Ctx.ReturnClause,
96
+ Ctx.ReturnBody,
97
+ Ctx.ReturnItems,
98
+ Ctx.ReturnItem,
99
+ Ctx.Expression,
100
+ Ctx.Atom,
101
+ Ctx.Variable,
102
+ Ctx.SymbolicName,
103
+ Ctx.Pattern,
104
+ Ctx.PatternPart,
105
+ Ctx.AnonymousPatternPart,
106
+ Ctx.PatternElement,
107
+ Ctx.NodePattern,
108
+ Ctx.TerminalNode
109
+ ];
110
+ const missing = required.filter((name) => !allNames.has(name));
111
+ if (missing.length > 0) {
112
+ throw new Error(
113
+ `ANTLR4 context name mismatch. Core contexts missing: ${missing.join(", ")}. Available: ${[...allNames].join(", ")}`
114
+ );
115
+ }
116
+ }
117
+ var contextNamesValidated = false;
118
+ function ensureContextNamesValid() {
119
+ if (contextNamesValidated) return;
120
+ if (process.env.NODE_ENV === "test") return;
121
+ validateContextNames();
122
+ contextNamesValidated = true;
123
+ }
124
+ if (process.env.NODE_ENV === "development") {
125
+ validateContextNames();
126
+ }
127
+ var ErrorCollector = class {
128
+ errors = [];
129
+ syntaxError(_recognizer, _offendingSymbol, line, column, message, _e) {
130
+ this.errors.push(`line ${line}:${column} ${message}`);
131
+ }
132
+ // no-op — we only care about syntax errors
133
+ reportAmbiguity() {
134
+ }
135
+ reportAttemptingFullContext() {
136
+ }
137
+ reportContextSensitivity() {
138
+ }
139
+ };
140
+ function findPropertyLookup(exprCtx) {
141
+ if (!exprCtx) return null;
142
+ const walk = (ctx) => {
143
+ if (!ctx.children) return null;
144
+ for (const child of ctx.children) {
145
+ if (child.constructor.name === Ctx.PropertyLookup) return child;
146
+ const found = walk(child);
147
+ if (found) return found;
148
+ }
149
+ return null;
150
+ };
151
+ return walk(exprCtx);
152
+ }
153
+ function findChild(ctx, name) {
154
+ if (!ctx?.children) return null;
155
+ return ctx.children.find((c) => c.constructor.name === name) ?? null;
156
+ }
157
+ function findAllChildren(ctx, name) {
158
+ if (!ctx?.children) return [];
159
+ return ctx.children.filter((c) => c.constructor.name === name);
160
+ }
161
+ function hasTerminal(ctx, text) {
162
+ if (!ctx?.children) return false;
163
+ return ctx.children.some((c) => c.symbol?.text === text);
164
+ }
165
+ function getTerminalText(ctx) {
166
+ if (!ctx) return void 0;
167
+ const term = findChild(ctx, Ctx.TerminalNode);
168
+ return term?.symbol?.text;
169
+ }
170
+ function getSymbolicName(ctx) {
171
+ if (!ctx) return void 0;
172
+ const sym = findChild(ctx, Ctx.SymbolicName);
173
+ if (!sym) return getTerminalText(ctx);
174
+ const term = findChild(sym, Ctx.TerminalNode);
175
+ if (term) return term.symbol?.text;
176
+ const keyword = findChild(sym, Ctx.Keyword);
177
+ if (keyword) {
178
+ const keywordTerm = findChild(keyword, Ctx.TerminalNode);
179
+ return keywordTerm?.symbol?.text;
180
+ }
181
+ return sym.getText();
182
+ }
183
+ function getAtom(exprCtx) {
184
+ if (!exprCtx) return null;
185
+ const walk = (ctx) => {
186
+ if (ctx.constructor.name === Ctx.Atom) return ctx;
187
+ if (!ctx.children) return null;
188
+ for (const child of ctx.children) {
189
+ const found = walk(child);
190
+ if (found) return found;
191
+ }
192
+ return null;
193
+ };
194
+ return walk(exprCtx);
195
+ }
196
+ function evaluateExpression(exprCtx) {
197
+ if (!exprCtx) return void 0;
198
+ const atom = getAtom(exprCtx);
199
+ if (!atom) return void 0;
200
+ const funcCtx = findChild(atom, Ctx.FunctionInvocation);
201
+ if (funcCtx) {
202
+ const bodyCtx = findChild(funcCtx, Ctx.FunctionInvocationBody);
203
+ const funcNameCtx = findChild(bodyCtx, Ctx.FunctionName);
204
+ const funcName = getTerminalText(funcNameCtx);
205
+ const argExpr = findChild(funcCtx, Ctx.Expression);
206
+ const argAtom = getAtom(argExpr ?? void 0);
207
+ const argVar = argAtom ? findChild(argAtom, Ctx.Variable) : null;
208
+ const argName = getSymbolicName(argVar);
209
+ let argProperty;
210
+ if (argAtom) {
211
+ const propLookup = findChild(argAtom, Ctx.PropertyLookup);
212
+ if (!propLookup) {
213
+ const parentPropLookup = findPropertyLookup(argExpr);
214
+ if (parentPropLookup) {
215
+ const pkCtx = findChild(parentPropLookup, Ctx.PropertyKey);
216
+ argProperty = getSymbolicName(pkCtx);
217
+ }
218
+ } else {
219
+ const pkCtx = findChild(propLookup, Ctx.PropertyKey);
220
+ argProperty = getSymbolicName(pkCtx);
221
+ }
222
+ }
223
+ if (funcName && argName) {
224
+ return {
225
+ type: "Aggregation",
226
+ aggregationType: funcName.toUpperCase(),
227
+ variable: argName,
228
+ property: argProperty
229
+ };
230
+ }
231
+ }
232
+ const varCtx = findChild(atom, Ctx.Variable);
233
+ if (varCtx) {
234
+ const name = getSymbolicName(varCtx);
235
+ if (name) {
236
+ const propLookup = findPropertyLookup(exprCtx);
237
+ if (propLookup) {
238
+ const propName = getSymbolicName(findChild(propLookup, Ctx.PropertyKey));
239
+ if (propName) {
240
+ return { type: "PropertyAccess", variable: name, property: propName };
241
+ }
242
+ }
243
+ return { type: "PropertyAccess", variable: name, property: void 0 };
244
+ }
245
+ }
246
+ const literalCtx = findChild(atom, Ctx.Literal);
247
+ const literal = extractLiteral(literalCtx);
248
+ if (literal) return literal;
249
+ return void 0;
250
+ }
251
+ function unescapeStringLiteral(raw) {
252
+ if (raw.startsWith('"') && raw.endsWith('"')) {
253
+ raw = raw.slice(1, -1);
254
+ }
255
+ return raw.replace(/\\(.)/g, (match, char, offset) => {
256
+ switch (char) {
257
+ case '"':
258
+ return '"';
259
+ case "\\":
260
+ return "\\";
261
+ case "n":
262
+ return "\n";
263
+ case "t":
264
+ return " ";
265
+ case "r":
266
+ return "\r";
267
+ case "b":
268
+ return "\b";
269
+ case "f":
270
+ return "\f";
271
+ case "/":
272
+ return "/";
273
+ case "u": {
274
+ const hex = raw.slice(offset + 2, offset + 6);
275
+ return hex.length === 4 ? String.fromCharCode(parseInt(hex, 16)) : `\\u${hex}`;
276
+ }
277
+ default:
278
+ return `\\${char}`;
279
+ }
280
+ });
281
+ }
282
+ function extractLiteral(literalCtx) {
283
+ if (!literalCtx) return void 0;
284
+ const stringLit = findChild(literalCtx, Ctx.StringLiteral);
285
+ if (stringLit) {
286
+ const raw = getTerminalText(stringLit);
287
+ if (raw) return { type: "Literal", value: unescapeStringLiteral(raw) };
288
+ }
289
+ const numLit = findChild(literalCtx, Ctx.NumberLiteral);
290
+ if (numLit) {
291
+ const intLit = findChild(numLit, Ctx.IntegerLiteral);
292
+ if (intLit) {
293
+ const text = getTerminalText(intLit);
294
+ if (text) return { type: "Literal", value: parseInt(text, 10) };
295
+ }
296
+ const floatLit = findChild(numLit, Ctx.FloatLiteral);
297
+ if (floatLit) {
298
+ const text = getTerminalText(floatLit);
299
+ if (text) return { type: "Literal", value: parseFloat(text) };
300
+ }
301
+ }
302
+ const boolLit = findChild(literalCtx, Ctx.BooleanLiteral);
303
+ if (boolLit) {
304
+ const text = getTerminalText(boolLit);
305
+ if (text === "true") return { type: "Literal", value: true };
306
+ if (text === "false") return { type: "Literal", value: false };
307
+ }
308
+ if (hasTerminal(literalCtx, "null") || hasTerminal(literalCtx, "NULL")) {
309
+ return { type: "Literal", value: null };
310
+ }
311
+ return void 0;
312
+ }
313
+ function extractNodePattern(nodePatternCtx) {
314
+ if (!nodePatternCtx) return { variable: "", label: void 0, properties: void 0 };
315
+ const variable = getSymbolicName(findChild(nodePatternCtx, Ctx.Variable)) ?? "";
316
+ const labelsCtx = findChild(nodePatternCtx, Ctx.NodeLabels);
317
+ const labelCtx = findChild(labelsCtx, Ctx.NodeLabel);
318
+ const labelNameCtx = findChild(labelCtx, Ctx.LabelName);
319
+ const label = getSymbolicName(labelNameCtx);
320
+ const propsCtx = findChild(nodePatternCtx, Ctx.Properties);
321
+ const mapLitCtx = findChild(propsCtx, Ctx.MapLiteral);
322
+ const properties = extractProperties(mapLitCtx);
323
+ return { variable, label, properties };
324
+ }
325
+ function extractProperties(mapLiteralCtx) {
326
+ if (!mapLiteralCtx) return void 0;
327
+ const entries = findAllChildren(mapLiteralCtx, Ctx.LiteralEntry);
328
+ if (entries.length === 0) return void 0;
329
+ const props = {};
330
+ for (const entry of entries) {
331
+ const keyCtx = findChild(entry, Ctx.PropertyKey);
332
+ const key = getSymbolicName(keyCtx);
333
+ const exprCtx = findChild(entry, Ctx.Expression);
334
+ const value = evaluateExpression(exprCtx);
335
+ if (key && value && value.type === "Literal") {
336
+ props[key] = value.value;
337
+ }
338
+ }
339
+ return Object.keys(props).length > 0 ? props : void 0;
340
+ }
341
+ function extractRelationPattern(relPatternCtx) {
342
+ if (!relPatternCtx) return { variable: void 0, type: void 0, minDepth: void 0, maxDepth: void 0, direction: "UNDIRECTED" };
343
+ const direction = extractDirection(relPatternCtx);
344
+ const detailCtx = findChild(relPatternCtx, Ctx.RelationshipDetail);
345
+ const variable = detailCtx ? getSymbolicName(findChild(detailCtx, Ctx.Variable)) : void 0;
346
+ const typesCtx = findChild(detailCtx, Ctx.RelationshipTypes);
347
+ const typeCtx = findChild(typesCtx, Ctx.RelationshipType);
348
+ const typeNameCtx = findChild(typeCtx, Ctx.RelTypeName);
349
+ const type = getSymbolicName(typeNameCtx);
350
+ const rangeCtx = findChild(detailCtx, Ctx.RangeLiteral);
351
+ if (rangeCtx) {
352
+ const intLits = findAllChildren(rangeCtx, Ctx.IntegerLiteral);
353
+ const values = intLits.map((ic) => {
354
+ const text = getTerminalText(ic);
355
+ return text ? parseInt(text, 10) : 0;
356
+ });
357
+ if (values.length >= 1) {
358
+ return {
359
+ variable,
360
+ type,
361
+ minDepth: values[0],
362
+ maxDepth: values.length >= 2 ? values[1] : values[0],
363
+ direction
364
+ };
365
+ }
366
+ }
367
+ return { variable, type, minDepth: void 0, maxDepth: void 0, direction };
368
+ }
369
+ function extractDirection(relPatternCtx) {
370
+ if (!relPatternCtx) return "UNDIRECTED";
371
+ const startCtx = findChild(relPatternCtx, Ctx.RelationshipPatternStart);
372
+ const endCtx = findChild(relPatternCtx, Ctx.RelationshipPatternEnd);
373
+ const hasStartArrow = !!findChild(startCtx, Ctx.LeftArrowHead);
374
+ const hasEndArrow = !!findChild(endCtx, Ctx.RightArrowHead);
375
+ if (hasStartArrow && hasEndArrow) return "UNDIRECTED";
376
+ if (hasStartArrow) return "IN";
377
+ if (hasEndArrow) return "OUT";
378
+ return "UNDIRECTED";
379
+ }
380
+ function extractMatchClause(clauseCtx) {
381
+ const matchCtx = findChild(clauseCtx, Ctx.MatchClause);
382
+ const optional = hasTerminal(clauseCtx, "OPTIONAL") || matchCtx && hasTerminal(matchCtx, "OPTIONAL");
383
+ const patternCtx = findChild(matchCtx, Ctx.Pattern);
384
+ if (!patternCtx) throw new Error("Failed to parse MATCH: missing Pattern node.");
385
+ const patternPart = findChild(patternCtx, Ctx.PatternPart);
386
+ if (!patternPart) throw new Error("Failed to parse MATCH: missing PatternPart node.");
387
+ const anonPart = findChild(patternPart, Ctx.AnonymousPatternPart);
388
+ if (!anonPart) throw new Error("Failed to parse MATCH: missing AnonymousPatternPart node.");
389
+ const element = findChild(anonPart, Ctx.PatternElement);
390
+ if (!element) throw new Error("Failed to parse MATCH: missing PatternElement node.");
391
+ const nodePatterns = findAllChildren(element, Ctx.NodePattern);
392
+ const chains = findAllChildren(element, Ctx.PatternElementChain);
393
+ const sourcePattern = nodePatterns[0] ? extractNodePattern(nodePatterns[0]) : { variable: "", label: void 0, properties: void 0 };
394
+ let relationPattern = { variable: void 0, type: void 0, minDepth: void 0, maxDepth: void 0, direction: "UNDIRECTED" };
395
+ let targetPattern = { variable: "", label: void 0, properties: void 0 };
396
+ const hasChains = chains.length > 0;
397
+ if (chains.length > 1) {
398
+ throw new Error("Multi-hop patterns (more than one relationship chain) are not supported. Use multiple MATCH stages or a WITH clause.");
399
+ }
400
+ if (hasChains) {
401
+ const chain = chains[0];
402
+ const relPatternCtx = findChild(chain, Ctx.RelationshipPattern);
403
+ relationPattern = extractRelationPattern(relPatternCtx);
404
+ const targetNodeCtx = findChild(chain, Ctx.NodePattern);
405
+ if (targetNodeCtx) {
406
+ targetPattern = extractNodePattern(targetNodeCtx);
407
+ }
408
+ } else if (nodePatterns.length > 1) {
409
+ targetPattern = extractNodePattern(nodePatterns[1]);
410
+ }
411
+ return { optional: !!optional, hasChains, sourcePattern, relationPattern, targetPattern };
412
+ }
413
+ function computeDefaultAlias(expr) {
414
+ if (expr.type === "PropertyAccess") {
415
+ return expr.property ?? expr.variable;
416
+ }
417
+ if (expr.type === "Aggregation") {
418
+ return `${expr.aggregationType}(${expr.variable})`;
419
+ }
420
+ return String(expr.value);
421
+ }
422
+ function extractReturnBody(returnBody) {
423
+ if (!returnBody) return [];
424
+ const returnItems = findChild(returnBody, Ctx.ReturnItems);
425
+ if (!returnItems) return [];
426
+ const items = findAllChildren(returnItems, Ctx.ReturnItem);
427
+ const parsedItems = [];
428
+ for (const item of items) {
429
+ const exprCtx = findChild(item, Ctx.Expression);
430
+ const expr = evaluateExpression(exprCtx);
431
+ if (!expr) continue;
432
+ const hasAs = hasTerminal(item, "AS");
433
+ let asAlias;
434
+ if (hasAs) {
435
+ const aliasVar = findChild(item, Ctx.Variable);
436
+ asAlias = getSymbolicName(aliasVar);
437
+ }
438
+ parsedItems.push({ expr, hasAs, asAlias });
439
+ }
440
+ const aliasCounts = /* @__PURE__ */ new Map();
441
+ for (const { expr, hasAs, asAlias } of parsedItems) {
442
+ if (hasAs) continue;
443
+ const alias = computeDefaultAlias(expr);
444
+ aliasCounts.set(alias, (aliasCounts.get(alias) ?? 0) + 1);
445
+ }
446
+ const projections = [];
447
+ const usedAliases = /* @__PURE__ */ new Set();
448
+ for (const { expr, hasAs, asAlias } of parsedItems) {
449
+ if (hasAs) {
450
+ if (asAlias) {
451
+ projections.push({ expression: expr, alias: asAlias });
452
+ usedAliases.add(asAlias);
453
+ }
454
+ continue;
455
+ }
456
+ let alias = computeDefaultAlias(expr);
457
+ if ((aliasCounts.get(alias) ?? 0) > 1 && expr.type === "PropertyAccess" && expr.property) {
458
+ alias = `${expr.variable}.${expr.property}`;
459
+ }
460
+ if (usedAliases.has(alias)) {
461
+ let idx = 1;
462
+ alias = `${alias}_${idx}`;
463
+ while (usedAliases.has(alias)) {
464
+ idx++;
465
+ alias = `${alias.slice(0, -String(idx).length - 1)}_${idx}`;
466
+ }
467
+ }
468
+ usedAliases.add(alias);
469
+ projections.push({ expression: expr, alias });
470
+ }
471
+ return projections;
472
+ }
473
+ function extractOrderBy(returnBody) {
474
+ const orderCtx = findChild(returnBody, Ctx.Order);
475
+ if (!orderCtx) return void 0;
476
+ const sortItems = findAllChildren(orderCtx, Ctx.SortItem);
477
+ if (sortItems.length === 0) return void 0;
478
+ const items = [];
479
+ for (const sortItem of sortItems) {
480
+ const exprCtx = findChild(sortItem, Ctx.Expression);
481
+ const expr = evaluateExpression(exprCtx);
482
+ if (!expr) continue;
483
+ const hasDesc = hasTerminal(sortItem, "DESC");
484
+ const direction = hasDesc ? "DESC" : "ASC";
485
+ items.push({ expression: expr, direction });
486
+ }
487
+ return items.length > 0 ? items : void 0;
488
+ }
489
+ function extractLimit(returnBody) {
490
+ const limitCtx = findChild(returnBody, Ctx.Limit);
491
+ if (!limitCtx) return void 0;
492
+ const exprCtx = findChild(limitCtx, Ctx.Expression);
493
+ const expr = evaluateExpression(exprCtx);
494
+ if (expr && expr.type === "Literal" && typeof expr.value === "number") {
495
+ return expr.value;
496
+ }
497
+ return void 0;
498
+ }
499
+ function extractSkip(returnBody) {
500
+ const skipCtx = findChild(returnBody, Ctx.Skip);
501
+ if (!skipCtx) return void 0;
502
+ const exprCtx = findChild(skipCtx, Ctx.Expression);
503
+ const expr = evaluateExpression(exprCtx);
504
+ if (expr && expr.type === "Literal" && typeof expr.value === "number") {
505
+ return expr.value;
506
+ }
507
+ return void 0;
508
+ }
509
+ function extractReturnClause(clauseCtx) {
510
+ const returnCtx = findChild(clauseCtx, Ctx.ReturnClause);
511
+ if (!returnCtx) return void 0;
512
+ const returnBody = findChild(returnCtx, Ctx.ReturnBody);
513
+ const projections = extractReturnBody(returnBody);
514
+ const orderBy = extractOrderBy(returnBody);
515
+ const skip = extractSkip(returnBody);
516
+ const limit = extractLimit(returnBody);
517
+ return { projections, orderBy, skip, limit };
518
+ }
519
+ function extractWithClause(clauseCtx) {
520
+ const withCtx = findChild(clauseCtx, Ctx.WithClause);
521
+ if (!withCtx) return void 0;
522
+ const returnBody = findChild(withCtx, Ctx.ReturnBody);
523
+ const projections = extractReturnBody(returnBody);
524
+ const orderBy = extractOrderBy(returnBody);
525
+ const skip = extractSkip(returnBody);
526
+ const limit = extractLimit(returnBody);
527
+ const whereCtx = findChild(withCtx, Ctx.Where);
528
+ const whereExpr = findChild(whereCtx, Ctx.Expression);
529
+ const where = whereExpr ? extractComparison(whereExpr) : void 0;
530
+ return { projections, where, orderBy, skip, limit };
531
+ }
532
+ function extractComparison(exprCtx) {
533
+ if (!exprCtx) return void 0;
534
+ let foundPartial;
535
+ let foundComp;
536
+ const walk = (ctx) => {
537
+ if (ctx.constructor.name === Ctx.PartialComparisonExpression) {
538
+ foundPartial = ctx;
539
+ } else if (ctx.constructor.name === Ctx.ComparisonExpression) {
540
+ foundComp = ctx;
541
+ }
542
+ if (ctx.children) {
543
+ for (const child of ctx.children) {
544
+ walk(child);
545
+ }
546
+ }
547
+ };
548
+ walk(exprCtx);
549
+ if (!foundPartial || !foundComp) return void 0;
550
+ const operatorTerm = findChild(foundPartial, Ctx.TerminalNode);
551
+ const operator = operatorTerm?.symbol?.text;
552
+ if (!operator) return void 0;
553
+ const leftExprCtx = findChild(foundComp, Ctx.AddOrSubtractExpression);
554
+ const left = extractValueExpression(leftExprCtx);
555
+ const rightExprCtx = findChild(foundPartial, Ctx.AddOrSubtractExpression);
556
+ const right = extractValueExpression(rightExprCtx);
557
+ if (left && right) {
558
+ return { type: "BinaryExpression", operator, left, right };
559
+ }
560
+ return void 0;
561
+ }
562
+ function extractValueExpression(ctx) {
563
+ if (!ctx) return void 0;
564
+ const atom = getAtom(ctx);
565
+ if (!atom) return void 0;
566
+ const varCtx = findChild(atom, Ctx.Variable);
567
+ if (varCtx) {
568
+ const name = getSymbolicName(varCtx);
569
+ if (name) {
570
+ const propLookup = findPropertyLookup(ctx);
571
+ if (propLookup) {
572
+ const propName = getSymbolicName(findChild(propLookup, Ctx.PropertyKey));
573
+ if (propName) {
574
+ return { type: "PropertyAccess", variable: name, property: propName };
575
+ }
576
+ }
577
+ return { type: "PropertyAccess", variable: name, property: void 0 };
578
+ }
579
+ }
580
+ const literalCtx = findChild(atom, Ctx.Literal);
581
+ return extractLiteral(literalCtx);
582
+ }
583
+ function extractWriteClause(clauseCtx) {
584
+ const setCtx = findChild(clauseCtx, Ctx.SetClause);
585
+ if (setCtx) {
586
+ const setItem = findChild(setCtx, Ctx.SetItem);
587
+ if (!setItem) throw new Error("Failed to parse SET: missing SetItem node in AST.");
588
+ const propExpr = findChild(setItem, Ctx.PropertyExpression);
589
+ if (!propExpr) throw new Error("Failed to parse SET: missing PropertyExpression node in AST.");
590
+ const atom = findChild(propExpr, Ctx.Atom);
591
+ if (!atom) throw new Error("Failed to parse SET: missing Atom node in AST.");
592
+ const varCtx = findChild(atom, Ctx.Variable);
593
+ const variable = getSymbolicName(varCtx);
594
+ if (!variable) throw new Error("Failed to parse SET: missing variable name.");
595
+ const propLookup = findChild(propExpr, Ctx.PropertyLookup);
596
+ if (!propLookup) throw new Error("Failed to parse SET: missing PropertyLookup node in AST.");
597
+ const propKeyCtx = findChild(propLookup, Ctx.PropertyKey);
598
+ const property = getSymbolicName(propKeyCtx);
599
+ if (!property) throw new Error("Failed to parse SET: missing property name.");
600
+ const exprCtx = findChild(setItem, Ctx.Expression);
601
+ const valueExpr = evaluateExpression(exprCtx);
602
+ if (!valueExpr) {
603
+ throw new Error(`Failed to parse SET: could not extract value for "${variable}.${property}".`);
604
+ }
605
+ if (valueExpr.type !== "Literal") {
606
+ throw new Error(`Failed to parse SET: only literal values are supported on the right-hand side, got ${valueExpr.type}.`);
607
+ }
608
+ return { type: "SET", variable, property, value: valueExpr.value };
609
+ }
610
+ const createCtx = findChild(clauseCtx, Ctx.CreateClause);
611
+ if (createCtx) {
612
+ const patternCtx = findChild(createCtx, Ctx.Pattern);
613
+ if (!patternCtx) throw new Error("Failed to parse CREATE: missing Pattern node in AST.");
614
+ const patternPart = findChild(patternCtx, Ctx.PatternPart);
615
+ if (!patternPart) throw new Error("Failed to parse CREATE: missing PatternPart node in AST.");
616
+ const anonPart = findChild(patternPart, Ctx.AnonymousPatternPart);
617
+ if (!anonPart) throw new Error("Failed to parse CREATE: missing AnonymousPatternPart node in AST.");
618
+ const element = findChild(anonPart, Ctx.PatternElement);
619
+ if (!element) throw new Error("Failed to parse CREATE: missing PatternElement node in AST.");
620
+ const nodePatternCtx = findChild(element, Ctx.NodePattern);
621
+ if (!nodePatternCtx) throw new Error("Failed to parse CREATE: missing NodePattern node in AST.");
622
+ const variable = getSymbolicName(findChild(nodePatternCtx, Ctx.Variable)) ?? "";
623
+ const labelsCtx = findChild(nodePatternCtx, Ctx.NodeLabels);
624
+ const labelCtx = findChild(labelsCtx, Ctx.NodeLabel);
625
+ const labelNameCtx = findChild(labelCtx, Ctx.LabelName);
626
+ const label = getSymbolicName(labelNameCtx);
627
+ const propsCtx = findChild(nodePatternCtx, Ctx.Properties);
628
+ const mapLitCtx = findChild(propsCtx, Ctx.MapLiteral);
629
+ const properties = extractProperties(mapLitCtx);
630
+ return { type: "CREATE", variable, label, properties };
631
+ }
632
+ const deleteCtx = findChild(clauseCtx, Ctx.DeleteClause);
633
+ if (deleteCtx) {
634
+ const exprCtx = findChild(deleteCtx, Ctx.Expression);
635
+ if (!exprCtx) throw new Error("Failed to parse DELETE: missing Expression node in AST.");
636
+ const atom = getAtom(exprCtx);
637
+ if (!atom) throw new Error("Failed to parse DELETE: missing Atom node in AST.");
638
+ const varCtx = findChild(atom, Ctx.Variable);
639
+ const variable = getSymbolicName(varCtx);
640
+ if (!variable) throw new Error("Failed to parse DELETE: missing variable name.");
641
+ return { type: "DELETE", variable };
642
+ }
643
+ return void 0;
644
+ }
645
+ function parseCypher(query) {
646
+ ensureContextNamesValid();
647
+ const chars = antlr4.CharStreams.fromString(query);
648
+ const lexer = new CypherLexer(chars);
649
+ const tokens = new antlr4.CommonTokenStream(lexer);
650
+ const parser = new CypherParser(tokens);
651
+ const collector = new ErrorCollector();
652
+ parser.removeErrorListeners();
653
+ parser.addErrorListener(collector);
654
+ const tree = parser.cypher();
655
+ if (collector.errors.length > 0) {
656
+ throw new Error(`Failed to parse Cypher query: ${collector.errors.join("; ")}`);
657
+ }
658
+ const stages = [];
659
+ let returnClause;
660
+ const cypherPart = tree.children?.[0];
661
+ const cypherQuery = findChild(cypherPart, Ctx.CypherQuery);
662
+ const statement = findChild(cypherQuery, Ctx.Statement);
663
+ const queryCtx = findChild(statement, Ctx.Query);
664
+ const regularQuery = findChild(queryCtx, Ctx.RegularQuery);
665
+ const singleQuery = findChild(regularQuery, Ctx.SingleQuery);
666
+ if (!singleQuery) {
667
+ return { type: "Query", stages, return: returnClause };
668
+ }
669
+ const clauses = findAllChildren(singleQuery, Ctx.Clause);
670
+ for (const clause of clauses) {
671
+ if (findChild(clause, Ctx.MatchClause)) {
672
+ stages.push({ type: "MATCH", clause: extractMatchClause(clause) });
673
+ } else if (findChild(clause, Ctx.ReturnClause)) {
674
+ returnClause = extractReturnClause(clause);
675
+ } else if (findChild(clause, Ctx.WithClause)) {
676
+ const withClause = extractWithClause(clause);
677
+ if (withClause) stages.push({ type: "WITH", clause: withClause });
678
+ } else if (findChild(clause, Ctx.SetClause)) {
679
+ const writeClause = extractWriteClause(clause);
680
+ if (writeClause) stages.push({ type: "WRITE", clause: writeClause });
681
+ } else if (findChild(clause, Ctx.CreateClause)) {
682
+ const writeClause = extractWriteClause(clause);
683
+ if (writeClause) stages.push({ type: "WRITE", clause: writeClause });
684
+ } else if (findChild(clause, Ctx.DeleteClause)) {
685
+ const writeClause = extractWriteClause(clause);
686
+ if (writeClause) stages.push({ type: "WRITE", clause: writeClause });
687
+ }
688
+ }
689
+ return { type: "Query", stages, return: returnClause };
690
+ }
691
+
692
+ // src/engine/cypher-engine.ts
693
+ import { randomUUID } from "crypto";
694
+ var CHAIN_BASE = /* @__PURE__ */ Symbol("contextBase");
695
+ var CHAIN_OVERRIDES = /* @__PURE__ */ Symbol("contextOverrides");
696
+ function isContextChain(ctx) {
697
+ return CHAIN_BASE in ctx && CHAIN_OVERRIDES in ctx;
698
+ }
699
+ function resolveChainValue(chain, key) {
700
+ let current = chain;
701
+ while (current !== null) {
702
+ if (isContextChain(current)) {
703
+ const val = current[CHAIN_OVERRIDES][key];
704
+ if (val !== void 0) return val;
705
+ current = current[CHAIN_BASE];
706
+ } else {
707
+ return current[key];
708
+ }
709
+ }
710
+ return void 0;
711
+ }
712
+ function materialiseChain(chain) {
713
+ const result = {};
714
+ const stack = [];
715
+ let current = chain;
716
+ while (current !== null) {
717
+ if (isContextChain(current)) {
718
+ stack.push(current[CHAIN_OVERRIDES]);
719
+ current = current[CHAIN_BASE];
720
+ } else {
721
+ stack.push(current);
722
+ break;
723
+ }
724
+ }
725
+ for (let i = 0; i < stack.length; i++) {
726
+ Object.assign(result, stack[i]);
727
+ }
728
+ return result;
729
+ }
730
+ var AdvancedCypherGraphologyEngine = class {
731
+ graph;
732
+ indexes;
733
+ constructor(graph, indexes) {
734
+ this.graph = graph;
735
+ this.indexes = indexes;
736
+ }
737
+ /**
738
+ * MAIN ENTRY POINT
739
+ * Sequentially executes query stages and formats the return projection.
740
+ */
741
+ execute(ast) {
742
+ let contexts = [{}];
743
+ for (const stage of ast.stages) {
744
+ if (stage.type === "MATCH") {
745
+ contexts = this.executeMatch(stage.clause, contexts);
746
+ } else if (stage.type === "WITH") {
747
+ contexts = this.executeWith(stage.clause, contexts);
748
+ } else if (stage.type === "WRITE") {
749
+ this.executeWrite(stage.clause, contexts);
750
+ }
751
+ }
752
+ if (ast.return) {
753
+ return this.executeReturn(ast.return, contexts);
754
+ }
755
+ return [];
756
+ }
757
+ // ── Index-based node lookup (optimisation #2, #3) ─────────────────────────
758
+ /**
759
+ * Resolve node IDs matching a pattern using indexes when available.
760
+ * Falls back to full-graph scan when indexes are absent.
761
+ */
762
+ getMatchingNodeIds(pattern) {
763
+ const indexes = this.indexes;
764
+ if (!indexes) {
765
+ return this.graph.filterNodes(
766
+ (_node, attr) => this.matchNodeCriteria(attr, pattern)
767
+ );
768
+ }
769
+ const { labelIndex, propertyIndex } = indexes;
770
+ const label = pattern.label;
771
+ const props = pattern.properties;
772
+ const propKeys = props ? Object.keys(props) : [];
773
+ const hasLabel = label !== void 0;
774
+ const hasProps = propKeys.length > 0;
775
+ if (hasLabel && hasProps && props) {
776
+ const labelSet = labelIndex.get(label);
777
+ if (!labelSet || labelSet.size === 0) return [];
778
+ const firstKey = propKeys[0];
779
+ if (!firstKey) return [];
780
+ const firstVal = String(props[firstKey]);
781
+ const propSet = propertyIndex.get(firstKey)?.get(firstVal);
782
+ if (!propSet || propSet.size === 0) return [];
783
+ const candidates = propSet.size < labelSet.size ? [...propSet].filter((id) => labelSet.has(id)) : [...labelSet].filter((id) => propSet.has(id));
784
+ if (propKeys.length <= 1) return candidates;
785
+ return candidates.filter((id) => {
786
+ const attrs = this.graph.getNodeAttributes(id);
787
+ return propKeys.slice(1).every((k) => attrs[k] === props[k]);
788
+ });
789
+ }
790
+ if (hasLabel) {
791
+ const labelSet = labelIndex.get(label);
792
+ return labelSet ? [...labelSet] : [];
793
+ }
794
+ if (hasProps && props) {
795
+ const firstKey = propKeys[0];
796
+ if (!firstKey) return [];
797
+ const firstVal = String(props[firstKey]);
798
+ const propSet = propertyIndex.get(firstKey)?.get(firstVal);
799
+ if (!propSet) return [];
800
+ if (propKeys.length === 1) return [...propSet];
801
+ return [...propSet].filter((id) => {
802
+ const attrs = this.graph.getNodeAttributes(id);
803
+ return propKeys.slice(1).every((k) => attrs[k] === props[k]);
804
+ });
805
+ }
806
+ return this.graph.filterNodes(() => true);
807
+ }
808
+ // ── 1. MATCH & OPTIONAL MATCH STAGE ────────────────────────────────────────
809
+ // Optimisations applied:
810
+ // #1 DFS uses shared onStack set + shared edge array (no per-edge copies)
811
+ // #2 Label index for start/target node resolution
812
+ // #3 Property index for filtered patterns
813
+ // #4 Context chains avoid full context copies during traversal
814
+ executeMatch(clause, incomingContexts) {
815
+ const { sourcePattern, relationPattern, targetPattern, optional, hasChains } = clause;
816
+ const outgoingContexts = [];
817
+ const eligibleTargetIds = new Set(this.getMatchingNodeIds(targetPattern));
818
+ for (const context of incomingContexts) {
819
+ let startNodeIds = [];
820
+ const boundNode = resolveChainValue(context, sourcePattern.variable);
821
+ if (boundNode && typeof boundNode === "object" && !Array.isArray(boundNode) && "id" in boundNode) {
822
+ const boundId = boundNode.id;
823
+ if (this.graph.hasNode(boundId)) {
824
+ const freshAttrs = this.graph.getNodeAttributes(boundId);
825
+ if (this.matchNodeCriteria(freshAttrs, sourcePattern)) {
826
+ startNodeIds = [boundId];
827
+ }
828
+ }
829
+ } else {
830
+ startNodeIds = this.getMatchingNodeIds(sourcePattern);
831
+ }
832
+ let matchFoundForThisContext = false;
833
+ startNodeIds.forEach((startId) => {
834
+ const sourceAttr = this.graph.getNodeAttributes(startId);
835
+ const sourceNode = { id: startId, ...sourceAttr };
836
+ if (!hasChains) {
837
+ matchFoundForThisContext = true;
838
+ outgoingContexts.push({
839
+ [CHAIN_BASE]: context,
840
+ [CHAIN_OVERRIDES]: { [sourcePattern.variable]: sourceNode }
841
+ });
842
+ return;
843
+ }
844
+ const minDepth = relationPattern.minDepth ?? 1;
845
+ const maxDepth = relationPattern.maxDepth ?? 1;
846
+ const getNeighbors = this.buildNeighborGetter(relationPattern);
847
+ const onStack = /* @__PURE__ */ new Set();
848
+ const edgeHistory = [];
849
+ const explore = (currentId) => {
850
+ if (onStack.has(currentId)) return;
851
+ onStack.add(currentId);
852
+ if (edgeHistory.length >= minDepth && eligibleTargetIds.has(currentId)) {
853
+ matchFoundForThisContext = true;
854
+ const targetAttr = this.graph.getNodeAttributes(currentId);
855
+ const chain = {
856
+ [CHAIN_BASE]: context,
857
+ [CHAIN_OVERRIDES]: {
858
+ [sourcePattern.variable]: sourceNode,
859
+ [targetPattern.variable]: { id: currentId, ...targetAttr }
860
+ }
861
+ };
862
+ if (relationPattern.variable) {
863
+ chain[CHAIN_OVERRIDES][relationPattern.variable] = edgeHistory.map(
864
+ (edgeId) => ({ id: edgeId, ...this.graph.getEdgeAttributes(edgeId) })
865
+ );
866
+ }
867
+ outgoingContexts.push(chain);
868
+ }
869
+ if (edgeHistory.length >= maxDepth) {
870
+ onStack.delete(currentId);
871
+ return;
872
+ }
873
+ getNeighbors(currentId, (neighborId, edgeId) => {
874
+ if (neighborId === currentId) {
875
+ if (edgeHistory.length + 1 >= minDepth && eligibleTargetIds.has(currentId)) {
876
+ matchFoundForThisContext = true;
877
+ const targetAttr = this.graph.getNodeAttributes(currentId);
878
+ const chain = {
879
+ [CHAIN_BASE]: context,
880
+ [CHAIN_OVERRIDES]: {
881
+ [sourcePattern.variable]: sourceNode,
882
+ [targetPattern.variable]: { id: currentId, ...targetAttr }
883
+ }
884
+ };
885
+ if (relationPattern.variable) {
886
+ chain[CHAIN_OVERRIDES][relationPattern.variable] = [...edgeHistory, edgeId].map(
887
+ (eid) => ({ id: eid, ...this.graph.getEdgeAttributes(eid) })
888
+ );
889
+ }
890
+ outgoingContexts.push(chain);
891
+ }
892
+ return;
893
+ }
894
+ edgeHistory.push(edgeId);
895
+ explore(neighborId);
896
+ edgeHistory.pop();
897
+ });
898
+ onStack.delete(currentId);
899
+ };
900
+ explore(startId);
901
+ });
902
+ if (optional && !matchFoundForThisContext) {
903
+ const nullChain = {
904
+ [CHAIN_BASE]: context,
905
+ [CHAIN_OVERRIDES]: { [targetPattern.variable]: null }
906
+ };
907
+ if (relationPattern.variable) nullChain[CHAIN_OVERRIDES][relationPattern.variable] = [];
908
+ outgoingContexts.push(nullChain);
909
+ }
910
+ }
911
+ return outgoingContexts;
912
+ }
913
+ /**
914
+ * Build a neighbor iterator using the edge-type adjacency index when available.
915
+ * Falls back to graph iteration for untyped edges or missing indexes.
916
+ */
917
+ buildNeighborGetter(relation) {
918
+ const indexes = this.indexes;
919
+ const edgeType = relation.type;
920
+ const hasIndex = indexes !== void 0 && edgeType !== void 0;
921
+ if (hasIndex && edgeType && relation.direction === "OUT") {
922
+ const adj = indexes.edgeTypeIndex.out.get(edgeType);
923
+ return (nodeId, cb) => {
924
+ const neighbors = adj?.get(nodeId);
925
+ if (!neighbors) return;
926
+ for (const n of neighbors) {
927
+ cb(n.target, n.edgeId);
928
+ }
929
+ };
930
+ }
931
+ if (hasIndex && edgeType && relation.direction === "IN") {
932
+ const adj = indexes.edgeTypeIndex.in.get(edgeType);
933
+ return (nodeId, cb) => {
934
+ const neighbors = adj?.get(nodeId);
935
+ if (!neighbors) return;
936
+ for (const n of neighbors) {
937
+ cb(n.source, n.edgeId);
938
+ }
939
+ };
940
+ }
941
+ if (hasIndex && edgeType && relation.direction === "UNDIRECTED") {
942
+ const adjOut = indexes.edgeTypeIndex.out.get(edgeType);
943
+ const adjIn = indexes.edgeTypeIndex.in.get(edgeType);
944
+ return (nodeId, cb) => {
945
+ const outNeighbors = adjOut?.get(nodeId);
946
+ if (outNeighbors) {
947
+ for (const n of outNeighbors) {
948
+ cb(n.target, n.edgeId);
949
+ }
950
+ }
951
+ const inNeighbors = adjIn?.get(nodeId);
952
+ if (inNeighbors) {
953
+ for (const n of inNeighbors) {
954
+ cb(n.source, n.edgeId);
955
+ }
956
+ }
957
+ };
958
+ }
959
+ return (nodeId, cb) => {
960
+ const iterator = (id, edgeCb) => {
961
+ if (relation.direction === "OUT") this.graph.forEachOutboundEdge(id, edgeCb);
962
+ else if (relation.direction === "IN") this.graph.forEachInboundEdge(id, edgeCb);
963
+ else this.graph.forEachEdge(id, edgeCb);
964
+ };
965
+ iterator(nodeId, (edgeId, edgeAttr, source, target) => {
966
+ if (relation.type && edgeAttr.type !== relation.type) return;
967
+ const neighborId = nodeId === source ? target : source;
968
+ cb(neighborId, edgeId);
969
+ });
970
+ };
971
+ }
972
+ // ── 2. WITH & IMPLICIT GROUPING AGGREGATIONS STAGE ─────────────────────────
973
+ // Optimisations applied:
974
+ // #4 Context chains throughout, materialised only for grouping
975
+ // #5 Single-pass aggregation (all agg types computed in one row scan)
976
+ executeWith(clause, contexts) {
977
+ const keysSimple = clause.projections.filter((p) => p.expression.type !== "Aggregation");
978
+ const keysAggr = clause.projections.filter((p) => p.expression.type === "Aggregation");
979
+ const groups = /* @__PURE__ */ new Map();
980
+ const materialised = contexts.map((c) => materialiseChain(c));
981
+ for (const context of materialised) {
982
+ const groupKeyObj = {};
983
+ keysSimple.forEach((p) => {
984
+ groupKeyObj[p.alias] = this.evaluateExpression(p.expression, context);
985
+ });
986
+ const sortedKeys = Object.keys(groupKeyObj).sort();
987
+ const groupKeyStr = sortedKeys.map((k) => JSON.stringify([k, groupKeyObj[k]])).join(",");
988
+ if (!groups.has(groupKeyStr)) {
989
+ groups.set(groupKeyStr, { simpleValues: groupKeyObj, rows: [] });
990
+ }
991
+ groups.get(groupKeyStr).rows.push(context);
992
+ }
993
+ let newContexts = [];
994
+ groups.forEach(({ simpleValues, rows }) => {
995
+ newContexts.push(this.computeAggregations(simpleValues, rows, keysAggr));
996
+ });
997
+ if (clause.where) {
998
+ newContexts = newContexts.filter((ctx) => this.evaluateWhere(clause.where, ctx));
999
+ }
1000
+ if (clause.orderBy && clause.orderBy.length > 0) {
1001
+ newContexts = this.applyOrderByToContexts(newContexts, clause.orderBy);
1002
+ }
1003
+ if (clause.skip !== void 0 && clause.skip !== null) {
1004
+ newContexts = newContexts.slice(clause.skip);
1005
+ }
1006
+ if (clause.limit !== void 0 && clause.limit !== null) {
1007
+ newContexts = newContexts.slice(0, clause.limit);
1008
+ }
1009
+ return newContexts;
1010
+ }
1011
+ // ── Shared aggregation logic (optimisation #5) ─────────────────────────────
1012
+ // Single-pass: compute COUNT, SUM, AVG, MIN, MAX for all aggregation
1013
+ // variables in one row scan. Used by both executeWith and executeReturn.
1014
+ computeAggregations(baseContext, rows, aggrProjections) {
1015
+ const newContext = { ...baseContext };
1016
+ const aggVars = /* @__PURE__ */ new Map();
1017
+ aggrProjections.forEach((p) => {
1018
+ if (p.expression.type === "Aggregation") {
1019
+ const key = `${p.expression.variable}:${p.expression.property ?? ""}`;
1020
+ aggVars.set(key, p.expression);
1021
+ }
1022
+ });
1023
+ const numericCache = /* @__PURE__ */ new Map();
1024
+ const nonNullCache = /* @__PURE__ */ new Map();
1025
+ for (const row of rows) {
1026
+ for (const expr of aggVars.values()) {
1027
+ const key = `${expr.variable}:${expr.property ?? ""}`;
1028
+ const baseVal = row[expr.variable];
1029
+ const val = expr.property ? baseVal?.[expr.property] : baseVal;
1030
+ if (val !== null && val !== void 0) {
1031
+ nonNullCache.set(key, (nonNullCache.get(key) ?? 0) + 1);
1032
+ }
1033
+ if (typeof val === "number") {
1034
+ if (!numericCache.has(key)) numericCache.set(key, []);
1035
+ const arr = numericCache.get(key);
1036
+ if (arr) arr.push(val);
1037
+ }
1038
+ }
1039
+ }
1040
+ aggrProjections.forEach((p) => {
1041
+ const expr = p.expression;
1042
+ if (expr.type !== "Aggregation") return;
1043
+ const key = `${expr.variable}:${expr.property ?? ""}`;
1044
+ const numericValues = numericCache.get(key) ?? [];
1045
+ const nonNullCount = nonNullCache.get(key) ?? 0;
1046
+ if (expr.aggregationType === "COUNT") {
1047
+ newContext[p.alias] = nonNullCount;
1048
+ } else if (expr.aggregationType === "SUM") {
1049
+ newContext[p.alias] = numericValues.reduce((a, b) => a + b, 0);
1050
+ } else if (expr.aggregationType === "AVG") {
1051
+ newContext[p.alias] = numericValues.length > 0 ? numericValues.reduce((a, b) => a + b, 0) / numericValues.length : null;
1052
+ } else if (expr.aggregationType === "MIN") {
1053
+ newContext[p.alias] = numericValues.length > 0 ? Math.min(...numericValues) : null;
1054
+ } else if (expr.aggregationType === "MAX") {
1055
+ newContext[p.alias] = numericValues.length > 0 ? Math.max(...numericValues) : null;
1056
+ }
1057
+ });
1058
+ return newContext;
1059
+ }
1060
+ // ── 3. WRITE MUTATIONS STAGE (CREATE, SET, DELETE) ─────────────────────────
1061
+ // NOTE: indexes are invalidated after mutations so subsequent MATCH/WITH
1062
+ // stages fall back to full-graph scan and see the updated graph state.
1063
+ executeWrite(clause, contexts) {
1064
+ for (let i = 0; i < contexts.length; i++) {
1065
+ const ctx = contexts[i];
1066
+ if (ctx && isContextChain(ctx)) {
1067
+ contexts[i] = materialiseChain(ctx);
1068
+ }
1069
+ }
1070
+ const materialised = contexts;
1071
+ if (clause.type === "CREATE") {
1072
+ const newId = randomUUID();
1073
+ this.graph.addNode(newId, { label: clause.label, ...clause.properties });
1074
+ const newNode = { id: newId, label: clause.label, ...clause.properties };
1075
+ for (const context of materialised) {
1076
+ context[clause.variable] = newNode;
1077
+ }
1078
+ } else if (clause.type === "SET") {
1079
+ const nodeIds = /* @__PURE__ */ new Set();
1080
+ for (const context of materialised) {
1081
+ const targetNode = context[clause.variable];
1082
+ if (targetNode && targetNode.id) nodeIds.add(targetNode.id);
1083
+ }
1084
+ for (const nodeId of nodeIds) {
1085
+ this.graph.setNodeAttribute(nodeId, clause.property, clause.value);
1086
+ }
1087
+ for (const context of materialised) {
1088
+ const targetNode = context[clause.variable];
1089
+ if (targetNode && targetNode.id && nodeIds.has(targetNode.id)) {
1090
+ const fresh = { id: targetNode.id, ...this.graph.getNodeAttributes(targetNode.id) };
1091
+ context[clause.variable] = fresh;
1092
+ }
1093
+ }
1094
+ } else if (clause.type === "DELETE") {
1095
+ const nodeIds = /* @__PURE__ */ new Set();
1096
+ for (const context of materialised) {
1097
+ const targetNode = context[clause.variable];
1098
+ if (targetNode && targetNode.id && this.graph.hasNode(targetNode.id)) {
1099
+ nodeIds.add(targetNode.id);
1100
+ }
1101
+ }
1102
+ for (const nodeId of nodeIds) {
1103
+ this.graph.dropNode(nodeId);
1104
+ }
1105
+ for (const context of materialised) {
1106
+ const targetNode = context[clause.variable];
1107
+ if (targetNode && targetNode.id && nodeIds.has(targetNode.id)) {
1108
+ context[clause.variable] = null;
1109
+ }
1110
+ }
1111
+ }
1112
+ this.indexes = void 0;
1113
+ }
1114
+ // ── 4. RETURN PROJECTION STAGE ─────────────────────────────────────────────
1115
+ // Optimisations applied:
1116
+ // #5 Single-pass aggregation
1117
+ // #11 Pre-computed sort keys (Schwartzian transform)
1118
+ executeReturn(clause, contexts) {
1119
+ const keysSimple = clause.projections.filter((p) => p.expression.type !== "Aggregation");
1120
+ const keysAggr = clause.projections.filter((p) => p.expression.type === "Aggregation");
1121
+ let results;
1122
+ if (keysAggr.length > 0) {
1123
+ const materialised = contexts.map((c) => materialiseChain(c));
1124
+ const result = {};
1125
+ keysSimple.forEach((p) => {
1126
+ const values = materialised.map((ctx) => this.evaluateExpression(p.expression, ctx));
1127
+ const uniqueValues = new Set(values.map((v) => JSON.stringify(v)));
1128
+ if (uniqueValues.size > 1) {
1129
+ throw new Error(
1130
+ `Mixed aggregation and non-aggregation in RETURN without WITH: "${p.alias}" has different values across rows. Use a WITH clause to group first.`
1131
+ );
1132
+ }
1133
+ result[p.alias] = values[0];
1134
+ });
1135
+ const aggResult = this.computeAggregations(result, materialised, keysAggr);
1136
+ results = [aggResult];
1137
+ } else {
1138
+ const materialised = contexts.map((c) => materialiseChain(c));
1139
+ let workingContexts = materialised;
1140
+ if (clause.orderBy && clause.orderBy.length > 0) {
1141
+ workingContexts = this.applyOrderByToContexts(workingContexts, clause.orderBy);
1142
+ }
1143
+ if (clause.skip !== void 0 && clause.skip !== null) {
1144
+ workingContexts = workingContexts.slice(clause.skip);
1145
+ }
1146
+ if (clause.limit !== void 0 && clause.limit !== null) {
1147
+ workingContexts = workingContexts.slice(0, clause.limit);
1148
+ }
1149
+ results = workingContexts.map((context) => {
1150
+ const res = {};
1151
+ clause.projections.forEach((p) => {
1152
+ res[p.alias] = this.evaluateExpression(p.expression, context);
1153
+ });
1154
+ return res;
1155
+ });
1156
+ }
1157
+ return results;
1158
+ }
1159
+ // ── Expression evaluation ──────────────────────────────────────────────────
1160
+ evaluateExpression(expr, context) {
1161
+ if (expr.type === "PropertyAccess") {
1162
+ const obj = context[expr.variable];
1163
+ if (obj === void 0) return void 0;
1164
+ if (obj === null) return null;
1165
+ if (expr.property) return obj[expr.property];
1166
+ return obj;
1167
+ }
1168
+ if (expr.type === "Literal") return expr.value;
1169
+ if (expr.type === "Aggregation") return void 0;
1170
+ return void 0;
1171
+ }
1172
+ evaluateWhere(whereNode, context) {
1173
+ const leftValue = this.evaluateExpression(whereNode.left, context);
1174
+ const rightValue = this.evaluateExpression(whereNode.right, context);
1175
+ switch (whereNode.operator) {
1176
+ case ">":
1177
+ if (typeof leftValue !== "number" || typeof rightValue !== "number") {
1178
+ throw new Error(`WHERE comparison "${whereNode.operator}" requires numeric values, got ${JSON.stringify(leftValue)} and ${JSON.stringify(rightValue)}`);
1179
+ }
1180
+ return leftValue > rightValue;
1181
+ case "<":
1182
+ if (typeof leftValue !== "number" || typeof rightValue !== "number") {
1183
+ throw new Error(`WHERE comparison "${whereNode.operator}" requires numeric values, got ${JSON.stringify(leftValue)} and ${JSON.stringify(rightValue)}`);
1184
+ }
1185
+ return leftValue < rightValue;
1186
+ case "=":
1187
+ return leftValue === rightValue;
1188
+ case "CONTAINS":
1189
+ return String(leftValue).includes(String(rightValue));
1190
+ default:
1191
+ return false;
1192
+ }
1193
+ }
1194
+ // ── ORDER BY with pre-computed sort keys (optimisation #11) ────────────────
1195
+ // Schwartzian transform: compute sort keys once per context, then sort by keys.
1196
+ // Avoids re-evaluating expressions on every comparison (n log n → n + n log n key compares).
1197
+ applyOrderByToContexts(contexts, orderBy) {
1198
+ const keyed = contexts.map((ctx) => ({
1199
+ ctx,
1200
+ keys: orderBy.map((item) => this.evaluateExpression(item.expression, ctx))
1201
+ }));
1202
+ keyed.sort((a, b) => {
1203
+ for (let i = 0; i < orderBy.length; i++) {
1204
+ const cmp = this.compareValues(a.keys[i], b.keys[i]);
1205
+ const item = orderBy[i];
1206
+ if (cmp !== 0 && item) return item.direction === "DESC" ? -cmp : cmp;
1207
+ }
1208
+ return 0;
1209
+ });
1210
+ return keyed.map((k) => k.ctx);
1211
+ }
1212
+ /**
1213
+ * Compare two values for sorting. Handles nulls, numbers, strings, booleans.
1214
+ * null < boolean < number < string < object
1215
+ * Mixed-type coercion differs from Neo4j (which throws). Here we stringify
1216
+ * for pragmatic compatibility in exploratory queries.
1217
+ */
1218
+ compareValues(a, b) {
1219
+ if (a === null || a === void 0) {
1220
+ if (b === null || b === void 0) return 0;
1221
+ return -1;
1222
+ }
1223
+ if (b === null || b === void 0) return 1;
1224
+ if (typeof a === "number" && typeof b === "number") return a - b;
1225
+ if (typeof a === "string" && typeof b === "string") return a < b ? -1 : a > b ? 1 : 0;
1226
+ if (typeof a === "boolean" && typeof b === "boolean") return a === b ? 0 : a ? -1 : 1;
1227
+ const aStr = String(a);
1228
+ const bStr = String(b);
1229
+ return aStr < bStr ? -1 : aStr > bStr ? 1 : 0;
1230
+ }
1231
+ matchNodeCriteria(nodeAttr, pattern) {
1232
+ if (pattern.label !== void 0 && nodeAttr.label !== pattern.label) return false;
1233
+ const props = pattern.properties;
1234
+ if (props) {
1235
+ return Object.keys(props).every((k) => nodeAttr[k] === props[k]);
1236
+ }
1237
+ return true;
1238
+ }
1239
+ };
1240
+
1241
+ // src/indexes.ts
1242
+ function buildIndexes(nodeIterator, graph) {
1243
+ const labelIndex = /* @__PURE__ */ new Map();
1244
+ const propertyIndex = /* @__PURE__ */ new Map();
1245
+ const edgeOut = /* @__PURE__ */ new Map();
1246
+ const edgeIn = /* @__PURE__ */ new Map();
1247
+ for (const [id, attrs] of nodeIterator) {
1248
+ const label = attrs.label;
1249
+ if (label && typeof label === "string") {
1250
+ let labelSet = labelIndex.get(label);
1251
+ if (!labelSet) {
1252
+ labelSet = /* @__PURE__ */ new Set();
1253
+ labelIndex.set(label, labelSet);
1254
+ }
1255
+ labelSet.add(id);
1256
+ }
1257
+ for (const [key, value] of Object.entries(attrs)) {
1258
+ if (key === "id" || key === "label") continue;
1259
+ if (value === null || value === void 0 || typeof value === "object") continue;
1260
+ let valMap = propertyIndex.get(key);
1261
+ if (!valMap) {
1262
+ valMap = /* @__PURE__ */ new Map();
1263
+ propertyIndex.set(key, valMap);
1264
+ }
1265
+ const valKey = String(value);
1266
+ let nodeSet = valMap.get(valKey);
1267
+ if (!nodeSet) {
1268
+ nodeSet = /* @__PURE__ */ new Set();
1269
+ valMap.set(valKey, nodeSet);
1270
+ }
1271
+ nodeSet.add(id);
1272
+ }
1273
+ }
1274
+ graph.forEachEdge((edgeId, attrs, source, target) => {
1275
+ const edgeType = attrs.type && typeof attrs.type === "string" ? attrs.type : "__UNTYPED__";
1276
+ let outMap = edgeOut.get(edgeType);
1277
+ if (!outMap) {
1278
+ outMap = /* @__PURE__ */ new Map();
1279
+ edgeOut.set(edgeType, outMap);
1280
+ }
1281
+ let outList = outMap.get(source);
1282
+ if (!outList) {
1283
+ outList = [];
1284
+ outMap.set(source, outList);
1285
+ }
1286
+ outList.push({ target, edgeId });
1287
+ let inMap = edgeIn.get(edgeType);
1288
+ if (!inMap) {
1289
+ inMap = /* @__PURE__ */ new Map();
1290
+ edgeIn.set(edgeType, inMap);
1291
+ }
1292
+ let inList = inMap.get(target);
1293
+ if (!inList) {
1294
+ inList = [];
1295
+ inMap.set(target, inList);
1296
+ }
1297
+ inList.push({ source, edgeId });
1298
+ });
1299
+ return {
1300
+ labelIndex,
1301
+ propertyIndex,
1302
+ edgeTypeIndex: { out: edgeOut, in: edgeIn }
1303
+ };
1304
+ }
1305
+ function buildGraphIndexesFromGraph(graph) {
1306
+ const allNodes = graph.filterNodes(() => true);
1307
+ const nodeEntries = allNodes.map((id) => [id, graph.getNodeAttributes(id)]);
1308
+ return buildIndexes(nodeEntries, graph);
1309
+ }
1310
+ function buildGraphIndexesFromData(data, graph) {
1311
+ const nodeEntries = data.nodes.map((node) => [node.id, node]);
1312
+ return buildIndexes(nodeEntries, graph);
1313
+ }
1314
+
1315
+ // src/graph.ts
1316
+ import GraphModule from "graphology";
1317
+ var Graph = GraphModule;
1318
+ function assertGraphApi(graph) {
1319
+ const requiredMethods = [
1320
+ "addNode",
1321
+ "addEdge",
1322
+ "getNodeAttributes",
1323
+ "getEdgeAttributes",
1324
+ "filterNodes",
1325
+ "forEachOutboundEdge",
1326
+ "forEachInboundEdge",
1327
+ "forEachEdge",
1328
+ "setNodeAttribute",
1329
+ "hasNode",
1330
+ "dropNode"
1331
+ ];
1332
+ for (const method of requiredMethods) {
1333
+ if (typeof graph[method] !== "function") {
1334
+ throw new Error(`Graphology API mismatch: missing method "${method}".`);
1335
+ }
1336
+ }
1337
+ }
1338
+ if (process.env.NODE_ENV === "development") {
1339
+ assertGraphApi(new Graph());
1340
+ }
1341
+
1342
+ // src/lib.ts
1343
+ var GraphError = class extends Error {
1344
+ constructor(message) {
1345
+ super(message);
1346
+ this.name = "GraphError";
1347
+ }
1348
+ };
1349
+ function validateGraphData(data) {
1350
+ if (!data || typeof data !== "object") {
1351
+ throw new GraphError('Invalid graph data: expected a JSON object with "nodes" and "edges" arrays.');
1352
+ }
1353
+ const obj = data;
1354
+ if (!Array.isArray(obj.nodes)) {
1355
+ throw new GraphError('Invalid graph data: "nodes" must be an array.');
1356
+ }
1357
+ if (!Array.isArray(obj.edges)) {
1358
+ throw new GraphError('Invalid graph data: "edges" must be an array.');
1359
+ }
1360
+ const seenNodeIds = /* @__PURE__ */ new Set();
1361
+ for (let i = 0; i < obj.nodes.length; i++) {
1362
+ const node = obj.nodes[i];
1363
+ if (!node || typeof node !== "object") {
1364
+ throw new GraphError(`Invalid graph data: node at index ${i} must be an object.`);
1365
+ }
1366
+ const n = node;
1367
+ if (typeof n.id !== "string" || !n.id) {
1368
+ throw new GraphError(`Invalid graph data: node at index ${i} must have a non-empty string "id".`);
1369
+ }
1370
+ if (seenNodeIds.has(n.id)) {
1371
+ throw new GraphError(`Invalid graph data: duplicate node id "${n.id}" at index ${i}.`);
1372
+ }
1373
+ seenNodeIds.add(n.id);
1374
+ }
1375
+ const nodeIds = seenNodeIds;
1376
+ const seenEdges = /* @__PURE__ */ new Set();
1377
+ for (let i = 0; i < obj.edges.length; i++) {
1378
+ const edge = obj.edges[i];
1379
+ if (!edge || typeof edge !== "object") {
1380
+ throw new GraphError(`Invalid graph data: edge at index ${i} must be an object.`);
1381
+ }
1382
+ const e = edge;
1383
+ if (typeof e.source !== "string" || !e.source) {
1384
+ throw new GraphError(`Invalid graph data: edge at index ${i} must have a non-empty string "source".`);
1385
+ }
1386
+ if (typeof e.target !== "string" || !e.target) {
1387
+ throw new GraphError(`Invalid graph data: edge at index ${i} must have a non-empty string "target".`);
1388
+ }
1389
+ if (!nodeIds.has(e.source)) {
1390
+ throw new GraphError(`Invalid graph data: edge at index ${i} references unknown source node "${e.source}".`);
1391
+ }
1392
+ if (!nodeIds.has(e.target)) {
1393
+ throw new GraphError(`Invalid graph data: edge at index ${i} references unknown target node "${e.target}".`);
1394
+ }
1395
+ const edgeKey = `${e.source}->${e.target}`;
1396
+ if (seenEdges.has(edgeKey)) {
1397
+ throw new GraphError(`Invalid graph data: duplicate edge "${edgeKey}" at index ${i}. Graphology does not support multi-graphs.`);
1398
+ }
1399
+ seenEdges.add(edgeKey);
1400
+ }
1401
+ return {
1402
+ nodes: obj.nodes,
1403
+ edges: obj.edges
1404
+ };
1405
+ }
1406
+ function createGraph(data) {
1407
+ const validated = validateGraphData(data);
1408
+ return buildGraph(validated);
1409
+ }
1410
+ function buildGraph(validated) {
1411
+ const graph = new Graph();
1412
+ for (const node of validated.nodes) {
1413
+ const { id, ...attrs } = node;
1414
+ graph.addNode(id, attrs);
1415
+ }
1416
+ for (const edge of validated.edges) {
1417
+ const { source, target, ...attrs } = edge;
1418
+ graph.addEdge(source, target, attrs);
1419
+ }
1420
+ return graph;
1421
+ }
1422
+ function buildGraphIndexes(dataOrGraph, graph) {
1423
+ if (graph === void 0) {
1424
+ if (isGraphInstance(dataOrGraph)) {
1425
+ return buildGraphIndexesFromGraph(dataOrGraph);
1426
+ }
1427
+ const validated2 = validateGraphData(dataOrGraph);
1428
+ const builtGraph = buildGraph(validated2);
1429
+ return buildGraphIndexesFromGraph(builtGraph);
1430
+ }
1431
+ const validated = validateGraphData(dataOrGraph);
1432
+ return buildGraphIndexesFromData(validated, graph);
1433
+ }
1434
+ function parseCypher2(query) {
1435
+ return parseCypher(query);
1436
+ }
1437
+ function executeQuery(graphOrData, query) {
1438
+ const graph = isGraphInstance(graphOrData) ? graphOrData : buildGraph(validateGraphData(graphOrData));
1439
+ const indexes = buildGraphIndexesFromGraph(graph);
1440
+ const engine = new AdvancedCypherGraphologyEngine(graph, indexes);
1441
+ const ast = parseCypher(query);
1442
+ return engine.execute(ast);
1443
+ }
1444
+ function isGraphInstance(value) {
1445
+ return typeof value.hasNode === "function" && typeof value.filterNodes === "function" && typeof value.forEachEdge === "function";
1446
+ }
1447
+ export {
1448
+ Graph,
1449
+ AdvancedCypherGraphologyEngine as GraphEngine,
1450
+ GraphError,
1451
+ buildGraphIndexes,
1452
+ createGraph,
1453
+ executeQuery,
1454
+ parseCypher2 as parseCypher
1455
+ };