flowquery 1.0.18 → 1.0.21

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 (158) hide show
  1. package/.gitattributes +3 -0
  2. package/.github/workflows/python-publish.yml +56 -4
  3. package/.github/workflows/release.yml +26 -19
  4. package/.husky/pre-commit +26 -0
  5. package/README.md +37 -32
  6. package/dist/flowquery.min.js +1 -1
  7. package/dist/graph/data.d.ts +5 -4
  8. package/dist/graph/data.d.ts.map +1 -1
  9. package/dist/graph/data.js +38 -20
  10. package/dist/graph/data.js.map +1 -1
  11. package/dist/graph/node.d.ts +2 -0
  12. package/dist/graph/node.d.ts.map +1 -1
  13. package/dist/graph/node.js +23 -0
  14. package/dist/graph/node.js.map +1 -1
  15. package/dist/graph/node_data.js +1 -1
  16. package/dist/graph/node_data.js.map +1 -1
  17. package/dist/graph/pattern.d.ts.map +1 -1
  18. package/dist/graph/pattern.js +11 -4
  19. package/dist/graph/pattern.js.map +1 -1
  20. package/dist/graph/relationship.d.ts +6 -1
  21. package/dist/graph/relationship.d.ts.map +1 -1
  22. package/dist/graph/relationship.js +43 -5
  23. package/dist/graph/relationship.js.map +1 -1
  24. package/dist/graph/relationship_data.d.ts +2 -0
  25. package/dist/graph/relationship_data.d.ts.map +1 -1
  26. package/dist/graph/relationship_data.js +8 -1
  27. package/dist/graph/relationship_data.js.map +1 -1
  28. package/dist/graph/relationship_match_collector.js +2 -2
  29. package/dist/graph/relationship_match_collector.js.map +1 -1
  30. package/dist/graph/relationship_reference.d.ts.map +1 -1
  31. package/dist/graph/relationship_reference.js +2 -1
  32. package/dist/graph/relationship_reference.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/parsing/parser.d.ts +6 -0
  36. package/dist/parsing/parser.d.ts.map +1 -1
  37. package/dist/parsing/parser.js +139 -72
  38. package/dist/parsing/parser.js.map +1 -1
  39. package/docs/flowquery.min.js +1 -1
  40. package/flowquery-py/misc/data/test.json +10 -0
  41. package/flowquery-py/misc/data/users.json +242 -0
  42. package/flowquery-py/notebooks/TestFlowQuery.ipynb +440 -0
  43. package/flowquery-py/pyproject.toml +48 -2
  44. package/flowquery-py/src/__init__.py +7 -5
  45. package/flowquery-py/src/compute/runner.py +14 -10
  46. package/flowquery-py/src/extensibility.py +8 -8
  47. package/flowquery-py/src/graph/__init__.py +7 -7
  48. package/flowquery-py/src/graph/data.py +38 -20
  49. package/flowquery-py/src/graph/database.py +10 -20
  50. package/flowquery-py/src/graph/node.py +50 -19
  51. package/flowquery-py/src/graph/node_data.py +1 -1
  52. package/flowquery-py/src/graph/node_reference.py +10 -11
  53. package/flowquery-py/src/graph/pattern.py +27 -37
  54. package/flowquery-py/src/graph/pattern_expression.py +13 -11
  55. package/flowquery-py/src/graph/patterns.py +2 -2
  56. package/flowquery-py/src/graph/physical_node.py +4 -3
  57. package/flowquery-py/src/graph/physical_relationship.py +5 -5
  58. package/flowquery-py/src/graph/relationship.py +62 -14
  59. package/flowquery-py/src/graph/relationship_data.py +7 -2
  60. package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
  61. package/flowquery-py/src/graph/relationship_reference.py +4 -4
  62. package/flowquery-py/src/io/command_line.py +13 -14
  63. package/flowquery-py/src/parsing/__init__.py +2 -2
  64. package/flowquery-py/src/parsing/alias_option.py +1 -1
  65. package/flowquery-py/src/parsing/ast_node.py +21 -20
  66. package/flowquery-py/src/parsing/base_parser.py +7 -7
  67. package/flowquery-py/src/parsing/components/__init__.py +3 -3
  68. package/flowquery-py/src/parsing/components/from_.py +3 -1
  69. package/flowquery-py/src/parsing/components/headers.py +2 -2
  70. package/flowquery-py/src/parsing/components/null.py +2 -2
  71. package/flowquery-py/src/parsing/context.py +7 -7
  72. package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
  73. package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
  74. package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
  75. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
  76. package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
  77. package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
  78. package/flowquery-py/src/parsing/expressions/expression.py +16 -13
  79. package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
  80. package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
  81. package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
  82. package/flowquery-py/src/parsing/expressions/number.py +3 -3
  83. package/flowquery-py/src/parsing/expressions/operator.py +16 -16
  84. package/flowquery-py/src/parsing/expressions/reference.py +3 -3
  85. package/flowquery-py/src/parsing/expressions/string.py +2 -2
  86. package/flowquery-py/src/parsing/functions/__init__.py +17 -17
  87. package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
  88. package/flowquery-py/src/parsing/functions/async_function.py +12 -9
  89. package/flowquery-py/src/parsing/functions/avg.py +4 -4
  90. package/flowquery-py/src/parsing/functions/collect.py +6 -6
  91. package/flowquery-py/src/parsing/functions/function.py +6 -6
  92. package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
  93. package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
  94. package/flowquery-py/src/parsing/functions/functions.py +14 -6
  95. package/flowquery-py/src/parsing/functions/join.py +3 -3
  96. package/flowquery-py/src/parsing/functions/keys.py +3 -3
  97. package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
  98. package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
  99. package/flowquery-py/src/parsing/functions/rand.py +2 -2
  100. package/flowquery-py/src/parsing/functions/range_.py +9 -4
  101. package/flowquery-py/src/parsing/functions/replace.py +2 -2
  102. package/flowquery-py/src/parsing/functions/round_.py +2 -2
  103. package/flowquery-py/src/parsing/functions/size.py +2 -2
  104. package/flowquery-py/src/parsing/functions/split.py +9 -4
  105. package/flowquery-py/src/parsing/functions/stringify.py +3 -3
  106. package/flowquery-py/src/parsing/functions/sum.py +4 -4
  107. package/flowquery-py/src/parsing/functions/to_json.py +2 -2
  108. package/flowquery-py/src/parsing/functions/type_.py +3 -3
  109. package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
  110. package/flowquery-py/src/parsing/logic/__init__.py +2 -2
  111. package/flowquery-py/src/parsing/logic/case.py +0 -1
  112. package/flowquery-py/src/parsing/logic/when.py +3 -1
  113. package/flowquery-py/src/parsing/operations/__init__.py +10 -10
  114. package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
  115. package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
  116. package/flowquery-py/src/parsing/operations/call.py +6 -7
  117. package/flowquery-py/src/parsing/operations/create_node.py +5 -4
  118. package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
  119. package/flowquery-py/src/parsing/operations/group_by.py +18 -16
  120. package/flowquery-py/src/parsing/operations/load.py +21 -19
  121. package/flowquery-py/src/parsing/operations/match.py +8 -7
  122. package/flowquery-py/src/parsing/operations/operation.py +3 -3
  123. package/flowquery-py/src/parsing/operations/projection.py +6 -6
  124. package/flowquery-py/src/parsing/operations/return_op.py +9 -5
  125. package/flowquery-py/src/parsing/operations/unwind.py +3 -2
  126. package/flowquery-py/src/parsing/operations/where.py +9 -7
  127. package/flowquery-py/src/parsing/operations/with_op.py +2 -2
  128. package/flowquery-py/src/parsing/parser.py +178 -114
  129. package/flowquery-py/src/parsing/token_to_node.py +2 -2
  130. package/flowquery-py/src/tokenization/__init__.py +4 -4
  131. package/flowquery-py/src/tokenization/keyword.py +1 -1
  132. package/flowquery-py/src/tokenization/operator.py +1 -1
  133. package/flowquery-py/src/tokenization/string_walker.py +4 -4
  134. package/flowquery-py/src/tokenization/symbol.py +1 -1
  135. package/flowquery-py/src/tokenization/token.py +11 -11
  136. package/flowquery-py/src/tokenization/token_mapper.py +10 -9
  137. package/flowquery-py/src/tokenization/token_type.py +1 -1
  138. package/flowquery-py/src/tokenization/tokenizer.py +19 -19
  139. package/flowquery-py/src/tokenization/trie.py +18 -17
  140. package/flowquery-py/src/utils/__init__.py +1 -1
  141. package/flowquery-py/src/utils/object_utils.py +3 -3
  142. package/flowquery-py/src/utils/string_utils.py +12 -12
  143. package/flowquery-py/tests/compute/test_runner.py +214 -7
  144. package/flowquery-py/tests/parsing/test_parser.py +41 -0
  145. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  146. package/package.json +1 -1
  147. package/src/graph/data.ts +38 -20
  148. package/src/graph/node.ts +23 -0
  149. package/src/graph/node_data.ts +1 -1
  150. package/src/graph/pattern.ts +13 -4
  151. package/src/graph/relationship.ts +45 -5
  152. package/src/graph/relationship_data.ts +8 -1
  153. package/src/graph/relationship_match_collector.ts +1 -1
  154. package/src/graph/relationship_reference.ts +2 -1
  155. package/src/index.ts +5 -5
  156. package/src/parsing/parser.ts +139 -71
  157. package/tests/compute/runner.test.ts +249 -79
  158. package/tests/parsing/parser.test.ts +32 -0
