From ce44df81f514a9e6bcee9bf7f0c4a915326e7186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Cord=C3=B3n?= Date: Thu, 11 Apr 2024 10:14:03 +0100 Subject: [PATCH] [pb3CtuNX] Makes a new procedure instead --- core/src/main/java/apoc/convert/Json.java | 74 ++- .../java/apoc/convert/ConvertJsonTest.java | 297 +++++++++- .../apoc/convert/ConvertPathsToTreeTest.java | 551 ++++++++++++++++++ 3 files changed, 889 insertions(+), 33 deletions(-) create mode 100644 core/src/test/java/apoc/convert/ConvertPathsToTreeTest.java diff --git a/core/src/main/java/apoc/convert/Json.java b/core/src/main/java/apoc/convert/Json.java index 6486db54b1..8bf74b4502 100644 --- a/core/src/main/java/apoc/convert/Json.java +++ b/core/src/main/java/apoc/convert/Json.java @@ -241,6 +241,73 @@ public Stream toTree( return result; } + @Procedure("apoc.convert.paths.toTree") + @Description( + "apoc.convert.paths.toTree([paths],[lowerCaseRels=true], [config]) creates a stream of nested documents representing the at least one root of these paths") + // todo optinally provide root node + public Stream pathsToTree( + @Name("paths") List paths, + @Name(value = "lowerCaseRels", defaultValue = "true") boolean lowerCaseRels, + @Name(value = "config", defaultValue = "{}") Map config) { + if (paths == null || paths.isEmpty()) return Stream.of(new MapResult(Collections.emptyMap())); + ConvertConfig conf = new ConvertConfig(config); + Map> nodes = conf.getNodes(); + Map> rels = conf.getRels(); + Set visitedInOtherPaths = new HashSet<>(); + Set nodesToKeepInResult = new HashSet<>(); + Map> tree = new HashMap<>(); + + Stream allPaths = paths.stream(); + if (conf.isSortPaths()) { + allPaths = allPaths.sorted(Comparator.comparingInt(Path::length).reversed()); + } + allPaths.forEach(path -> { + // This api will always return relationships in an outgoing fashion ()-[r]->() + var pathRelationships = path.relationships(); + pathRelationships.iterator().forEachRemaining((currentRel) -> { + Node currentNode = currentRel.getStartNode(); + Long currentNodeId = currentNode.getId(); + + if (!visitedInOtherPaths.contains(currentNodeId)) { + nodesToKeepInResult.add(currentNodeId); + } + + Node nextNode = currentRel.getEndNode(); + Map nodeMap = + tree.computeIfAbsent(currentNode.getId(), (id) -> toMap(currentNode, nodes)); + + Long nextNodeId = nextNode.getId(); + String typeName = lowerCaseRels + ? currentRel.getType().name().toLowerCase() + : currentRel.getType().name(); + // todo take direction into account and create collection into outgoing direction ?? + // parent-[:HAS_CHILD]->(child) vs. (parent)<-[:PARENT_OF]-(child) + if (!nodeMap.containsKey(typeName)) nodeMap.put(typeName, new ArrayList<>()); + // Check that this combination of rel and node doesn't already exist + List> currentNodeRels = (List) nodeMap.get(typeName); + boolean alreadyProcessedRel = currentNodeRels.stream() + .anyMatch(elem -> elem.get("_id").equals(nextNodeId) + && elem.get(typeName + "._id").equals(currentRel.getId())); + if (!alreadyProcessedRel) { + boolean nodeAlreadyVisited = tree.containsKey(nextNodeId); + Map nextNodeMap = toMap(nextNode, nodes); + addRelProperties(nextNodeMap, typeName, currentRel, rels); + + if (!nodeAlreadyVisited) { + tree.put(nextNodeId, nextNodeMap); + } + + visitedInOtherPaths.add(nextNodeId); + currentNodeRels.add(nextNodeMap); + } + }); + }); + + var result = + nodesToKeepInResult.stream().map(nodeId -> tree.get(nodeId)).map(MapResult::new); + return result; + } + @UserFunction("apoc.convert.toSortedJsonMap") @Description( "apoc.convert.toSortedJsonMap(node|map, ignoreCase:true) - returns a JSON map with keys sorted alphabetically, with optional case sensitivity") @@ -290,8 +357,11 @@ private Map toMap(Node n, Map> nodeFilters) String type = Util.labelString(n); result.put("_id", n.getId()); result.put("_type", type); - if (nodeFilters.containsKey(type)) { // Check if list contains LABEL - props = filterProperties(props, nodeFilters.get(type)); + var types = type.split(":"); + var filter = + Arrays.stream(types).filter((t) -> nodeFilters.containsKey(t)).findFirst(); + if (filter.isPresent()) { // Check if list contains LABEL + props = filterProperties(props, nodeFilters.get(filter.get())); } result.putAll(props); return result; diff --git a/core/src/test/java/apoc/convert/ConvertJsonTest.java b/core/src/test/java/apoc/convert/ConvertJsonTest.java index fbe0a66c17..f7a13f1cc2 100644 --- a/core/src/test/java/apoc/convert/ConvertJsonTest.java +++ b/core/src/test/java/apoc/convert/ConvertJsonTest.java @@ -85,10 +85,6 @@ public void clear() { db.executeTransactionally("MATCH (n) DETACH DELETE n;"); } - public String normalize(String input) { - return input.replaceAll("\\s", ""); - } - @Test public void testJsonPath() { // -- json.path @@ -503,7 +499,7 @@ public void testToTreeIssue2190() { } @Test - public void testConvertToTreeSimpleGraph() { + public void testToTreeSimplePath() { /* r:R a:A --------> b:B */ @@ -540,7 +536,185 @@ public void testConvertToTreeSimpleGraph() { } @Test - public void testConvertToTreeComplexGraph() { + public void testToTreeSimpleReversePath() { + /* r:R + a:A <-------- b:B + */ + db.executeTransactionally("CREATE " + "(a: A {nodeName: 'a'})<-[r: R {relName: 'r'}]-(b: B {nodeName: 'b'})"); + + var query = "MATCH path = (n)<-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"b\"," + + " \"r\":[" + + " {" + + " \"nodeName\":\"a\"," + + " \"r._id\":0," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"_id\":1" + + " }" + + "}"; + assertEquals(rows.size(), 1); + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeSimpleBidirectionalPath() { + /* r1:R + --------> + a:A b:B + <-------- + r2:R + */ + db.executeTransactionally("CREATE " + + "(a: A {nodeName: 'a'})<-[r1: R {relName: 'r'}]-(b: B {nodeName: 'b'})," + + "(a)-[r2: R {relName: 'r'}]->(b)"); + + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"r\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r._id\":1," + + " \"r\":[" + + " {" + + " \"nodeName\":\"a\"," + + " \"r._id\":0," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"_id\":1," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"A\"," + + " \"_id\":0" + + " }" + + "}"; + assertEquals(rows.size(), 1); + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeSimpleBidirectionalQuery() { + /* r1:R + a:A --------> b:B + */ + db.executeTransactionally("CREATE (a: A {nodeName: 'a'})-[r1: R {relName: 'r'}]->(b: B {nodeName: 'b'})"); + + // Note this would be returning both the path (a)-[r]->(b) and (b)<-[r]-(a) + // but we only expect a tree starting in 'a' + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"r\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r._id\":0," + + " \"_type\":\"B\"," + + " \"_id\":1," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"A\"," + + " \"_id\":0" + + " }" + + "}"; + assertEquals(rows.size(), 1); + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeBidirectionalPathAndQuery() { + /* r1:R1 r2:R2 + a:A ---------> b:B --------> a + */ + db.executeTransactionally( + "CREATE (a: A {nodeName: 'a'})-[r1: R1 {relName: 'r1'}]->(b: B {nodeName: 'b'})-[r2: R2 {relName: 'r2'}]->(a)"); + + // The query is bidirectional in this case, so + // we would have duplicated paths, but we do not + // expect duplicated trees + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 1); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"b\"," + + " \"_type\":\"B\"," + + " \"_id\":1," + + " \"r2\":[" + + " {" + + " \"nodeName\":\"a\"," + + " \"r1\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r1._id\":0," + + " \"_type\":\"B\"," + + " \"r1.relName\":\"r1\"," + + " \"_id\":1" + + " }" + + " ]," + + " \"_type\":\"A\"," + + " \"r2._id\":1," + + " \"_id\":0," + + " \"r2.relName\":\"r2\"" + + " }" + + " ]" + + " }" + + "}"; + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeComplexGraph() { /* r1:R1 r2:R2 a:A --------> b:B <------ c:C | @@ -609,7 +783,77 @@ public void testConvertToTreeComplexGraph() { } @Test - public void testGraphWithLoops() { + public void testToTreeComplexGraphBidirectionalQuery() { + /* r1:R1 r2:R2 + a:A --------> b:B <------ c:C + | + r3:R3 | + \|/ + d:D + */ + db.executeTransactionally("CREATE " + "(a: A {nodeName: 'a'})-[r1: R1 {relName: 'r1'}]->(b: B {nodeName: 'b'})," + + "(b)<-[r2: R2 {relName: 'r2'}]-(c: C {nodeName: 'c'})," + + "(b)-[r3: R3 {relName: 'r3'}]->(d: D {nodeName: 'd'})"); + + // The query is bidirectional in this case, we don't expect duplicated paths + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 2); + var expectedFirstRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r1\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r3\":[" + + " {" + + " \"nodeName\":\"d\"," + + " \"r3._id\":2," + + " \"r3.relName\":\"r3\"," + + " \"_type\":\"D\"," + + " \"_id\":3" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"r1._id\":0," + + " \"_id\":1," + + " \"r1.relName\":\"r1\"" + + " }" + + " ]" + + " }" + + "}"; + var expectedSecondRow = "{" + " \"tree\":{" + + " \"nodeName\":\"c\"," + + " \"r2\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r2._id\":1," + + " \"_type\":\"B\"," + + " \"r2.relName\":\"r2\"," + + " \"_id\":1" + + " }" + + " ]," + + " \"_type\":\"C\"," + + " \"_id\":2" + + " }" + + "}"; + assertEquals(parseJson(expectedFirstRow), rows.get(0)); + assertEquals(parseJson(expectedSecondRow), rows.get(1)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeGraphWithLoops() { /* r1:R1 r2:R2 a:A ---------> b:B --------> c:C / /|\ @@ -670,16 +914,16 @@ public void testGraphWithLoops() { } @Test - public void testLoopPath() { - /* r1:R1 r2:R2 - a:A ---------> b:B --------> a + public void testToTreeMultiLabelFilters() { + /* r:R + a:A:B -------> c:C */ db.executeTransactionally( - "CREATE (a: A {nodeName: 'a'})-[r1: R1 {relName: 'r1'}]->(b: B {nodeName: 'b'})-[r2: R2 {relName: 'r2'}]->(a)"); + "CREATE " + "(a: A: B {nodeName: 'a & b'})-[r: R {relName: 'r'}]->(c: C {nodeName: 'c'})"); var query = "MATCH path = (n)-[r]->(m)\n" + "WITH COLLECT(path) AS paths\n" - + "CALL apoc.convert.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "CALL apoc.convert.toTree(paths, true, {nodes: { A: ['-nodeName'] } }) YIELD value AS tree\n" + "RETURN tree"; try (Transaction tx = db.beginTx()) { @@ -687,33 +931,24 @@ public void testLoopPath() { var rows = result.stream().collect(Collectors.toList()); assertEquals(rows.size(), 1); + // No nodename under A:B var expectedRow = "{" + " \"tree\":{" - + " \"nodeName\":\"a\"," - + " \"_type\":\"A\"," - + " \"_id\":0," - + " \"r1\":[" + + " \"r\":[" + " {" - + " \"nodeName\":\"b\"," - + " \"r2\":[" - + " {" - + " \"nodeName\":\"a\"," - + " \"r2._id\":1," - + " \"_type\":\"A\"," - + " \"r2.relName\":\"r2\"," - + " \"_id\":0" - + " }" - + " ]," - + " \"_type\":\"B\"," - + " \"r1._id\":0," + + " \"nodeName\":\"c\"," + + " \"r._id\":0," + + " \"_type\":\"C\"," + " \"_id\":1," - + " \"r1.relName\":\"r1\"" + + " \"r.relName\":\"r\"" + " }" - + " ]" + + " ]," + + " \"_type\":\"A:B\"," + + " \"_id\":0" + " }" + "}"; assertEquals(parseJson(expectedRow), rows.get(0)); } catch (Exception e) { - fail("Test failed with message " + e.getMessage()); + fail("Test failed with message " + e + "\nStacktrace: \n" + java.util.Arrays.toString(e.getStackTrace())); } } diff --git a/core/src/test/java/apoc/convert/ConvertPathsToTreeTest.java b/core/src/test/java/apoc/convert/ConvertPathsToTreeTest.java new file mode 100644 index 0000000000..7d92c445ea --- /dev/null +++ b/core/src/test/java/apoc/convert/ConvertPathsToTreeTest.java @@ -0,0 +1,551 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package apoc.convert; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import apoc.util.JsonUtil; +import apoc.util.TestUtil; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; +import org.neo4j.test.rule.DbmsRule; +import org.neo4j.test.rule.ImpermanentDbmsRule; + +public class ConvertPathsToTreeTest { + private Object parseJson(String json) { + return JsonUtil.parse(json, null, null); + } + + @Rule + public DbmsRule db = new ImpermanentDbmsRule(); + + @Before + public void setUp() throws Exception { + TestUtil.registerProcedure(db, Json.class); + } + + @After + public void teardown() { + db.shutdown(); + } + + @After + public void clear() { + db.executeTransactionally("MATCH (n) DETACH DELETE n;"); + } + + @Test + public void testToTreeSimplePath() { + /* r:R + a:A --------> b:B + */ + db.executeTransactionally("CREATE " + "(a: A {nodeName: 'a'})-[r: R {relName: 'r'}]->(b: B {nodeName: 'b'})"); + + var query = "MATCH path = (n)-[r]->(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"r\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r._id\":0," + + " \"_type\":\"B\"," + + " \"_id\":1," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"A\"," + + " \"_id\":0" + + " }" + + "}"; + assertEquals(rows.size(), 1); + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeSimpleReversePath() { + /* r:R + a:A <-------- b:B + */ + db.executeTransactionally("CREATE " + "(a: A {nodeName: 'a'})<-[r: R {relName: 'r'}]-(b: B {nodeName: 'b'})"); + + var query = "MATCH path = (n)<-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"b\"," + + " \"r\":[" + + " {" + + " \"nodeName\":\"a\"," + + " \"r._id\":0," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"_id\":1" + + " }" + + "}"; + assertEquals(rows.size(), 1); + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeSimpleBidirectionalPath() { + /* r1:R + --------> + a:A b:B + <-------- + r2:R + */ + db.executeTransactionally("CREATE " + + "(a: A {nodeName: 'a'})<-[r1: R {relName: 'r'}]-(b: B {nodeName: 'b'})," + + "(a)-[r2: R {relName: 'r'}]->(b)"); + + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"r\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r._id\":1," + + " \"r\":[" + + " {" + + " \"nodeName\":\"a\"," + + " \"r._id\":0," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"_id\":1," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"A\"," + + " \"_id\":0" + + " }" + + "}"; + assertEquals(rows.size(), 1); + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeSimpleBidirectionalQuery() { + /* r1:R + a:A --------> b:B + */ + db.executeTransactionally("CREATE (a: A {nodeName: 'a'})-[r1: R {relName: 'r'}]->(b: B {nodeName: 'b'})"); + + // Note this would be returning both the path (a)-[r]->(b) and (b)<-[r]-(a) + // but we only expect a tree starting in 'a' + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"r\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r._id\":0," + + " \"_type\":\"B\"," + + " \"_id\":1," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"A\"," + + " \"_id\":0" + + " }" + + "}"; + assertEquals(rows.size(), 1); + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeBidirectionalPathAndQuery() { + /* r1:R1 r2:R2 + a:A ---------> b:B --------> a + */ + db.executeTransactionally( + "CREATE (a: A {nodeName: 'a'})-[r1: R1 {relName: 'r1'}]->(b: B {nodeName: 'b'})-[r2: R2 {relName: 'r2'}]->(a)"); + + // The query is bidirectional in this case, so + // we would have duplicated paths, but we do not + // expect duplicated trees + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 1); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"b\"," + + " \"_type\":\"B\"," + + " \"_id\":1," + + " \"r2\":[" + + " {" + + " \"nodeName\":\"a\"," + + " \"r1\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r1._id\":0," + + " \"_type\":\"B\"," + + " \"r1.relName\":\"r1\"," + + " \"_id\":1" + + " }" + + " ]," + + " \"_type\":\"A\"," + + " \"r2._id\":1," + + " \"_id\":0," + + " \"r2.relName\":\"r2\"" + + " }" + + " ]" + + " }" + + "}"; + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeComplexGraph() { + /* r1:R1 r2:R2 + a:A --------> b:B <------ c:C + | + r3:R3 | + \|/ + d:D + */ + db.executeTransactionally("CREATE " + "(a: A {nodeName: 'a'})-[r1: R1 {relName: 'r1'}]->(b: B {nodeName: 'b'})," + + "(b)<-[r2: R2 {relName: 'r2'}]-(c: C {nodeName: 'c'})," + + "(b)-[r3: R3 {relName: 'r3'}]->(d: D {nodeName: 'd'})"); + + var query = "MATCH path = (n)-[r]->(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 2); + var expectedFirstRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r1\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r3\":[" + + " {" + + " \"nodeName\":\"d\"," + + " \"r3._id\":2," + + " \"r3.relName\":\"r3\"," + + " \"_type\":\"D\"," + + " \"_id\":3" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"r1._id\":0," + + " \"_id\":1," + + " \"r1.relName\":\"r1\"" + + " }" + + " ]" + + " }" + + "}"; + var expectedSecondRow = "{" + " \"tree\":{" + + " \"nodeName\":\"c\"," + + " \"r2\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r2._id\":1," + + " \"_type\":\"B\"," + + " \"r2.relName\":\"r2\"," + + " \"_id\":1" + + " }" + + " ]," + + " \"_type\":\"C\"," + + " \"_id\":2" + + " }" + + "}"; + assertEquals(parseJson(expectedFirstRow), rows.get(0)); + assertEquals(parseJson(expectedSecondRow), rows.get(1)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeComplexGraphBidirectionalQuery() { + /* r1:R1 r2:R2 + a:A --------> b:B <------ c:C + | + r3:R3 | + \|/ + d:D + */ + db.executeTransactionally("CREATE " + "(a: A {nodeName: 'a'})-[r1: R1 {relName: 'r1'}]->(b: B {nodeName: 'b'})," + + "(b)<-[r2: R2 {relName: 'r2'}]-(c: C {nodeName: 'c'})," + + "(b)-[r3: R3 {relName: 'r3'}]->(d: D {nodeName: 'd'})"); + + // The query is bidirectional in this case, we don't expect duplicated paths + var query = "MATCH path = (n)-[r]-(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 2); + var expectedFirstRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r1\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r3\":[" + + " {" + + " \"nodeName\":\"d\"," + + " \"r3._id\":2," + + " \"r3.relName\":\"r3\"," + + " \"_type\":\"D\"," + + " \"_id\":3" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"r1._id\":0," + + " \"_id\":1," + + " \"r1.relName\":\"r1\"" + + " }" + + " ]" + + " }" + + "}"; + var expectedSecondRow = "{" + " \"tree\":{" + + " \"nodeName\":\"c\"," + + " \"r2\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r2._id\":1," + + " \"_type\":\"B\"," + + " \"r2.relName\":\"r2\"," + + " \"_id\":1" + + " }" + + " ]," + + " \"_type\":\"C\"," + + " \"_id\":2" + + " }" + + "}"; + assertEquals(parseJson(expectedFirstRow), rows.get(0)); + assertEquals(parseJson(expectedSecondRow), rows.get(1)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeGraphWithLoops() { + /* r1:R1 r2:R2 + a:A ---------> b:B --------> c:C + / /|\ + |___| + r3:R3 + */ + db.executeTransactionally("CREATE " + "(a: A {nodeName: 'a'})-[r1: R1 {relName: 'r1'}]->(b: B {nodeName: 'b'})," + + "(b)-[r2: R2 {relName: 'r2'}]->(c:C {nodeName: 'c'})," + + "(b)-[r3: R3 {relName: 'r3'}]->(b)"); + + var query = "MATCH path = (n)-[r]->(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {sortPaths: false}) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 1); + var expectedRow = "{" + " \"tree\":{" + + " \"nodeName\":\"a\"," + + " \"_type\":\"A\"," + + " \"_id\":0," + + " \"r1\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r2\":[" + + " {" + + " \"nodeName\":\"c\"," + + " \"r2._id\":1," + + " \"_type\":\"C\"," + + " \"r2.relName\":\"r2\"," + + " \"_id\":2" + + " }" + + " ]," + + " \"r3\":[" + + " {" + + " \"nodeName\":\"b\"," + + " \"r3._id\":2," + + " \"r3.relName\":\"r3\"," + + " \"_type\":\"B\"," + + " \"_id\":1" + + " }" + + " ]," + + " \"_type\":\"B\"," + + " \"r1._id\":0," + + " \"_id\":1," + + " \"r1.relName\":\"r1\"" + + " }" + + " ]" + + " }" + + "}"; + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } + + @Test + public void testToTreeMultiLabelFilters() { + /* r:R + a:A:B -------> c:C + */ + db.executeTransactionally( + "CREATE " + "(a: A: B {nodeName: 'a & b'})-[r: R {relName: 'r'}]->(c: C {nodeName: 'c'})"); + + var query = "MATCH path = (n)-[r]->(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.paths.toTree(paths, true, {nodes: { A: ['-nodeName'] } }) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 1); + // No nodename under A:B + var expectedRow = "{" + " \"tree\":{" + + " \"r\":[" + + " {" + + " \"nodeName\":\"c\"," + + " \"r._id\":0," + + " \"_type\":\"C\"," + + " \"_id\":1," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"A:B\"," + + " \"_id\":0" + + " }" + + "}"; + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e + "\nStacktrace: \n" + java.util.Arrays.toString(e.getStackTrace())); + } + } + + @Test + public void testToTreeMultiLabelFiltersForOldProcedure() { + /* r:R + a:A:B -------> c:C + */ + db.executeTransactionally( + "CREATE " + "(a: A: B {nodeName: 'a & b'})-[r: R {relName: 'r'}]->(c: C {nodeName: 'c'})"); + + var query = "MATCH path = (n)-[r]->(m)\n" + + "WITH COLLECT(path) AS paths\n" + + "CALL apoc.convert.toTree(paths, true, {nodes: { A: ['-nodeName'] } }) YIELD value AS tree\n" + + "RETURN tree"; + + try (Transaction tx = db.beginTx()) { + Result result = tx.execute(query); + var rows = result.stream().collect(Collectors.toList()); + + assertEquals(rows.size(), 1); + // No nodename under A:B + var expectedRow = "{" + " \"tree\":{" + + " \"r\":[" + + " {" + + " \"nodeName\":\"c\"," + + " \"r._id\":0," + + " \"_type\":\"C\"," + + " \"_id\":1," + + " \"r.relName\":\"r\"" + + " }" + + " ]," + + " \"_type\":\"A:B\"," + + " \"_id\":0" + + " }" + + "}"; + assertEquals(parseJson(expectedRow), rows.get(0)); + } catch (Exception e) { + fail("Test failed with message " + e.getMessage()); + } + } +}