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.
- package/.gitattributes +3 -0
- package/.github/workflows/python-publish.yml +56 -4
- package/.github/workflows/release.yml +26 -19
- package/.husky/pre-commit +26 -0
- package/README.md +37 -32
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/data.d.ts +5 -4
- package/dist/graph/data.d.ts.map +1 -1
- package/dist/graph/data.js +38 -20
- package/dist/graph/data.js.map +1 -1
- package/dist/graph/node.d.ts +2 -0
- package/dist/graph/node.d.ts.map +1 -1
- package/dist/graph/node.js +23 -0
- package/dist/graph/node.js.map +1 -1
- package/dist/graph/node_data.js +1 -1
- package/dist/graph/node_data.js.map +1 -1
- package/dist/graph/pattern.d.ts.map +1 -1
- package/dist/graph/pattern.js +11 -4
- package/dist/graph/pattern.js.map +1 -1
- package/dist/graph/relationship.d.ts +6 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +43 -5
- package/dist/graph/relationship.js.map +1 -1
- package/dist/graph/relationship_data.d.ts +2 -0
- package/dist/graph/relationship_data.d.ts.map +1 -1
- package/dist/graph/relationship_data.js +8 -1
- package/dist/graph/relationship_data.js.map +1 -1
- package/dist/graph/relationship_match_collector.js +2 -2
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/dist/graph/relationship_reference.d.ts.map +1 -1
- package/dist/graph/relationship_reference.js +2 -1
- package/dist/graph/relationship_reference.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/parsing/parser.d.ts +6 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +139 -72
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/misc/data/test.json +10 -0
- package/flowquery-py/misc/data/users.json +242 -0
- package/flowquery-py/notebooks/TestFlowQuery.ipynb +440 -0
- package/flowquery-py/pyproject.toml +48 -2
- package/flowquery-py/src/__init__.py +7 -5
- package/flowquery-py/src/compute/runner.py +14 -10
- package/flowquery-py/src/extensibility.py +8 -8
- package/flowquery-py/src/graph/__init__.py +7 -7
- package/flowquery-py/src/graph/data.py +38 -20
- package/flowquery-py/src/graph/database.py +10 -20
- package/flowquery-py/src/graph/node.py +50 -19
- package/flowquery-py/src/graph/node_data.py +1 -1
- package/flowquery-py/src/graph/node_reference.py +10 -11
- package/flowquery-py/src/graph/pattern.py +27 -37
- package/flowquery-py/src/graph/pattern_expression.py +13 -11
- package/flowquery-py/src/graph/patterns.py +2 -2
- package/flowquery-py/src/graph/physical_node.py +4 -3
- package/flowquery-py/src/graph/physical_relationship.py +5 -5
- package/flowquery-py/src/graph/relationship.py +62 -14
- package/flowquery-py/src/graph/relationship_data.py +7 -2
- package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
- package/flowquery-py/src/graph/relationship_reference.py +4 -4
- package/flowquery-py/src/io/command_line.py +13 -14
- package/flowquery-py/src/parsing/__init__.py +2 -2
- package/flowquery-py/src/parsing/alias_option.py +1 -1
- package/flowquery-py/src/parsing/ast_node.py +21 -20
- package/flowquery-py/src/parsing/base_parser.py +7 -7
- package/flowquery-py/src/parsing/components/__init__.py +3 -3
- package/flowquery-py/src/parsing/components/from_.py +3 -1
- package/flowquery-py/src/parsing/components/headers.py +2 -2
- package/flowquery-py/src/parsing/components/null.py +2 -2
- package/flowquery-py/src/parsing/context.py +7 -7
- package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
- package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
- package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
- package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
- package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
- package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
- package/flowquery-py/src/parsing/expressions/expression.py +16 -13
- package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
- package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
- package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
- package/flowquery-py/src/parsing/expressions/number.py +3 -3
- package/flowquery-py/src/parsing/expressions/operator.py +16 -16
- package/flowquery-py/src/parsing/expressions/reference.py +3 -3
- package/flowquery-py/src/parsing/expressions/string.py +2 -2
- package/flowquery-py/src/parsing/functions/__init__.py +17 -17
- package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
- package/flowquery-py/src/parsing/functions/async_function.py +12 -9
- package/flowquery-py/src/parsing/functions/avg.py +4 -4
- package/flowquery-py/src/parsing/functions/collect.py +6 -6
- package/flowquery-py/src/parsing/functions/function.py +6 -6
- package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
- package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
- package/flowquery-py/src/parsing/functions/functions.py +14 -6
- package/flowquery-py/src/parsing/functions/join.py +3 -3
- package/flowquery-py/src/parsing/functions/keys.py +3 -3
- package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
- package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
- package/flowquery-py/src/parsing/functions/rand.py +2 -2
- package/flowquery-py/src/parsing/functions/range_.py +9 -4
- package/flowquery-py/src/parsing/functions/replace.py +2 -2
- package/flowquery-py/src/parsing/functions/round_.py +2 -2
- package/flowquery-py/src/parsing/functions/size.py +2 -2
- package/flowquery-py/src/parsing/functions/split.py +9 -4
- package/flowquery-py/src/parsing/functions/stringify.py +3 -3
- package/flowquery-py/src/parsing/functions/sum.py +4 -4
- package/flowquery-py/src/parsing/functions/to_json.py +2 -2
- package/flowquery-py/src/parsing/functions/type_.py +3 -3
- package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
- package/flowquery-py/src/parsing/logic/__init__.py +2 -2
- package/flowquery-py/src/parsing/logic/case.py +0 -1
- package/flowquery-py/src/parsing/logic/when.py +3 -1
- package/flowquery-py/src/parsing/operations/__init__.py +10 -10
- package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
- package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
- package/flowquery-py/src/parsing/operations/call.py +6 -7
- package/flowquery-py/src/parsing/operations/create_node.py +5 -4
- package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
- package/flowquery-py/src/parsing/operations/group_by.py +18 -16
- package/flowquery-py/src/parsing/operations/load.py +21 -19
- package/flowquery-py/src/parsing/operations/match.py +8 -7
- package/flowquery-py/src/parsing/operations/operation.py +3 -3
- package/flowquery-py/src/parsing/operations/projection.py +6 -6
- package/flowquery-py/src/parsing/operations/return_op.py +9 -5
- package/flowquery-py/src/parsing/operations/unwind.py +3 -2
- package/flowquery-py/src/parsing/operations/where.py +9 -7
- package/flowquery-py/src/parsing/operations/with_op.py +2 -2
- package/flowquery-py/src/parsing/parser.py +178 -114
- package/flowquery-py/src/parsing/token_to_node.py +2 -2
- package/flowquery-py/src/tokenization/__init__.py +4 -4
- package/flowquery-py/src/tokenization/keyword.py +1 -1
- package/flowquery-py/src/tokenization/operator.py +1 -1
- package/flowquery-py/src/tokenization/string_walker.py +4 -4
- package/flowquery-py/src/tokenization/symbol.py +1 -1
- package/flowquery-py/src/tokenization/token.py +11 -11
- package/flowquery-py/src/tokenization/token_mapper.py +10 -9
- package/flowquery-py/src/tokenization/token_type.py +1 -1
- package/flowquery-py/src/tokenization/tokenizer.py +19 -19
- package/flowquery-py/src/tokenization/trie.py +18 -17
- package/flowquery-py/src/utils/__init__.py +1 -1
- package/flowquery-py/src/utils/object_utils.py +3 -3
- package/flowquery-py/src/utils/string_utils.py +12 -12
- package/flowquery-py/tests/compute/test_runner.py +214 -7
- package/flowquery-py/tests/parsing/test_parser.py +41 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/data.ts +38 -20
- package/src/graph/node.ts +23 -0
- package/src/graph/node_data.ts +1 -1
- package/src/graph/pattern.ts +13 -4
- package/src/graph/relationship.ts +45 -5
- package/src/graph/relationship_data.ts +8 -1
- package/src/graph/relationship_match_collector.ts +1 -1
- package/src/graph/relationship_reference.ts +2 -1
- package/src/index.ts +5 -5
- package/src/parsing/parser.ts +139 -71
- package/tests/compute/runner.test.ts +249 -79
- 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(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
expect(results[
|
|
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(
|
|
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(
|
|
1032
|
-
|
|
1033
|
-
expect(results[0].r
|
|
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(
|
|
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(
|
|
1046
|
-
expect(results[
|
|
1047
|
-
expect(results[
|
|
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(
|
|
1062
|
-
expect(results[
|
|
1063
|
-
expect(results[
|
|
1064
|
-
expect(results[
|
|
1065
|
-
expect(results[
|
|
1066
|
-
|
|
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
|
-
|
|
1069
|
-
expect(results[
|
|
1070
|
-
expect(results[
|
|
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(
|
|
1101
|
+
expect(results.length).toBe(10);
|
|
1104
1102
|
|
|
1105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
expect(results[
|
|
1125
|
-
expect(results[
|
|
1126
|
-
expect(results[
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
expect(results[
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
expect(results[
|
|
1151
|
-
|
|
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
|
-
|
|
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
|
+
});
|