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.
Files changed (40) hide show
  1. package/dist/db.d.ts.map +1 -1
  2. package/dist/db.js +42 -3
  3. package/dist/db.js.map +1 -1
  4. package/dist/engine/hybrid-executor.d.ts +118 -0
  5. package/dist/engine/hybrid-executor.d.ts.map +1 -0
  6. package/dist/engine/hybrid-executor.js +205 -0
  7. package/dist/engine/hybrid-executor.js.map +1 -0
  8. package/dist/engine/index.d.ts +36 -0
  9. package/dist/engine/index.d.ts.map +1 -0
  10. package/dist/engine/index.js +34 -0
  11. package/dist/engine/index.js.map +1 -0
  12. package/dist/engine/memory-graph.d.ts +68 -0
  13. package/dist/engine/memory-graph.d.ts.map +1 -0
  14. package/dist/engine/memory-graph.js +176 -0
  15. package/dist/engine/memory-graph.js.map +1 -0
  16. package/dist/engine/query-planner.d.ts +62 -0
  17. package/dist/engine/query-planner.d.ts.map +1 -0
  18. package/dist/engine/query-planner.js +481 -0
  19. package/dist/engine/query-planner.js.map +1 -0
  20. package/dist/engine/subgraph-loader.d.ts +41 -0
  21. package/dist/engine/subgraph-loader.d.ts.map +1 -0
  22. package/dist/engine/subgraph-loader.js +172 -0
  23. package/dist/engine/subgraph-loader.js.map +1 -0
  24. package/dist/executor.d.ts +17 -0
  25. package/dist/executor.d.ts.map +1 -1
  26. package/dist/executor.js +286 -100
  27. package/dist/executor.js.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/parser.d.ts +47 -3
  33. package/dist/parser.d.ts.map +1 -1
  34. package/dist/parser.js +228 -41
  35. package/dist/parser.js.map +1 -1
  36. package/dist/translator.d.ts +53 -0
  37. package/dist/translator.d.ts.map +1 -1
  38. package/dist/translator.js +1545 -186
  39. package/dist/translator.js.map +1 -1
  40. 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
- switch (pattern) {
227
- case "PHASED": {
228
- const result = this.tryPhasedExecution(parseResult.query, params);
229
- if (result !== null)
230
- return makeResult(result);
231
- break;
232
- }
233
- case "UNWIND_CREATE": {
234
- const result = this.tryUnwindCreateExecution(parseResult.query, params);
235
- if (result !== null)
236
- return makeResult(result);
237
- break;
238
- }
239
- case "UNWIND_MERGE": {
240
- const result = this.tryUnwindMergeExecution(parseResult.query, params);
241
- if (result !== null)
242
- return makeResult(result);
243
- break;
244
- }
245
- case "COLLECT_UNWIND": {
246
- const result = this.tryCollectUnwindExecution(parseResult.query, params);
247
- if (result !== null)
248
- return makeResult(result);
249
- break;
250
- }
251
- case "COLLECT_DELETE": {
252
- const result = this.tryCollectDeleteExecution(parseResult.query, params);
253
- if (result !== null)
254
- return makeResult(result);
255
- break;
256
- }
257
- case "CREATE_RETURN": {
258
- const result = this.tryCreateReturnExecution(parseResult.query, params);
259
- if (result !== null)
260
- return makeResult(result);
261
- break;
262
- }
263
- case "BOUND_REL_LIST": {
264
- const result = this.tryBoundRelationshipListExecution(parseResult.query, params);
265
- if (result !== null)
266
- return makeResult(result);
267
- break;
268
- }
269
- case "MERGE": {
270
- const result = this.tryMergeExecution(parseResult.query, params);
271
- if (result !== null)
272
- return makeResult(result);
273
- break;
274
- }
275
- case "MULTI_PHASE": {
276
- const result = this.tryMultiPhaseExecution(parseResult.query, params);
277
- if (result !== null)
278
- return makeResult(result);
279
- break;
280
- }
281
- case "FOREACH": {
282
- const result = this.tryForeachExecution(parseResult.query, params);
283
- if (result !== null)
284
- return makeResult(result);
285
- break;
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
- if (result.rows.length > 0 || stmt.sql.trim().toUpperCase().startsWith("SELECT")) {
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. STANDARD - Default SQL translation
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.resolvePropertiesInContext(pattern.properties || {}, rowContext, params);
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.resolvePropertiesInContext(pattern.source.properties || {}, rowContext, params);
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.resolvePropertiesInContext(pattern.target.properties || {}, rowContext, params);
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 "/": return leftNum / rightNum;
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 "/": return leftNum / rightNum;
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 left + right;
4553
- case "-": return left - right;
4554
- case "*": return left * right;
4555
- case "/": return left / right;
4556
- case "%": return left % right;
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 left + right;
4598
- case "-": return left - right;
4599
- case "*": return left * right;
4600
- case "/": return left / right;
4601
- case "%": return left % right;
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 "/": return left / right;
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
- resolved[key] = this.resolvePropertyValueWithUnwind(value, params, unwindContext);
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 "/": return leftNum / rightNum;
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.resolveProperties(pattern.properties || {}, params);
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
- // Query the node
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
- resultRow[alias] = this.getNodeProperties(row.id, row.properties);
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
- const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${property}') as value FROM nodes WHERE id = ?`, [id]);
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
- resultRow[alias] = this.deepParseJson(nodeResult.rows[0].value);
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 json_extract(properties, '$.${property}') as value FROM edges WHERE id = ?`, [id]);
5879
+ const edgeResult = this.db.execute(`SELECT properties FROM edges WHERE id = ?`, [id]);
5794
5880
  if (edgeResult.rows.length > 0) {
5795
- resultRow[alias] = this.deepParseJson(edgeResult.rows[0].value);
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 "/": return leftNum / rightNum;
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.resolveProperties(rel.source.properties || {}, params);
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.resolveProperties(rel.target.properties || {}, params);
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 resolved = resolveAlias(assignment.variable);
8015
- if (resolved) {
8016
- referencedInSet.add(resolved);
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.resolveProperties(rel.source.properties || {}, params);
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.resolveProperties(rel.target.properties || {}, params);
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
  */