diff --git a/core/src/main/java/apoc/export/cypher/ExportCypher.java b/core/src/main/java/apoc/export/cypher/ExportCypher.java index fafd0019d3..6d00961d70 100644 --- a/core/src/main/java/apoc/export/cypher/ExportCypher.java +++ b/core/src/main/java/apoc/export/cypher/ExportCypher.java @@ -112,7 +112,7 @@ public Stream query(@Name("query") String query, @Name(value = if (Util.isNullOrEmpty(fileName)) fileName=null; ExportConfig c = new ExportConfig(config); Result result = tx.execute(query); - SubGraph graph = CypherResultSubGraph.from(tx, result, c.getRelsInBetween()); + SubGraph graph = CypherResultSubGraph.from(tx, result, c.getRelsInBetween(), false); String source = String.format("statement: nodes(%d), rels(%d)", Iterables.count(graph.getNodes()), Iterables.count(graph.getRelationships())); diff --git a/core/src/main/java/apoc/export/graphml/ExportGraphML.java b/core/src/main/java/apoc/export/graphml/ExportGraphML.java index 3dd5194934..0fa343f4f1 100644 --- a/core/src/main/java/apoc/export/graphml/ExportGraphML.java +++ b/core/src/main/java/apoc/export/graphml/ExportGraphML.java @@ -128,7 +128,7 @@ public Stream graph(@Name("graph") Map graph, @Name public Stream query(@Name("query") String query, @Name("file") String fileName, @Name("config") Map config) throws Exception { ExportConfig c = new ExportConfig(config); Result result = tx.execute(query); - SubGraph graph = CypherResultSubGraph.from(tx, result, c.getRelsInBetween()); + SubGraph graph = CypherResultSubGraph.from(tx, result, c.getRelsInBetween(), false); String source = String.format("statement: nodes(%d), rels(%d)", Iterables.count(graph.getNodes()), Iterables.count(graph.getRelationships())); return exportGraphML(fileName, source, graph, c); diff --git a/core/src/main/java/org/neo4j/cypher/export/CypherResultSubGraph.java b/core/src/main/java/org/neo4j/cypher/export/CypherResultSubGraph.java index e55769550d..b6ba83d8d6 100644 --- a/core/src/main/java/org/neo4j/cypher/export/CypherResultSubGraph.java +++ b/core/src/main/java/org/neo4j/cypher/export/CypherResultSubGraph.java @@ -57,18 +57,27 @@ void addNode( long id, Node data ) labels.addAll( Iterables.asCollection( data.getLabels() ) ); } - public void add( Relationship rel ) + public void add( Relationship rel, boolean addNodes ) { final long id = rel.getId(); - if ( !relationships.containsKey( id ) ) - { - addRel( id, rel ); - add( rel.getStartNode() ); - add( rel.getEndNode() ); + if (!relationships.containsKey(id)) { + addRel(id, rel); + // start and end nodes will be added only with the `apoc.meta.*` procedures, + // not with the `apoc.export.*.query` ones + if (addNodes) { + add(rel.getStartNode()); + add(rel.getEndNode()); + } } } - public static SubGraph from(Transaction tx, Result result, boolean addBetween) + + + public static SubGraph from(Transaction tx, Result result, boolean addBetween) { + return from(tx, result, addBetween, true); + } + + public static SubGraph from(Transaction tx, Result result, boolean addBetween, boolean addRelNodes) { final CypherResultSubGraph graph = new CypherResultSubGraph(); final List columns = result.columns(); @@ -76,7 +85,7 @@ public static SubGraph from(Transaction tx, Result result, boolean addBetween) for (Map row : loop(result)) { for (String column : columns) { final Object value = row.get(column); - graph.addToGraph(value); + graph.addToGraph(value, addRelNodes); } } } catch (AuthorizationViolationException e) { @@ -160,7 +169,7 @@ private void addRelationshipsBetweenNodes() } } - private void addToGraph( Object value ) + private void addToGraph( Object value, boolean addRelNodes ) { if ( value instanceof Node ) { @@ -168,13 +177,13 @@ private void addToGraph( Object value ) } if ( value instanceof Relationship ) { - add( (Relationship) value ); + add( (Relationship) value, addRelNodes ); } if ( value instanceof Iterable ) { for ( Object inner : (Iterable) value ) { - addToGraph( inner ); + addToGraph( inner, addRelNodes ); } } } diff --git a/core/src/test/java/apoc/export/cypher/ExportCypherTest.java b/core/src/test/java/apoc/export/cypher/ExportCypherTest.java index bf5c05064d..78871ea68f 100644 --- a/core/src/test/java/apoc/export/cypher/ExportCypherTest.java +++ b/core/src/test/java/apoc/export/cypher/ExportCypherTest.java @@ -18,6 +18,7 @@ */ package apoc.export.cypher; +import apoc.export.util.ExportConfig; import apoc.util.BinaryTestUtil; import apoc.util.CompressionAlgo; import apoc.util.MapUtil; @@ -58,6 +59,16 @@ * @since 22.05.16 */ public class ExportCypherTest { + private static final String queryWithRelOnly = "MATCH (start:Foo)-[rel:KNOWS]->(end:Bar) RETURN rel"; + private static final String queryWithStartAndRel = "MATCH (start:Foo)-[rel:KNOWS]->(end:Bar) RETURN start, rel"; + private final static String exportQuery = "CALL apoc.export.cypher.query($query, $file, $config)"; + + private static final String exportDataWithStartAndRel = "MATCH (start:Foo)-[rel:KNOWS]->(end:Bar) " + + "CALL apoc.export.cypher.data([start], [rel], $file, $config) " + + "YIELD nodes, relationships, properties RETURN *"; + private static final String exportDataWithRelOnly = "MATCH (start:Foo)-[rel:KNOWS]->(end:Bar) " + + "CALL apoc.export.cypher.data([], [rel], $file, $config) " + + "YIELD nodes, relationships, properties RETURN *"; private static final Map exportConfig = map("useOptimizations", map("type", "none"), "separateFiles", true, "format", "neo4j-admin"); @@ -167,7 +178,7 @@ public void testExportAllCypherForCypherShell() throws Exception { public void testExportQueryCypherForNeo4j() throws Exception { String fileName = "all.cypher"; String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"), "format", "neo4j-shell")), (r) -> { }); assertEquals(EXPECTED_NEO4J_SHELL, readFile(fileName)); @@ -176,10 +187,95 @@ public void testExportQueryCypherForNeo4j() throws Exception { private static String readFile(String fileName) { return readFile(fileName, CompressionAlgo.NONE); } - + private static String readFile(String fileName, CompressionAlgo algo) { return BinaryTestUtil.readFileToString(new File(directory, fileName), UTF_8, algo); } + @Test + public void testExportQueryOnlyRel() { + final Map config = withoutOptimization( map("format", "neo4j-shell") ); + Map params = Map.of("query", queryWithRelOnly, + "config", config); + assertExportRelOnly(params, exportQuery); + + // check that apoc.export.cypher.data returns consistent results + assertExportRelOnly(params, exportDataWithRelOnly); + } + + @Test + public void testExportQueryOnlyRelWithNodeOfRelsTrue() { + // check that `nodesOfRelationships: true` doesn't change the result + final Map config = withoutOptimization( + map("format", "neo4j-shell", + "nodesOfRelationships", true) + ); + Map params = Map.of("query", queryWithRelOnly, + "config", config); + assertExportRelOnly(params, exportQuery); + + // check that apoc.export.cypher.data returns consistent results + assertExportRelOnly(params, exportDataWithRelOnly); + } + + @Test + public void testExportQueryOnlyRelAndStart() { + final Map config = withoutOptimization( map("format", "neo4j-shell") ); + Map params = Map.of("query", queryWithStartAndRel, + "config", config); + assertExportWithoutEndNode(params, exportQuery); + + // check that apoc.export.cypher.data returns consistent results + assertExportWithoutEndNode(params, exportDataWithStartAndRel); + } + + @Test + public void testExportQueryOnlyRelAndStartWithNodeOfRelsTrue() { + // check that with {nodesOfRelationships: true} the end node is returned as well + final Map config = withoutOptimization( + map("format", "neo4j-shell", + "nodesOfRelationships", true) + ); + Map params = Map.of("query", queryWithStartAndRel, + "config", config); + + String expectedNodesWithEndNode = String.format(EXPECTED_BEGIN_AND_FOO + + "CREATE (:Bar:`UNIQUE IMPORT LABEL` {age:42, name:\"bar\", `UNIQUE IMPORT ID`:1});%n" + + "COMMIT%n"); + String expectedWithEndNode = expectedNodesWithEndNode + EXPECTED_SCHEMA_ONLY_START + EXPECTED_REL_ONLY + EXPECTED_CLEAN_UP; + + commonDataAndQueryAssertions(exportQuery, params, + expectedWithEndNode, 2L, 1L, 5L); + + // apoc.export.cypher.data doesn't accept `nodesOfRelationships` config, + // so it returns the same result as the above one + assertExportWithoutEndNode(params, exportDataWithStartAndRel); + } + + private void assertExportWithoutEndNode(Map params, String exportData) { + commonDataAndQueryAssertions(exportData, params, + EXPECTED_WITHOUT_END_NODE, 1L, 1L, 3L); + } + + private void assertExportRelOnly(Map params, String exportData) { + commonDataAndQueryAssertions(exportData, params, + EXPECTED_REL_ONLY, 0L, 1L, 1L); + } + + private void commonDataAndQueryAssertions(String query, Map config, + String expectedOutput, long nodes, long relationships, long properties) { + String fileName = "testFile.cypher"; + + Map params = map("file", fileName); + params.putAll(config); + + TestUtil.testCall(db, query, params, r -> { + assertEquals(nodes, r.get("nodes")); + assertEquals(relationships, r.get("relationships")); + assertEquals(properties, r.get("properties")); + }); + assertEquals(expectedOutput, readFile(fileName)); + } + @Test public void testExportCypherAdminOperationErrorMessage() { @@ -191,12 +287,7 @@ public void testExportCypherAdminOperationErrorMessage() { for (String query : invalidQueries) { QueryExecutionException e = Assert.assertThrows(QueryExecutionException.class, - () -> TestUtil.testCall(db, "" + - "CALL apoc.export.cypher.query(" + - "$query," + - "$file," + - "$config" + - ")", + () -> TestUtil.testCall(db, exportQuery, MapUtil.map( "query", query, "file", filename, @@ -263,7 +354,7 @@ public void testExportAllCypherSchemaWithSaveConstraintNames() throws Exception final Map config = new HashMap<>(ExportCypherTest.exportConfig); config.put("saveConstraintNames", true); String fileName = "all.cypher"; - TestUtil.testCall(db, "CALL apoc.export.cypher.all($file,$config)", + TestUtil.testCall(db, "CALL apoc.export.cypher.all($file,$config)", map("file", fileName, "config", config), (r) -> assertResults(fileName, r, "database")); final String expectedFile = String.format(EXPECTED_SCHEMA_WITH_NAMES, StringUtils.EMPTY, StringUtils.EMPTY, " consBar", " uniqueConstraintComposite"); @@ -335,7 +426,7 @@ private void assertResults(String fileName, Map r, final String public void testExportQueryCypherPlainFormat() throws Exception { String fileName = "all.cypher"; String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"), "format", "plain")), (r) -> { }); assertEquals(EXPECTED_PLAIN, readFile(fileName)); @@ -345,7 +436,7 @@ public void testExportQueryCypherPlainFormat() throws Exception { public void testExportQueryCypherFormatUpdateAll() throws Exception { String fileName = "all.cypher"; String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"), "format", "neo4j-shell", "cypherFormat", "updateAll")), (r) -> { }); assertEquals(EXPECTED_NEO4J_MERGE, readFile(fileName)); @@ -355,7 +446,7 @@ public void testExportQueryCypherFormatUpdateAll() throws Exception { public void testExportQueryCypherFormatAddStructure() throws Exception { String fileName = "all.cypher"; String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"), "format", "neo4j-shell", "cypherFormat", "addStructure")), (r) -> { }); assertEquals(EXPECTED_NODES_MERGE_ON_CREATE_SET + EXPECTED_SCHEMA_EMPTY + EXPECTED_RELATIONSHIPS + EXPECTED_CLEAN_UP_EMPTY, readFile(fileName)); @@ -365,7 +456,7 @@ public void testExportQueryCypherFormatAddStructure() throws Exception { public void testExportQueryCypherFormatUpdateStructure() throws Exception { String fileName = "all.cypher"; String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"), "format", "neo4j-shell", "cypherFormat", "updateStructure")), (r) -> { }); assertEquals(EXPECTED_NODES_EMPTY + EXPECTED_SCHEMA_EMPTY + EXPECTED_RELATIONSHIPS_MERGE_ON_CREATE_SET + EXPECTED_CLEAN_UP_EMPTY, readFile(fileName)); @@ -407,7 +498,7 @@ public void testExportCypherNodePoint() throws FileNotFoundException { "(:Bar {place3d:point({ longitude: 12.78, latitude: 56.7, height: 100 })})"); String fileName = "temporalPoint.cypher"; String query = "MATCH (n:Test)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"),"format", "neo4j-shell")), (r) -> {}); assertEquals(EXPECTED_CYPHER_POINT, readFile(fileName)); @@ -423,7 +514,7 @@ public void testExportCypherNodeDate() throws FileNotFoundException { "(:Bar {datetime:datetime('2018-10-30T12:50:35.556')})"); String fileName = "temporalDate.cypher"; String query = "MATCH (n:Test)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"),"format", "neo4j-shell")), (r) -> {}); assertEquals(EXPECTED_CYPHER_DATE, readFile(fileName)); @@ -438,7 +529,7 @@ public void testExportCypherNodeTime() throws FileNotFoundException { "(:Bar {datetime:datetime('2018-10-30T12:50:35.556+0100')})"); String fileName = "temporalTime.cypher"; String query = "MATCH (n:Test)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"),"format", "neo4j-shell")), (r) -> {}); assertEquals(EXPECTED_CYPHER_TIME, readFile(fileName)); @@ -452,7 +543,7 @@ public void testExportCypherNodeDuration() throws FileNotFoundException { "(:Bar {duration:duration('P5M1.5D')})"); String fileName = "temporalDuration.cypher"; String query = "MATCH (n:Test)-[r]-(m) RETURN n,r,m"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"),"format", "neo4j-shell")), (r) -> {}); assertEquals(EXPECTED_CYPHER_DURATION, readFile(fileName)); @@ -463,7 +554,7 @@ public void testExportWithAscendingLabels() throws FileNotFoundException { db.executeTransactionally("CREATE (f:User:User1:User0:User12 {name:'Alan'})"); String fileName = "ascendingLabels.cypher"; String query = "MATCH (f:User) WHERE f.name='Alan' RETURN f"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query,$file,$config)", + TestUtil.testCall(db, exportQuery, map("file", fileName, "query", query, "config", map("useOptimizations", map("type", "none"),"format", "neo4j-shell")), (r) -> {}); assertEquals(EXPECTED_CYPHER_LABELS_ASCENDEND, readFile(fileName)); @@ -670,7 +761,7 @@ public void exportMultiTokenIndex() { ":commit%n"); // when - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query, $file, $config)", + TestUtil.testCall(db, exportQuery, map("query", query, "file", file, "config", config), (r) -> { // then @@ -716,7 +807,7 @@ public void shouldNotCreateUniqueImportIdForUniqueConstraint() { */ final String expected = "UNWIND [{name:\"A\", properties:{}}] AS row\n" + "CREATE (n:Bar{name: row.name}) SET n += row.properties SET n:Baz"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query, $file, $config)", + TestUtil.testCall(db, exportQuery, map("file", null, "query", query, "config", map("format", "plain", "stream", true)), (r) -> { final String cypherStatements = (String) r.get("cypherStatements"); String unwind = Stream.of(cypherStatements.split(";")) @@ -752,7 +843,7 @@ public void shouldQuotePropertyNameStartingWithDollarCharacter() { String query = "MATCH (n:Baz) RETURN n"; final String expected = "UNWIND [{name:\"A\", properties:{`$lock`:true}}] AS row\n" + "CREATE (n:Bar{name: row.name}) SET n += row.properties SET n:Baz"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query, $file, $config)", + TestUtil.testCall(db, exportQuery, map("file", null, "query", query, "config", map("format", "plain", "stream", true)), (r) -> { final String cypherStatements = (String) r.get("cypherStatements"); String unwind = Stream.of(cypherStatements.split(";")) @@ -807,7 +898,7 @@ public void shouldHandleTwoLabelsWithTwoUniqueConstraintsEach() { "MATCH (start:Bar{name: row.start.name})\n" + "MATCH (end:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.end._id})\n" + "CREATE (start)-[r:KNOWS]->(end) SET r += row.properties"; - TestUtil.testCall(db, "CALL apoc.export.cypher.query($query, $file, $config)", + TestUtil.testCall(db, exportQuery, map("file", null, "query", query, "config", map("format", "plain", "stream", true)), (r) -> { final String cypherStatements = (String) r.get("cypherStatements"); String unwind = Stream.of(cypherStatements.split(";")) @@ -915,7 +1006,17 @@ private void assertResultsOdd(String fileName, Map r) { assertTrue("Should get time greater than 0",((long) r.get("time")) >= 0); } + private Map withoutOptimization(Map map) { + map.put("useOptimizations", map("type", ExportConfig.OptimizationType.NONE.name())); + return map; + } + static class ExportCypherResults { + static final String EXPECTED_ISOLATED_NODE = "CREATE (:Bar:`UNIQUE IMPORT LABEL` {age:12, `UNIQUE IMPORT ID`:2});\n"; + static final String EXPECTED_BAR_END_NODE = "CREATE (:Bar {age:42, name:\"bar\"});%n"; + static final String EXPECTED_BEGIN_AND_FOO = "BEGIN%n" + + "CREATE (:Foo:`UNIQUE IMPORT LABEL` {born:date('2018-10-31'), name:\"foo\", `UNIQUE IMPORT ID`:0});%n"; + static final String EXPECTED_NODES = String.format("BEGIN%n" + "CREATE (:Foo:`UNIQUE IMPORT LABEL` {born:date('2018-10-31'), name:\"foo\", `UNIQUE IMPORT ID`:0});%n" + @@ -935,6 +1036,17 @@ static class ExportCypherResults { static final String EXPECTED_NODES_EMPTY = String.format("BEGIN%n" + "COMMIT%n"); + private static final String EXPECTED_IDX_FOO = "CREATE BTREE INDEX FOR (node:Foo) ON (node.name);%n"; + + private static final String EXPECTED_UNIQUE_IMPORT_CONSTRAINT_AND_AWAIT = "CREATE CONSTRAINT ON (node:`UNIQUE IMPORT LABEL`) ASSERT (node.`UNIQUE IMPORT ID`) IS UNIQUE;%n" + + "COMMIT%n" + + "SCHEMA AWAIT%n"; + + private static final String EXPECTED_CONSTRAINTS_AND_AWAIT = + "CREATE CONSTRAINT%s ON (node:Bar) ASSERT (node.name) IS UNIQUE;%n" + + "CREATE CONSTRAINT%s ON (node:Bar) ASSERT (node.name, node.age) IS UNIQUE;%n" + + EXPECTED_UNIQUE_IMPORT_CONSTRAINT_AND_AWAIT; + public static final String EXPECTED_BAR_FOO_INDEXES = "CREATE BTREE INDEX FOR (node:Bar) ON (node.first_name, node.last_name);%n" + "CREATE BTREE INDEX FOR (node:Foo) ON (node.name);%n"; @@ -951,11 +1063,15 @@ static class ExportCypherResults { static final String EXPECTED_SCHEMA_WITH_NAMES = "BEGIN%n" + "CREATE BTREE INDEX%s FOR (node:Bar) ON (node.first_name, node.last_name);%n" + "CREATE BTREE INDEX%s FOR (node:Foo) ON (node.name);%n" + - "CREATE CONSTRAINT%s ON (node:Bar) ASSERT (node.name) IS UNIQUE;%n" + - "CREATE CONSTRAINT%s ON (node:Bar) ASSERT (node.name, node.age) IS UNIQUE;%n" + - "CREATE CONSTRAINT ON (node:`UNIQUE IMPORT LABEL`) ASSERT (node.`UNIQUE IMPORT ID`) IS UNIQUE;%n" + - "COMMIT%n" + - "SCHEMA AWAIT%n"; + EXPECTED_CONSTRAINTS_AND_AWAIT; + + static final String EXPECTED_SCHEMA_ONLY_START = String.format("BEGIN%n" + + EXPECTED_IDX_FOO + + EXPECTED_UNIQUE_IMPORT_CONSTRAINT_AND_AWAIT); + static final String EXPECTED_REL_ONLY = "BEGIN\n" + + "MATCH (n1:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:0}), (n2:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:1}) CREATE (n1)-[r:KNOWS {since:2016}]->(n2);\n" + + "COMMIT\n"; + static final String EXPECTED_SCHEMA_EMPTY = String.format("BEGIN%n" + "COMMIT%n" + @@ -1537,6 +1653,19 @@ static class ExportCypherResults { .replace(NEO4J_SHELL.commit(), CYPHER_SHELL.commit()) .replace(NEO4J_SHELL.schemaAwait(), EXPECTED_INDEXES_AWAIT) .replace(NEO4J_SHELL.schemaAwait(), CYPHER_SHELL.schemaAwait()); + + static final String EXPECTED_WITHOUT_END_NODE = String.format(EXPECTED_BEGIN_AND_FOO + "COMMIT%n") + + EXPECTED_SCHEMA_ONLY_START + + EXPECTED_REL_ONLY + + EXPECTED_CLEAN_UP; + + public static String convertToCypherShellFormat(String input) { + return input + .replace( NEO4J_SHELL.begin(), CYPHER_SHELL.begin() ) + .replace( NEO4J_SHELL.commit(), CYPHER_SHELL.commit() ) + .replace( NEO4J_SHELL.schemaAwait(), EXPECTED_INDEXES_AWAIT ) + .replace( NEO4J_SHELL.schemaAwait(), CYPHER_SHELL.schemaAwait() ); + } } } diff --git a/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java b/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java index fbae09beea..377761d76e 100644 --- a/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java +++ b/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java @@ -49,21 +49,7 @@ import java.util.Set; import static apoc.ApocConfig.EXPORT_TO_FILE_ERROR; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_DATA; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_FALSE; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_READ_NODE_EDGE; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TINKER; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_EMPTY; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_NO_DATA_KEY; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_PATH; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_PATH_CAMEL_CASE; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_PATH_CAPTION; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_PATH_CAPTION_TINKER; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_PATH_WRONG_CAPTION; -import static apoc.export.graphml.ExportGraphMLTestUtil.EXPECTED_TYPES_WITHOUT_CHAR_DATA_KEYS; -import static apoc.export.graphml.ExportGraphMLTestUtil.assertXMLEquals; -import static apoc.export.graphml.ExportGraphMLTestUtil.setUpGraphMl; +import static apoc.export.graphml.ExportGraphMLTestUtil.*; import static apoc.util.BinaryTestUtil.getDecompressedData; import static apoc.util.BinaryTestUtil.fileToBinary; import static apoc.util.MapUtil.map; @@ -87,6 +73,17 @@ * @since 22.05.16 */ public class ExportGraphMLTest { + private static final String exportQueryStartAndRel = + "CALL apoc.export.graphml.query('MATCH (start:Start)-[rel:REL]->(end:End) RETURN start, rel', $file, $config)"; + private static final String exportQueryRelOnly = + "CALL apoc.export.graphml.query('MATCH (start:Start)-[rel:REL]->(end:End) RETURN rel', $file, $config)"; + + private static final String exportDataWithRelOnly = "MATCH (start:Start)-[rel:REL]->(end:End) " + + "CALL apoc.export.graphml.data([], [rel], $file, {}) " + + "YIELD nodes, relationships, properties RETURN *"; + private static final String exportDataWithStartAndRel = "MATCH (start:Start)-[rel:REL]->(end:End) " + + "CALL apoc.export.graphml.data([start], [rel], $file, $config) " + + "YIELD nodes, relationships, properties RETURN *"; @Rule public TestName testName = new TestName(); @@ -599,6 +596,97 @@ public void testExportGraphGraphMLQueryGephi() { assertXMLEquals(output, EXPECTED_TYPES_PATH); } + @Test + public void testExportOnlyRel() { + db.executeTransactionally("CREATE (:Start {startId: 1})-[:REL {foo: 'bar'}]->(:End {endId: '1'})"); + + Map params = Map.of("config", Map.of()); + assertExportRelOnly(exportQueryRelOnly, params); + + // check that apoc.export.cypher.data returns consistent results + assertExportRelOnly(exportDataWithRelOnly, params); + + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + + @Test + public void testExportOnlyRelWithNodesOfRelsTrue() { + db.executeTransactionally("CREATE (:Start {startId: 1})-[:REL {foo: 'bar'}]->(:End {endId: '1'})"); + + Map params = Map.of("config", Map.of("nodesOfRelationships", true)); + + assertExportRelOnly(exportQueryRelOnly, params); + + // check that apoc.export.cypher.data returns consistent results + assertExportRelOnly(exportDataWithRelOnly, params); + + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + + @Test + public void testExportStartAndRel() { + db.executeTransactionally("CREATE (:Start {startId: 1})-[:REL {foo: 'bar'}]->(:End {endId: '1'})"); + + Map params = Map.of("config", Map.of()); + assertExportWithoutEndNode(exportQueryStartAndRel, params); + + // check that apoc.export.graphml.data returns consistent results + assertExportWithoutEndNode(exportDataWithStartAndRel, params); + + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + + @Test + public void testExportStartAndRelWithNodesOfRelsTrue() { + db.executeTransactionally("CREATE (:Start {startId: 1})-[:REL {foo: 'bar'}]->(:End {endId: '1'})"); + + // check that with the `nodesOfRelationships` the end node is returned as well + Map params = Map.of("config", Map.of("nodesOfRelationships", true)); + + commonDataAndQueryAssertions(exportQueryStartAndRel, params, + EXPECTED_START_AND_REL_QUERY, 2L, 1L, 3L); + + // apoc.export.graphml.data doesn't accept `nodesOfRelationships` config, + // so it returns the same result as the above one + assertExportWithoutEndNode(exportDataWithStartAndRel, params); + + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + + private void assertExportWithoutEndNode(String exportQuery, Map params) { + String expectedWithoutEndNode = String.format(HEADER + + START_NODE_KEYS_QUERY + LABEL_KEY_QUERY + EDGES_KEYS_QUERY + + GRAPH + + START_NODE_QUERY + EDGES_QUERY + + FOOTER); + + commonDataAndQueryAssertions(exportQuery, params, + expectedWithoutEndNode, 1L, 1L, 2L); + } + + private void assertExportRelOnly(String exportQuery, Map config) { + String expectedWithoutNodes = String.format(HEADER + EDGES_KEYS_QUERY + GRAPH + EDGES_QUERY + FOOTER); + + commonDataAndQueryAssertions(exportQuery, config, + expectedWithoutNodes, 0L, 1L, 1L); + } + + private void commonDataAndQueryAssertions(String query, Map config, + String expectedOutput, long nodes, long relationships, long properties) { + File output = new File(directory, "testFile.graphml"); + + Map params = Util.map("file", output.getAbsolutePath()); + params.putAll(config); + + TestUtil.testCall(db, query, params, + r -> { + assertEquals(nodes, r.get("nodes")); + assertEquals(relationships, r.get("relationships")); + assertEquals(properties, r.get("properties")); + }); + assertXMLEquals(output, expectedOutput); + } + @Test public void testExportGraphGraphMLQueryGephiWithArrayCaption() { File output = new File(directory, "query.graphml"); diff --git a/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java b/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java index 5cf1194e78..6a121cb423 100644 --- a/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java +++ b/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java @@ -44,8 +44,8 @@ public class ExportGraphMLTestUtil { private static final String KEY_TYPES_EMPTY = "%n" + "%n" + "%n"; - private static final String GRAPH = "%n"; - private static final String HEADER = "%n" + + public static final String GRAPH = "%n"; + public static final String HEADER = "%n" + "%n"; private static final String KEY_TYPES_FALSE = "%n" + "%n" + @@ -122,7 +122,7 @@ public class ExportGraphMLTestUtil { "C:\\bright\\itecembed\\obj\\ada\\b3_status.ads\n" + ""; - private static final String FOOTER = "%n" + + public static final String FOOTER = "%n" + ""; private static final String DATA_PATH = ":Foo:Foo0:Foo2foo{\"crs\":\"wgs-84-3d\",\"latitude\":12.78,\"longitude\":56.7,\"height\":100.0}foo2018-10-10%n" + @@ -159,6 +159,20 @@ public class ExportGraphMLTestUtil { public static final String EXPECTED_TYPES_EMPTY = String.format(HEADER + KEY_TYPES_EMPTY + GRAPH + DATA_EMPTY + FOOTER); public static final String EXPECTED_TYPES_NO_DATA_KEY = String.format(HEADER + KEY_TYPES_NO_DATA_KEY + GRAPH + DATA_NO_DATA_KEY + FOOTER); + public static final String EDGES_QUERY = "RELbar%n"; + public static final String EDGES_KEYS_QUERY = "%n" + + "%n"; + + public static final String START_NODE_QUERY = ":Start1%n"; + public static final String START_NODE_KEYS_QUERY = "%n"; + public static final String LABEL_KEY_QUERY = "%n"; + public static final String END_NODE_QUERY = ":End1%n"; + public static final String END_NODE_KEYS_QUERY = "%n"; + public static final String EXPECTED_START_AND_REL_QUERY = String.format(HEADER + + END_NODE_KEYS_QUERY + START_NODE_KEYS_QUERY + LABEL_KEY_QUERY + EDGES_KEYS_QUERY + + GRAPH + + START_NODE_QUERY + END_NODE_QUERY + EDGES_QUERY + + FOOTER); public static void assertXMLEquals(Object output, String xmlString) { List attrsWithNodeIds = Arrays.asList("id", "source", "target");