@@ -804,10 +804,18 @@ test("Test match with multiple hop graph pattern", async () => {
804
804
  `);
805
805
  await match.run();
806
806
  const results = match.results;
807
- expect(results.length).toBe(3);
808
- expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 2" });
809
- expect(results[1]).toEqual({ name1: "Person 1", name2: "Person 3" });
810
- expect(results[2]).toEqual({ name1: "Person 2", name2: "Person 3" });
807
+ expect(results.length).toBe(7);
808
+ // Results are interleaved: each person's zero-hop comes before their multi-hop matches
809
+ // Person 1: zero-hop, then 1-hop to P2, then 2-hop to P3
810
+ expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 1" });
811
+ expect(results[1]).toEqual({ name1: "Person 1", name2: "Person 2" });
812
+ expect(results[2]).toEqual({ name1: "Person 1", name2: "Person 3" });
813
+ // Person 2: zero-hop, then 1-hop to P3
814
+ expect(results[3]).toEqual({ name1: "Person 2", name2: "Person 2" });
815
+ expect(results[4]).toEqual({ name1: "Person 2", name2: "Person 3" });
816
+ // Person 3 and 4: only zero-hop matches
817
+ expect(results[5]).toEqual({ name1: "Person 3", name2: "Person 3" });
818
+ expect(results[6]).toEqual({ name1: "Person 4", name2: "Person 4" });
811
819
  });
812
820
 
813
821
  test("Test match with double graph pattern", async () => {
@@ -1025,51 +1033,41 @@ test("Test multi-hop match with variable length relationships", async () => {
1025
1033
  `);
1026
1034
  await match.run();
1027
1035
  const results = match.results;
1028
- expect(results.length).toBe(6);
1036
+ expect(results.length).toBe(10);
1037
+
1038
+ // Results are interleaved: each person's zero-hop comes before their multi-hop matches
1039
+ // Note: first zero-hop has r=null, subsequent zero-hops may have r=[] or stale value
1029
1040
 
1041
+ // Person 1's results: zero-hop, 1-hop to P2, 2-hop to P3, 3-hop to P4
1030
1042
  expect(results[0].a.id).toBe(1);
1031
- expect(results[0].b.id).toBe(2);
1032
- expect(results[0].r.length).toBe(undefined);
1033
- expect(results[0].r.startNode.id).toBe(1);
1034
- expect(results[0].r.endNode.id).toBe(2);
1043
+ expect(results[0].b.id).toBe(1);
1044
+ // First zero-hop has r=null
1045
+ expect(results[0].r).toBe(null);
1035
1046
 
1036
1047
  expect(results[1].a.id).toBe(1);
1037
- expect(results[1].b.id).toBe(3);
1038
- expect(results[1].r.length).toBe(2);
1039
- expect(results[1].r[0].startNode.id).toBe(1);
1040
- expect(results[1].r[0].endNode.id).toBe(2);
1041
- expect(results[1].r[1].startNode.id).toBe(2);
1042
- expect(results[1].r[1].endNode.id).toBe(3);
1043
-
1048
+ expect(results[1].b.id).toBe(2);
1044
1049
  expect(results[2].a.id).toBe(1);
1045
- expect(results[2].b.id).toBe(4);
1046
- expect(results[2].r.length).toBe(3);
1047
- expect(results[2].r[0].startNode.id).toBe(1);
1048
- expect(results[2].r[0].endNode.id).toBe(2);
1049
- expect(results[2].r[1].startNode.id).toBe(2);
1050
- expect(results[2].r[1].endNode.id).toBe(3);
1051
- expect(results[2].r[2].startNode.id).toBe(3);
1052
- expect(results[2].r[2].endNode.id).toBe(4);
1053
-
1054
- expect(results[3].a.id).toBe(2);
1055
- expect(results[3].b.id).toBe(3);
1056
- expect(results[3].r.length).toBe(undefined);
1057
- expect(results[3].r.startNode.id).toBe(2);
1058
- expect(results[3].r.endNode.id).toBe(3);
1050
+ expect(results[2].b.id).toBe(3);
1051
+ expect(results[3].a.id).toBe(1);
1052
+ expect(results[3].b.id).toBe(4);
1059
1053
 
1054
+ // Person 2's results: zero-hop, 1-hop to P3, 2-hop to P4
1060
1055
  expect(results[4].a.id).toBe(2);
1061
- expect(results[4].b.id).toBe(4);
1062
- expect(results[4].r.length).toBe(2);
1063
- expect(results[4].r[0].startNode.id).toBe(2);
1064
- expect(results[4].r[0].endNode.id).toBe(3);
1065
- expect(results[4].r[1].startNode.id).toBe(3);
1066
- expect(results[4].r[1].endNode.id).toBe(4);
1056
+ expect(results[4].b.id).toBe(2);
1057
+ expect(results[5].a.id).toBe(2);
1058
+ expect(results[5].b.id).toBe(3);
1059
+ expect(results[6].a.id).toBe(2);
1060
+ expect(results[6].b.id).toBe(4);
1061
+
1062
+ // Person 3's results: zero-hop, 1-hop to P4
1063
+ expect(results[7].a.id).toBe(3);
1064
+ expect(results[7].b.id).toBe(3);
1065
+ expect(results[8].a.id).toBe(3);
1066
+ expect(results[8].b.id).toBe(4);
1067
1067
 
1068
- expect(results[5].a.id).toBe(3);
1069
- expect(results[5].b.id).toBe(4);
1070
- expect(results[5].r.length).toBe(undefined);
1071
- expect(results[5].r.startNode.id).toBe(3);
1072
- expect(results[5].r.endNode.id).toBe(4);
1068
+ // Person 4's result: zero-hop only
1069
+ expect(results[9].a.id).toBe(4);
1070
+ expect(results[9].b.id).toBe(4);
1073
1071
  });
