leangraph 1.1.2 → 1.1.3
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/db.d.ts.map +1 -1
- package/dist/db.js +42 -3
- package/dist/db.js.map +1 -1
- package/dist/engine/hybrid-executor.d.ts +118 -0
- package/dist/engine/hybrid-executor.d.ts.map +1 -0
- package/dist/engine/hybrid-executor.js +205 -0
- package/dist/engine/hybrid-executor.js.map +1 -0
- package/dist/engine/index.d.ts +36 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +34 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/memory-graph.d.ts +68 -0
- package/dist/engine/memory-graph.d.ts.map +1 -0
- package/dist/engine/memory-graph.js +176 -0
- package/dist/engine/memory-graph.js.map +1 -0
- package/dist/engine/query-planner.d.ts +62 -0
- package/dist/engine/query-planner.d.ts.map +1 -0
- package/dist/engine/query-planner.js +481 -0
- package/dist/engine/query-planner.js.map +1 -0
- package/dist/engine/subgraph-loader.d.ts +41 -0
- package/dist/engine/subgraph-loader.d.ts.map +1 -0
- package/dist/engine/subgraph-loader.js +172 -0
- package/dist/engine/subgraph-loader.js.map +1 -0
- package/dist/executor.d.ts +17 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +286 -100
- package/dist/executor.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +47 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +228 -41
- package/dist/parser.js.map +1 -1
- package/dist/translator.d.ts +53 -0
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js +1545 -186
- package/dist/translator.js.map +1 -1
- package/package.json +9 -3
package/dist/executor.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Query Executor - Full pipeline: Cypher → Parse → Translate → Execute → Format
|
|
2
2
|
import { parse, } from "./parser.js";
|
|
3
3
|
import { Translator } from "./translator.js";
|
|
4
|
+
import { HybridExecutor } from "./engine/hybrid-executor.js";
|
|
5
|
+
import { analyzeForHybrid, isHybridCompatiblePattern } from "./engine/query-planner.js";
|
|
4
6
|
// ============================================================================
|
|
5
7
|
// Timezone Helpers
|
|
6
8
|
// ============================================================================
|
|
@@ -80,8 +82,11 @@ export class Executor {
|
|
|
80
82
|
edgePropertyCache = new Map();
|
|
81
83
|
// Cache for full edge info (type, source_id, target_id) - populated by batchGetEdgeInfo
|
|
82
84
|
edgeInfoCache = new Map();
|
|
85
|
+
// Hybrid executor for var-length pattern queries
|
|
86
|
+
hybridExecutor;
|
|
83
87
|
constructor(db) {
|
|
84
88
|
this.db = db;
|
|
89
|
+
this.hybridExecutor = new HybridExecutor(db);
|
|
85
90
|
}
|
|
86
91
|
/**
|
|
87
92
|
* Get node properties from cache or parse from JSON string and cache them
|
|
@@ -222,70 +227,80 @@ export class Executor {
|
|
|
222
227
|
},
|
|
223
228
|
};
|
|
224
229
|
};
|
|
230
|
+
// EXPLAIN and PROFILE queries should go directly to SQL translation
|
|
231
|
+
// Skip special execution paths for these
|
|
232
|
+
const isExplainOrProfile = parseResult.query.explain || parseResult.query.profile;
|
|
225
233
|
// Dispatch based on pattern (each try* method still validates and may return null)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
234
|
+
if (!isExplainOrProfile)
|
|
235
|
+
switch (pattern) {
|
|
236
|
+
case "PHASED": {
|
|
237
|
+
const result = this.tryPhasedExecution(parseResult.query, params);
|
|
238
|
+
if (result !== null)
|
|
239
|
+
return makeResult(result);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "UNWIND_CREATE": {
|
|
243
|
+
const result = this.tryUnwindCreateExecution(parseResult.query, params);
|
|
244
|
+
if (result !== null)
|
|
245
|
+
return makeResult(result);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case "UNWIND_MERGE": {
|
|
249
|
+
const result = this.tryUnwindMergeExecution(parseResult.query, params);
|
|
250
|
+
if (result !== null)
|
|
251
|
+
return makeResult(result);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case "COLLECT_UNWIND": {
|
|
255
|
+
const result = this.tryCollectUnwindExecution(parseResult.query, params);
|
|
256
|
+
if (result !== null)
|
|
257
|
+
return makeResult(result);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "COLLECT_DELETE": {
|
|
261
|
+
const result = this.tryCollectDeleteExecution(parseResult.query, params);
|
|
262
|
+
if (result !== null)
|
|
263
|
+
return makeResult(result);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case "CREATE_RETURN": {
|
|
267
|
+
const result = this.tryCreateReturnExecution(parseResult.query, params);
|
|
268
|
+
if (result !== null)
|
|
269
|
+
return makeResult(result);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case "BOUND_REL_LIST": {
|
|
273
|
+
const result = this.tryBoundRelationshipListExecution(parseResult.query, params);
|
|
274
|
+
if (result !== null)
|
|
275
|
+
return makeResult(result);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "MERGE": {
|
|
279
|
+
const result = this.tryMergeExecution(parseResult.query, params);
|
|
280
|
+
if (result !== null)
|
|
281
|
+
return makeResult(result);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "MULTI_PHASE": {
|
|
285
|
+
const result = this.tryMultiPhaseExecution(parseResult.query, params);
|
|
286
|
+
if (result !== null)
|
|
287
|
+
return makeResult(result);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "FOREACH": {
|
|
291
|
+
const result = this.tryForeachExecution(parseResult.query, params);
|
|
292
|
+
if (result !== null)
|
|
293
|
+
return makeResult(result);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "HYBRID_VAR_LENGTH": {
|
|
297
|
+
const result = this.tryHybridVarLengthExecution(parseResult.query, params);
|
|
298
|
+
if (result !== null)
|
|
299
|
+
return makeResult(result);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
// STANDARD falls through to SQL translation below
|
|
286
303
|
}
|
|
287
|
-
// STANDARD falls through to SQL translation below
|
|
288
|
-
}
|
|
289
304
|
// 3. Standard single-phase execution: Translate to SQL
|
|
290
305
|
const translator = new Translator(params);
|
|
291
306
|
const translation = translator.translate(parseResult.query);
|
|
@@ -295,8 +310,9 @@ export class Executor {
|
|
|
295
310
|
this.db.transaction(() => {
|
|
296
311
|
for (const stmt of translation.statements) {
|
|
297
312
|
const result = this.db.execute(stmt.sql, stmt.params);
|
|
298
|
-
// If this is a SELECT (RETURN clause), capture the results
|
|
299
|
-
|
|
313
|
+
// If this is a SELECT or EXPLAIN (RETURN clause), capture the results
|
|
314
|
+
const sqlUpper = stmt.sql.trim().toUpperCase();
|
|
315
|
+
if (result.rows.length > 0 || sqlUpper.startsWith("SELECT") || sqlUpper.startsWith("EXPLAIN")) {
|
|
300
316
|
rows = result.rows;
|
|
301
317
|
}
|
|
302
318
|
}
|
|
@@ -559,7 +575,11 @@ export class Executor {
|
|
|
559
575
|
}
|
|
560
576
|
}
|
|
561
577
|
}
|
|
562
|
-
// 11.
|
|
578
|
+
// 11. HYBRID_VAR_LENGTH - Var-length pattern suitable for hybrid execution
|
|
579
|
+
if (isHybridCompatiblePattern(query)) {
|
|
580
|
+
return "HYBRID_VAR_LENGTH";
|
|
581
|
+
}
|
|
582
|
+
// 12. STANDARD - Default SQL translation
|
|
563
583
|
return "STANDARD";
|
|
564
584
|
}
|
|
565
585
|
/**
|
|
@@ -1976,7 +1996,7 @@ export class Executor {
|
|
|
1976
1996
|
executeCreateNodeInContext(pattern, rowContext, globalContext, params) {
|
|
1977
1997
|
const id = crypto.randomUUID();
|
|
1978
1998
|
const labelJson = this.normalizeLabelToJson(pattern.label);
|
|
1979
|
-
const props = this.
|
|
1999
|
+
const props = this.resolveNodePatternProperties(pattern, params, rowContext);
|
|
1980
2000
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [id, labelJson, JSON.stringify(props)]);
|
|
1981
2001
|
if (pattern.variable) {
|
|
1982
2002
|
// Store as a node object with _nf_id for consistency with MATCH output
|
|
@@ -2031,7 +2051,7 @@ export class Executor {
|
|
|
2031
2051
|
else {
|
|
2032
2052
|
// Create new source node
|
|
2033
2053
|
sourceId = crypto.randomUUID();
|
|
2034
|
-
const props = this.
|
|
2054
|
+
const props = this.resolveNodePatternProperties(pattern.source, params, rowContext);
|
|
2035
2055
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [sourceId, this.normalizeLabelToJson(pattern.source.label), JSON.stringify(props)]);
|
|
2036
2056
|
if (pattern.source.variable) {
|
|
2037
2057
|
rowContext.set(pattern.source.variable, sourceId);
|
|
@@ -2052,7 +2072,7 @@ export class Executor {
|
|
|
2052
2072
|
else {
|
|
2053
2073
|
// Create new target node
|
|
2054
2074
|
targetId = crypto.randomUUID();
|
|
2055
|
-
const props = this.
|
|
2075
|
+
const props = this.resolveNodePatternProperties(pattern.target, params, rowContext);
|
|
2056
2076
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [targetId, this.normalizeLabelToJson(pattern.target.label), JSON.stringify(props)]);
|
|
2057
2077
|
if (pattern.target.variable) {
|
|
2058
2078
|
rowContext.set(pattern.target.variable, targetId);
|
|
@@ -3830,7 +3850,13 @@ export class Executor {
|
|
|
3830
3850
|
case "+": return leftNum + rightNum;
|
|
3831
3851
|
case "-": return leftNum - rightNum;
|
|
3832
3852
|
case "*": return leftNum * rightNum;
|
|
3833
|
-
case "/":
|
|
3853
|
+
case "/": {
|
|
3854
|
+
// Cypher integer division: if both operands are integers, truncate toward zero
|
|
3855
|
+
if (Number.isInteger(leftNum) && Number.isInteger(rightNum)) {
|
|
3856
|
+
return Math.trunc(leftNum / rightNum);
|
|
3857
|
+
}
|
|
3858
|
+
return leftNum / rightNum;
|
|
3859
|
+
}
|
|
3834
3860
|
case "%": return leftNum % rightNum;
|
|
3835
3861
|
case "^": return Math.pow(leftNum, rightNum);
|
|
3836
3862
|
default: return null;
|
|
@@ -3864,7 +3890,13 @@ export class Executor {
|
|
|
3864
3890
|
case "+": return leftNum + rightNum;
|
|
3865
3891
|
case "-": return leftNum - rightNum;
|
|
3866
3892
|
case "*": return leftNum * rightNum;
|
|
3867
|
-
case "/":
|
|
3893
|
+
case "/": {
|
|
3894
|
+
// Cypher integer division: if both operands are integers, truncate toward zero
|
|
3895
|
+
if (Number.isInteger(leftNum) && Number.isInteger(rightNum)) {
|
|
3896
|
+
return Math.trunc(leftNum / rightNum);
|
|
3897
|
+
}
|
|
3898
|
+
return leftNum / rightNum;
|
|
3899
|
+
}
|
|
3868
3900
|
case "%": return leftNum % rightNum;
|
|
3869
3901
|
case "^": return Math.pow(leftNum, rightNum);
|
|
3870
3902
|
default: return null;
|
|
@@ -4548,12 +4580,20 @@ export class Executor {
|
|
|
4548
4580
|
else if (expr.type === "binary") {
|
|
4549
4581
|
const left = this.evaluateExpressionWithPropertyAliases(expr.left, resolvedIds, capturedPropertyValues, propertyAliasMap, params);
|
|
4550
4582
|
const right = this.evaluateExpressionWithPropertyAliases(expr.right, resolvedIds, capturedPropertyValues, propertyAliasMap, params);
|
|
4583
|
+
const leftNum = left;
|
|
4584
|
+
const rightNum = right;
|
|
4551
4585
|
switch (expr.operator) {
|
|
4552
|
-
case "+": return
|
|
4553
|
-
case "-": return
|
|
4554
|
-
case "*": return
|
|
4555
|
-
case "/":
|
|
4556
|
-
|
|
4586
|
+
case "+": return leftNum + rightNum;
|
|
4587
|
+
case "-": return leftNum - rightNum;
|
|
4588
|
+
case "*": return leftNum * rightNum;
|
|
4589
|
+
case "/": {
|
|
4590
|
+
// Cypher integer division: if both operands are integers, truncate toward zero
|
|
4591
|
+
if (Number.isInteger(leftNum) && Number.isInteger(rightNum)) {
|
|
4592
|
+
return Math.trunc(leftNum / rightNum);
|
|
4593
|
+
}
|
|
4594
|
+
return leftNum / rightNum;
|
|
4595
|
+
}
|
|
4596
|
+
case "%": return leftNum % rightNum;
|
|
4557
4597
|
default: return null;
|
|
4558
4598
|
}
|
|
4559
4599
|
}
|
|
@@ -4593,12 +4633,20 @@ export class Executor {
|
|
|
4593
4633
|
else if (expr.type === "binary") {
|
|
4594
4634
|
const left = this.evaluateExpressionForFilter(expr.left, createdIds, params);
|
|
4595
4635
|
const right = this.evaluateExpressionForFilter(expr.right, createdIds, params);
|
|
4636
|
+
const leftNum = left;
|
|
4637
|
+
const rightNum = right;
|
|
4596
4638
|
switch (expr.operator) {
|
|
4597
|
-
case "+": return
|
|
4598
|
-
case "-": return
|
|
4599
|
-
case "*": return
|
|
4600
|
-
case "/":
|
|
4601
|
-
|
|
4639
|
+
case "+": return leftNum + rightNum;
|
|
4640
|
+
case "-": return leftNum - rightNum;
|
|
4641
|
+
case "*": return leftNum * rightNum;
|
|
4642
|
+
case "/": {
|
|
4643
|
+
// Cypher integer division: if both operands are integers, truncate toward zero
|
|
4644
|
+
if (Number.isInteger(leftNum) && Number.isInteger(rightNum)) {
|
|
4645
|
+
return Math.trunc(leftNum / rightNum);
|
|
4646
|
+
}
|
|
4647
|
+
return leftNum / rightNum;
|
|
4648
|
+
}
|
|
4649
|
+
case "%": return leftNum % rightNum;
|
|
4602
4650
|
default: return null;
|
|
4603
4651
|
}
|
|
4604
4652
|
}
|
|
@@ -5276,7 +5324,13 @@ export class Executor {
|
|
|
5276
5324
|
case "+": return left + right;
|
|
5277
5325
|
case "-": return left - right;
|
|
5278
5326
|
case "*": return left * right;
|
|
5279
|
-
case "/":
|
|
5327
|
+
case "/": {
|
|
5328
|
+
// Cypher integer division: if both operands are integers, truncate toward zero
|
|
5329
|
+
if (Number.isInteger(left) && Number.isInteger(right)) {
|
|
5330
|
+
return Math.trunc(left / right);
|
|
5331
|
+
}
|
|
5332
|
+
return left / right;
|
|
5333
|
+
}
|
|
5280
5334
|
case "%": return left % right;
|
|
5281
5335
|
case "^": return Math.pow(left, right);
|
|
5282
5336
|
default: throw new Error(`Unsupported operator: ${expr.operator}`);
|
|
@@ -5302,11 +5356,16 @@ export class Executor {
|
|
|
5302
5356
|
}
|
|
5303
5357
|
/**
|
|
5304
5358
|
* Resolve properties, including unwind variable references and binary expressions
|
|
5359
|
+
* Note: In Cypher, setting a property to null means the property is not stored.
|
|
5305
5360
|
*/
|
|
5306
5361
|
resolvePropertiesWithUnwind(props, params, unwindContext) {
|
|
5307
5362
|
const resolved = {};
|
|
5308
5363
|
for (const [key, value] of Object.entries(props)) {
|
|
5309
|
-
|
|
5364
|
+
const resolvedValue = this.resolvePropertyValueWithUnwind(value, params, unwindContext);
|
|
5365
|
+
// In Cypher, setting a property to null means the property is not stored
|
|
5366
|
+
if (resolvedValue !== null) {
|
|
5367
|
+
resolved[key] = resolvedValue;
|
|
5368
|
+
}
|
|
5310
5369
|
}
|
|
5311
5370
|
return resolved;
|
|
5312
5371
|
}
|
|
@@ -5347,7 +5406,13 @@ export class Executor {
|
|
|
5347
5406
|
case "+": return leftNum + rightNum;
|
|
5348
5407
|
case "-": return leftNum - rightNum;
|
|
5349
5408
|
case "*": return leftNum * rightNum;
|
|
5350
|
-
case "/":
|
|
5409
|
+
case "/": {
|
|
5410
|
+
// Cypher integer division: if both operands are integers, truncate toward zero
|
|
5411
|
+
if (Number.isInteger(leftNum) && Number.isInteger(rightNum)) {
|
|
5412
|
+
return Math.trunc(leftNum / rightNum);
|
|
5413
|
+
}
|
|
5414
|
+
return leftNum / rightNum;
|
|
5415
|
+
}
|
|
5351
5416
|
case "%": return leftNum % rightNum;
|
|
5352
5417
|
case "^": return Math.pow(leftNum, rightNum);
|
|
5353
5418
|
default: return null;
|
|
@@ -5741,7 +5806,7 @@ export class Executor {
|
|
|
5741
5806
|
// Handle node pattern
|
|
5742
5807
|
const id = crypto.randomUUID();
|
|
5743
5808
|
const labelJson = this.normalizeLabelToJson(pattern.label);
|
|
5744
|
-
const props = this.
|
|
5809
|
+
const props = this.resolveNodePatternProperties(pattern, params);
|
|
5745
5810
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [id, labelJson, JSON.stringify(props)]);
|
|
5746
5811
|
if (pattern.variable) {
|
|
5747
5812
|
createdIds.set(pattern.variable, id);
|
|
@@ -5769,12 +5834,29 @@ export class Executor {
|
|
|
5769
5834
|
const variable = item.expression.variable;
|
|
5770
5835
|
const id = createdIds.get(variable);
|
|
5771
5836
|
if (id) {
|
|
5772
|
-
//
|
|
5837
|
+
// Try nodes first
|
|
5773
5838
|
const nodeResult = this.db.execute("SELECT id, label, properties FROM nodes WHERE id = ?", [id]);
|
|
5774
5839
|
if (nodeResult.rows.length > 0) {
|
|
5775
5840
|
const row = nodeResult.rows[0];
|
|
5776
|
-
// Neo4j 3.5 format: return properties directly
|
|
5777
|
-
|
|
5841
|
+
// Neo4j 3.5 format: return properties directly, but include _nf_id for identity
|
|
5842
|
+
const props = this.getNodeProperties(row.id, row.properties);
|
|
5843
|
+
resultRow[alias] = { ...props, _nf_id: row.id };
|
|
5844
|
+
}
|
|
5845
|
+
else {
|
|
5846
|
+
// Try edges if not found in nodes (relationship variable)
|
|
5847
|
+
const edgeResult = this.db.execute("SELECT id, type, properties FROM edges WHERE id = ?", [id]);
|
|
5848
|
+
if (edgeResult.rows.length > 0) {
|
|
5849
|
+
const row = edgeResult.rows[0];
|
|
5850
|
+
const props = typeof row.properties === "string"
|
|
5851
|
+
? JSON.parse(row.properties)
|
|
5852
|
+
: row.properties || {};
|
|
5853
|
+
// Return relationship in Neo4j format
|
|
5854
|
+
resultRow[alias] = {
|
|
5855
|
+
_type: "relationship",
|
|
5856
|
+
type: row.type,
|
|
5857
|
+
properties: props
|
|
5858
|
+
};
|
|
5859
|
+
}
|
|
5778
5860
|
}
|
|
5779
5861
|
}
|
|
5780
5862
|
}
|
|
@@ -5783,16 +5865,23 @@ export class Executor {
|
|
|
5783
5865
|
const property = item.expression.property;
|
|
5784
5866
|
const id = createdIds.get(variable);
|
|
5785
5867
|
if (id) {
|
|
5786
|
-
// Try nodes first
|
|
5787
|
-
|
|
5868
|
+
// Try nodes first - fetch full properties to preserve boolean types
|
|
5869
|
+
// (SQLite's json_extract returns 0/1 for booleans, losing type info)
|
|
5870
|
+
const nodeResult = this.db.execute(`SELECT properties FROM nodes WHERE id = ?`, [id]);
|
|
5788
5871
|
if (nodeResult.rows.length > 0) {
|
|
5789
|
-
|
|
5872
|
+
const props = typeof nodeResult.rows[0].properties === "string"
|
|
5873
|
+
? JSON.parse(nodeResult.rows[0].properties)
|
|
5874
|
+
: nodeResult.rows[0].properties || {};
|
|
5875
|
+
resultRow[alias] = props[property];
|
|
5790
5876
|
}
|
|
5791
5877
|
else {
|
|
5792
5878
|
// Try edges if not found in nodes
|
|
5793
|
-
const edgeResult = this.db.execute(`SELECT
|
|
5879
|
+
const edgeResult = this.db.execute(`SELECT properties FROM edges WHERE id = ?`, [id]);
|
|
5794
5880
|
if (edgeResult.rows.length > 0) {
|
|
5795
|
-
|
|
5881
|
+
const props = typeof edgeResult.rows[0].properties === "string"
|
|
5882
|
+
? JSON.parse(edgeResult.rows[0].properties)
|
|
5883
|
+
: edgeResult.rows[0].properties || {};
|
|
5884
|
+
resultRow[alias] = props[property];
|
|
5796
5885
|
}
|
|
5797
5886
|
}
|
|
5798
5887
|
}
|
|
@@ -7054,7 +7143,13 @@ export class Executor {
|
|
|
7054
7143
|
case "+": return leftNum + rightNum;
|
|
7055
7144
|
case "-": return leftNum - rightNum;
|
|
7056
7145
|
case "*": return leftNum * rightNum;
|
|
7057
|
-
case "/":
|
|
7146
|
+
case "/": {
|
|
7147
|
+
// Cypher integer division: if both operands are integers, truncate toward zero
|
|
7148
|
+
if (Number.isInteger(leftNum) && Number.isInteger(rightNum)) {
|
|
7149
|
+
return Math.trunc(leftNum / rightNum);
|
|
7150
|
+
}
|
|
7151
|
+
return leftNum / rightNum;
|
|
7152
|
+
}
|
|
7058
7153
|
case "%": return leftNum % rightNum;
|
|
7059
7154
|
default: return null;
|
|
7060
7155
|
}
|
|
@@ -7562,7 +7657,7 @@ export class Executor {
|
|
|
7562
7657
|
else {
|
|
7563
7658
|
// Create new source node (with or without label - anonymous nodes are valid)
|
|
7564
7659
|
sourceId = crypto.randomUUID();
|
|
7565
|
-
const props = this.
|
|
7660
|
+
const props = this.resolveNodePatternProperties(rel.source, params);
|
|
7566
7661
|
const labelJson = this.normalizeLabelToJson(rel.source.label);
|
|
7567
7662
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [sourceId, labelJson, JSON.stringify(props)]);
|
|
7568
7663
|
if (rel.source.variable) {
|
|
@@ -7580,7 +7675,7 @@ export class Executor {
|
|
|
7580
7675
|
else {
|
|
7581
7676
|
// Create new target node (with or without label - anonymous nodes are valid)
|
|
7582
7677
|
targetId = crypto.randomUUID();
|
|
7583
|
-
const props = this.
|
|
7678
|
+
const props = this.resolveNodePatternProperties(rel.target, params);
|
|
7584
7679
|
const labelJson = this.normalizeLabelToJson(rel.target.label);
|
|
7585
7680
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [targetId, labelJson, JSON.stringify(props)]);
|
|
7586
7681
|
if (rel.target.variable) {
|
|
@@ -7681,6 +7776,68 @@ export class Executor {
|
|
|
7681
7776
|
}
|
|
7682
7777
|
return [];
|
|
7683
7778
|
}
|
|
7779
|
+
/**
|
|
7780
|
+
* Try to execute a hybrid var-length pattern query using the in-memory graph engine.
|
|
7781
|
+
* Falls back to null if the query can't be executed via hybrid approach.
|
|
7782
|
+
*
|
|
7783
|
+
* Supports generalized pattern chains with N nodes and multiple var-length edges.
|
|
7784
|
+
*/
|
|
7785
|
+
tryHybridVarLengthExecution(query, params) {
|
|
7786
|
+
// Analyze the query for hybrid execution
|
|
7787
|
+
const analysis = analyzeForHybrid(query, params);
|
|
7788
|
+
if (!analysis.suitable || !analysis.params) {
|
|
7789
|
+
// Fall back to SQL execution
|
|
7790
|
+
return null;
|
|
7791
|
+
}
|
|
7792
|
+
// Execute using hybrid executor (generalized chain execution)
|
|
7793
|
+
const rawResults = this.hybridExecutor.executePatternChain(analysis.params);
|
|
7794
|
+
// Get the RETURN clause to understand what columns to output
|
|
7795
|
+
const returnClause = query.clauses.find((c) => c.type === "RETURN");
|
|
7796
|
+
if (!returnClause) {
|
|
7797
|
+
return null;
|
|
7798
|
+
}
|
|
7799
|
+
// Format results according to the RETURN clause
|
|
7800
|
+
const formattedResults = [];
|
|
7801
|
+
for (const result of rawResults) {
|
|
7802
|
+
const row = {};
|
|
7803
|
+
for (const item of returnClause.items) {
|
|
7804
|
+
const alias = item.alias || this.getReturnItemKey(item);
|
|
7805
|
+
if (item.expression.type === "property") {
|
|
7806
|
+
// Property access: a.name, b.age, etc.
|
|
7807
|
+
const varName = item.expression.variable;
|
|
7808
|
+
const propName = item.expression.property;
|
|
7809
|
+
// Look up the node by variable name in the result Map
|
|
7810
|
+
const node = varName ? result.get(varName) : undefined;
|
|
7811
|
+
if (node && propName) {
|
|
7812
|
+
row[alias] = node.properties[propName];
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7815
|
+
else if (item.expression.type === "variable") {
|
|
7816
|
+
// Full node: a, b, c
|
|
7817
|
+
const varName = item.expression.variable;
|
|
7818
|
+
// Look up the node by variable name in the result Map
|
|
7819
|
+
const node = varName ? result.get(varName) : undefined;
|
|
7820
|
+
if (node) {
|
|
7821
|
+
row[alias] = node.properties;
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7825
|
+
formattedResults.push(row);
|
|
7826
|
+
}
|
|
7827
|
+
return formattedResults;
|
|
7828
|
+
}
|
|
7829
|
+
/**
|
|
7830
|
+
* Get a key for a return item (used when no alias is provided).
|
|
7831
|
+
*/
|
|
7832
|
+
getReturnItemKey(item) {
|
|
7833
|
+
if (item.expression.type === "property") {
|
|
7834
|
+
return `${item.expression.variable}.${item.expression.property}`;
|
|
7835
|
+
}
|
|
7836
|
+
else if (item.expression.type === "variable") {
|
|
7837
|
+
return item.expression.variable || "";
|
|
7838
|
+
}
|
|
7839
|
+
return "";
|
|
7840
|
+
}
|
|
7684
7841
|
/**
|
|
7685
7842
|
* Execute a FOREACH clause within PhaseContext.
|
|
7686
7843
|
*/
|
|
@@ -8011,9 +8168,19 @@ export class Executor {
|
|
|
8011
8168
|
const referencedInSet = new Set();
|
|
8012
8169
|
for (const setClause of setClauses) {
|
|
8013
8170
|
for (const assignment of setClause.assignments) {
|
|
8014
|
-
const
|
|
8015
|
-
if (
|
|
8016
|
-
referencedInSet.add(
|
|
8171
|
+
const resolvedTarget = resolveAlias(assignment.variable);
|
|
8172
|
+
if (resolvedTarget) {
|
|
8173
|
+
referencedInSet.add(resolvedTarget);
|
|
8174
|
+
}
|
|
8175
|
+
if (assignment.value) {
|
|
8176
|
+
const valueVars = [];
|
|
8177
|
+
this.collectExpressionVariables(assignment.value, valueVars);
|
|
8178
|
+
for (const v of valueVars) {
|
|
8179
|
+
const resolvedValueVar = resolveAlias(v);
|
|
8180
|
+
if (resolvedValueVar) {
|
|
8181
|
+
referencedInSet.add(resolvedValueVar);
|
|
8182
|
+
}
|
|
8183
|
+
}
|
|
8017
8184
|
}
|
|
8018
8185
|
}
|
|
8019
8186
|
}
|
|
@@ -9276,7 +9443,7 @@ export class Executor {
|
|
|
9276
9443
|
else {
|
|
9277
9444
|
// Create new source node (with or without label - anonymous nodes are valid)
|
|
9278
9445
|
sourceId = crypto.randomUUID();
|
|
9279
|
-
const props = this.
|
|
9446
|
+
const props = this.resolveNodePatternProperties(rel.source, params);
|
|
9280
9447
|
const labelJson = this.normalizeLabelToJson(rel.source.label);
|
|
9281
9448
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [sourceId, labelJson, JSON.stringify(props)]);
|
|
9282
9449
|
// Add to resolvedIds so subsequent patterns can reference it
|
|
@@ -9295,7 +9462,7 @@ export class Executor {
|
|
|
9295
9462
|
else {
|
|
9296
9463
|
// Create new target node (with or without label - anonymous nodes are valid)
|
|
9297
9464
|
targetId = crypto.randomUUID();
|
|
9298
|
-
const props = this.
|
|
9465
|
+
const props = this.resolveNodePatternProperties(rel.target, params);
|
|
9299
9466
|
const labelJson = this.normalizeLabelToJson(rel.target.label);
|
|
9300
9467
|
this.db.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [targetId, labelJson, JSON.stringify(props)]);
|
|
9301
9468
|
// Add to resolvedIds so subsequent patterns can reference it
|
|
@@ -9315,6 +9482,25 @@ export class Executor {
|
|
|
9315
9482
|
resolvedIds[rel.edge.variable] = edgeId;
|
|
9316
9483
|
}
|
|
9317
9484
|
}
|
|
9485
|
+
/**
|
|
9486
|
+
* Resolve node properties, including (n $props) parameter maps.
|
|
9487
|
+
*/
|
|
9488
|
+
resolveNodePatternProperties(pattern, params, rowContext) {
|
|
9489
|
+
const resolved = rowContext
|
|
9490
|
+
? this.resolvePropertiesInContext(pattern.properties || {}, rowContext, params)
|
|
9491
|
+
: this.resolveProperties(pattern.properties || {}, params);
|
|
9492
|
+
if (!pattern.propertiesParam) {
|
|
9493
|
+
return resolved;
|
|
9494
|
+
}
|
|
9495
|
+
const paramValue = params[pattern.propertiesParam.name];
|
|
9496
|
+
if (paramValue === undefined || paramValue === null) {
|
|
9497
|
+
return resolved;
|
|
9498
|
+
}
|
|
9499
|
+
if (typeof paramValue !== "object" || Array.isArray(paramValue)) {
|
|
9500
|
+
throw new Error(`Expected parameter $${pattern.propertiesParam.name} to be a map`);
|
|
9501
|
+
}
|
|
9502
|
+
return { ...paramValue, ...resolved };
|
|
9503
|
+
}
|
|
9318
9504
|
/**
|
|
9319
9505
|
* Resolve parameter references and binary expressions in properties
|
|
9320
9506
|
*/
|