1074
1072
 
1075
1073
  test("Test return match pattern with variable length relationships", async () => {
@@ -1100,55 +1098,48 @@ test("Test return match pattern with variable length relationships", async () =>
1100
1098
  `);
1101
1099
  await match.run();
1102
1100
  const results = match.results;
1103
- expect(results.length).toBe(6);
1101
+ expect(results.length).toBe(10);
1104
1102
 
1105
- expect(results[0].pattern.length).toBe(3);
1103
+ // Index 0: Person 1 zero-hop - pattern = [node1] (single node, no duplicate)
1104
+ expect(results[0].pattern.length).toBe(1);
1106
1105
  expect(results[0].pattern[0].id).toBe(1);
1107
- expect(results[0].pattern[1].startNode.id).toBe(1);
1108
- expect(results[0].pattern[1].endNode.id).toBe(2);
1109
- expect(results[0].pattern[2].id).toBe(2);
1110
1106
 
1111
- expect(results[1].pattern.length).toBe(5);
1107
+ // Index 1: Person 1 -> Person 2 (1-hop): pattern = [node1, rel, node2]
1108
+ expect(results[1].pattern.length).toBe(3);
1112
1109
  expect(results[1].pattern[0].id).toBe(1);
1113
1110
  expect(results[1].pattern[1].startNode.id).toBe(1);
1114
1111
  expect(results[1].pattern[1].endNode.id).toBe(2);
1115
1112
  expect(results[1].pattern[2].id).toBe(2);
1116
- expect(results[1].pattern[3].startNode.id).toBe(2);
1117
- expect(results[1].pattern[3].endNode.id).toBe(3);
1118
- expect(results[1].pattern[4].id).toBe(3);
1119
1113
 
1120
- expect(results[2].pattern.length).toBe(7);
1114
+ // Index 2: Person 1 -> Person 3 (2-hop): pattern length = 5
1115
+ expect(results[2].pattern.length).toBe(5);
1121
1116
  expect(results[2].pattern[0].id).toBe(1);
1122
- expect(results[2].pattern[1].startNode.id).toBe(1);
1123
- expect(results[2].pattern[1].endNode.id).toBe(2);
1124
- expect(results[2].pattern[2].id).toBe(2);
1125
- expect(results[2].pattern[3].startNode.id).toBe(2);
1126
- expect(results[2].pattern[3].endNode.id).toBe(3);
1127
- expect(results[2].pattern[4].id).toBe(3);
1128
- expect(results[2].pattern[5].startNode.id).toBe(3);
1129
- expect(results[2].pattern[5].endNode.id).toBe(4);
1130
- expect(results[2].pattern[6].id).toBe(4);
1131
-
1132
- expect(results[3].pattern.length).toBe(3);
1133
- expect(results[3].pattern[0].id).toBe(2);
1134
- expect(results[3].pattern[1].startNode.id).toBe(2);
1135
- expect(results[3].pattern[1].endNode.id).toBe(3);
1136
- expect(results[3].pattern[2].id).toBe(3);
1137
-
1138
- expect(results[4].pattern.length).toBe(5);
1117
+
1118
+ // Index 3: Person 1 -> Person 4 (3-hop): pattern length = 7
1119
+ expect(results[3].pattern.length).toBe(7);
1120
+ expect(results[3].pattern[0].id).toBe(1);
1121
+ expect(results[3].pattern[6].id).toBe(4);
1122
+
1123
+ // Index 4: Person 2 zero-hop - pattern = [node2] (single node)
1124
+ expect(results[4].pattern.length).toBe(1);
1139
1125
  expect(results[4].pattern[0].id).toBe(2);
1140
- expect(results[4].pattern[1].startNode.id).toBe(2);
1141
- expect(results[4].pattern[1].endNode.id).toBe(3);
1142
- expect(results[4].pattern[2].id).toBe(3);
1143
- expect(results[4].pattern[3].startNode.id).toBe(3);
1144
- expect(results[4].pattern[3].endNode.id).toBe(4);
1145
- expect(results[4].pattern[4].id).toBe(4);
1146
1126
 
1127
+ // Index 5: Person 2 -> Person 3 (1-hop)
1147
1128
  expect(results[5].pattern.length).toBe(3);
1148
- expect(results[5].pattern[0].id).toBe(3);
1149
- expect(results[5].pattern[1].startNode.id).toBe(3);
1150
- expect(results[5].pattern[1].endNode.id).toBe(4);
1151
- expect(results[5].pattern[2].id).toBe(4);
1129
+
1130
+ // Index 6: Person 2 -> Person 4 (2-hop)
1131
+ expect(results[6].pattern.length).toBe(5);
1132
+
1133
+ // Index 7: Person 3 zero-hop - pattern = [node3] (single node)
1134
+ expect(results[7].pattern.length).toBe(1);
1135
+ expect(results[7].pattern[0].id).toBe(3);
1136
+
1137
+ // Index 8: Person 3 -> Person 4 (1-hop)
1138
+ expect(results[8].pattern.length).toBe(3);
1139
+
1140
+ // Index 9: Person 4 zero-hop - pattern = [node4] (single node)
1141
+ expect(results[9].pattern.length).toBe(1);
1142
+ expect(results[9].pattern[0].id).toBe(4);
1152
1143
  });
1153
1144
 
1154
1145
  test("Test statement with graph pattern in where clause", async () => {
@@ -1270,7 +1261,8 @@ test("Test manager chain", async () => {
1270
1261
  `);
1271
1262
  await match.run();
1272
1263
  const results = match.results;
1273
- expect(results.length).toBe(2);
1264
+ // 4 results: includes CEO (Employee 1) with zero-hop match (empty management chain)
1265
+ expect(results.length).toBe(4);
1274
1266
  });
1275
1267
 
1276
1268
  test("Test equality comparison", async () => {
@@ -1290,3 +1282,181 @@ test("Test equality comparison", async () => {
1290
1282
  }
1291
1283
  }
1292
1284
  });
1285
+
1286
+ test("Test match with constraints", async () => {
1287
+ await new Runner(`
1288
+ CREATE VIRTUAL (:Employee) AS {
1289
+ unwind [
1290
+ {id: 1, name: 'Employee 1'},
1291
+ {id: 2, name: 'Employee 2'},
1292
+ {id: 3, name: 'Employee 3'},
1293
+ {id: 4, name: 'Employee 4'}
1294
+ ] as record
1295
+ RETURN record.id as id, record.name as name
1296
+ }
1297
+ `).run();
1298
+ const match = new Runner(`
1299
+ match (e:Employee{name:'Employee 1'})
1300
+ return e.name as name
1301
+ `);
1302
+ await match.run();
1303
+ const results = match.results;
1304
+ expect(results.length).toBe(1);
1305
+ expect(results[0].name).toBe("Employee 1");
1306
+ });
1307
+
1308
+ test("Test match with leftward relationship direction", async () => {
1309
+ await new Runner(`
1310
+ CREATE VIRTUAL (:Person) AS {
1311
+ unwind [
1312
+ {id: 1, name: 'Person 1'},
1313
+ {id: 2, name: 'Person 2'},
1314
+ {id: 3, name: 'Person 3'}
1315
+ ] as record
1316
+ RETURN record.id as id, record.name as name
1317
+ }
1318
+ `).run();
1319
+ await new Runner(`
1320
+ CREATE VIRTUAL (:Person)-[:REPORTS_TO]-(:Person) AS {
1321
+ unwind [
1322
+ {left_id: 2, right_id: 1},
1323
+ {left_id: 3, right_id: 1}
1324
+ ] as record
1325
+ RETURN record.left_id as left_id, record.right_id as right_id
1326
+ }
1327
+ `).run();
1328
+ // Rightward: left_id -> right_id (2->1, 3->1)
1329
+ const rightMatch = new Runner(`
1330
+ MATCH (a:Person)-[:REPORTS_TO]->(b:Person)
1331
+ RETURN a.name AS employee, b.name AS manager
1332
+ `);
1333
+ await rightMatch.run();
1334
+ const rightResults = rightMatch.results;
1335
+ expect(rightResults.length).toBe(2);
1336
+ expect(rightResults[0]).toEqual({ employee: "Person 2", manager: "Person 1" });
1337
+ expect(rightResults[1]).toEqual({ employee: "Person 3", manager: "Person 1" });
1338
+
1339
+ // Leftward: right_id -> left_id (1->2, 1->3) — reverse traversal
1340
+ const leftMatch = new Runner(`
1341
+ MATCH (m:Person)<-[:REPORTS_TO]-(e:Person)
1342
+ RETURN m.name AS manager, e.name AS employee
1343
+ `);
1344
+ await leftMatch.run();
1345
+ const leftResults = leftMatch.results;
1346
+ expect(leftResults.length).toBe(2);
1347
+ expect(leftResults[0]).toEqual({ manager: "Person 1", employee: "Person 2" });
1348
+ expect(leftResults[1]).toEqual({ manager: "Person 1", employee: "Person 3" });
1349
+ });
1350
+
1351
+ test("Test match with leftward direction produces same results as rightward with swapped data", async () => {
1352
+ await new Runner(`
1353
+ CREATE VIRTUAL (:City) AS {
1354
+ unwind [
1355
+ {id: 1, name: 'New York'},
1356
+ {id: 2, name: 'Boston'},
1357
+ {id: 3, name: 'Chicago'}
1358
+ ] as record
1359
+ RETURN record.id as id, record.name as name
1360
+ }
1361
+ `).run();
1362
+ await new Runner(`
1363
+ CREATE VIRTUAL (:City)-[:ROUTE]-(:City) AS {
1364
+ unwind [
1365
+ {left_id: 1, right_id: 2},
1366
+ {left_id: 1, right_id: 3}
1367
+ ] as record
1368
+ RETURN record.left_id as left_id, record.right_id as right_id
1369
+ }
1370
+ `).run();
1371
+ // Leftward from destination: find where right_id matches, follow left_id
1372
+ const match = new Runner(`
1373
+ MATCH (dest:City)<-[:ROUTE]-(origin:City)
1374
+ RETURN dest.name AS destination, origin.name AS origin
1375
+ `);
1376
+ await match.run();
1377
+ const results = match.results;
1378
+ expect(results.length).toBe(2);
1379
+ expect(results[0]).toEqual({ destination: "Boston", origin: "New York" });
1380
+ expect(results[1]).toEqual({ destination: "Chicago", origin: "New York" });
1381
+ });
1382
+
1383
+ test("Test match with leftward variable-length relationships", async () => {
1384
+ await new Runner(`
1385
+ CREATE VIRTUAL (:Person) AS {
1386
+ unwind [
1387
+ {id: 1, name: 'Person 1'},
1388
+ {id: 2, name: 'Person 2'},
1389
+ {id: 3, name: 'Person 3'}
1390
+ ] as record
1391
+ RETURN record.id as id, record.name as name
1392
+ }
1393
+ `).run();
1394
+ await new Runner(`
1395
+ CREATE VIRTUAL (:Person)-[:MANAGES]-(:Person) AS {
1396
+ unwind [
1397
+ {left_id: 1, right_id: 2},
1398
+ {left_id: 2, right_id: 3}
1399
+ ] as record
1400
+ RETURN record.left_id as left_id, record.right_id as right_id
1401
+ }
1402
+ `).run();
1403
+ // Leftward variable-length: traverse from right_id to left_id
1404
+ // Person 3 can reach Person 2 (1 hop) and Person 1 (2 hops)
1405
+ const match = new Runner(`
1406
+ MATCH (a:Person)<-[:MANAGES*]-(b:Person)
1407
+ RETURN a.name AS name1, b.name AS name2
1408
+ `);
1409
+ await match.run();
1410
+ const results = match.results;
1411
+ // Zero-hop results for all 3 persons + multi-hop results
1412
+ // Leftward indexes on right_id. find(id) looks up right_id=id, follows left_id.
1413
+ // right_id=1: no records → Person 1 zero-hop only
1414
+ // right_id=2: record {left_id:1, right_id:2} → Person 2 → Person 1, then recurse find(1) → no more
1415
+ // right_id=3: record {left_id:2, right_id:3} → Person 3 → Person 2, then recurse find(2) → Person 1
1416
+ expect(results.length).toBe(6);
1417
+ // Person 1: zero-hop
1418
+ expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 1" });
1419
+ // Person 2: zero-hop, then reaches Person 1
1420
+ expect(results[1]).toEqual({ name1: "Person 2", name2: "Person 2" });
1421
+ expect(results[2]).toEqual({ name1: "Person 2", name2: "Person 1" });
1422
+ // Person 3: zero-hop, then reaches Person 2, then Person 1
1423
+ expect(results[3]).toEqual({ name1: "Person 3", name2: "Person 3" });
1424
+ expect(results[4]).toEqual({ name1: "Person 3", name2: "Person 2" });
1425
+ expect(results[5]).toEqual({ name1: "Person 3", name2: "Person 1" });
1426
+ });
1427
+
1428
+ test("Test match with leftward double graph pattern", async () => {
1429
+ await new Runner(`
1430
+ CREATE VIRTUAL (:Person) AS {
1431
+ unwind [
1432
+ {id: 1, name: 'Person 1'},
1433
+ {id: 2, name: 'Person 2'},
1434
+ {id: 3, name: 'Person 3'},
1435
+ {id: 4, name: 'Person 4'}
1436
+ ] as record
1437
+ RETURN record.id as id, record.name as name
1438
+ }
1439
+ `).run();
1440
+ await new Runner(`
1441
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
1442
+ unwind [
1443
+ {left_id: 1, right_id: 2},
1444
+ {left_id: 2, right_id: 3},
1445
+ {left_id: 3, right_id: 4}
1446
+ ] as record
1447
+ RETURN record.left_id as left_id, record.right_id as right_id
1448
+ }
1449
+ `).run();
1450
+ // Leftward chain: (c)<-[:KNOWS]-(b)<-[:KNOWS]-(a)
1451
+ // First rel: find right_id=c, follow left_id to b
1452
+ // Second rel: find right_id=b, follow left_id to a
1453
+ const match = new Runner(`
1454
+ MATCH (c:Person)<-[:KNOWS]-(b:Person)<-[:KNOWS]-(a:Person)
1455
+ RETURN a.name AS name1, b.name AS name2, c.name AS name3
1456
+ `);
1457
+ await match.run();
1458
+ const results = match.results;
1459
+ expect(results.length).toBe(2);
1460
+ expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 2", name3: "Person 3" });
1461
+ expect(results[1]).toEqual({ name1: "Person 2", name2: "Person 3", name3: "Person 4" });
1462
+ });
@@ -760,3 +760,35 @@ test("Test check pattern expression without NodeReference", () => {
760
760
  parser.parse("MATCH (a:Person) WHERE (:Person)-[:KNOWS]->(:Person) RETURN a");
761
761
  }).toThrow("PatternExpression must contain at least one NodeReference");
762
762
  });
763
+
764
+ test("Test node with properties", () => {
765
+ const parser = new Parser();
766
+ const ast = parser.parse("MATCH (a:Person{value: 'hello'}) return a");
767
+ // prettier-ignore
768
+ expect(ast.print()).toBe(
769
+ "ASTNode\n" +
770
+ "- Match\n" +
771
+ "- Return\n" +
772
+ "-- Expression (a)\n" +
773
+ "--- Reference (a)"
774
+ );
775
+ const match: Match = ast.firstChild() as Match;
776
+ const node: Node = match.patterns[0].chain[0] as Node;
777
+ expect(node.properties.get("value")?.value()).toBe("hello");
778
+ });
779
+
780
+ test("Test relationship with properties", () => {
781
+ const parser = new Parser();
782
+ const ast = parser.parse("MATCH (:Person)-[r:LIKES{since: 2022}]->(:Food) return a");
783
+ // prettier-ignore
784
+ expect(ast.print()).toBe(
785
+ "ASTNode\n" +
786
+ "- Match\n" +
787
+ "- Return\n" +
788
+ "-- Expression (a)\n" +
789
+ "--- Reference (a)"
790
+ );
791
+ const match: Match = ast.firstChild() as Match;
792
+ const relationship: Relationship = match.patterns[0].chain[1] as Relationship;
793
+ expect(relationship.properties.get("since")?.value()).toBe(2022);
794
+ });