|
1 | 1 | package apoc.export.cypher; |
2 | 2 |
|
| 3 | +import apoc.export.util.ExportConfig; |
3 | 4 | import apoc.graph.Graphs; |
| 5 | +import apoc.schema.Schemas; |
4 | 6 | import apoc.util.BinaryTestUtil; |
5 | 7 | import apoc.util.CompressionAlgo; |
6 | 8 | import apoc.util.TestUtil; |
|
12 | 14 | import org.junit.rules.TestName; |
13 | 15 | import org.neo4j.configuration.GraphDatabaseSettings; |
14 | 16 | import org.neo4j.graphdb.QueryExecutionException; |
| 17 | +import org.neo4j.graphdb.Relationship; |
| 18 | +import org.neo4j.graphdb.ResourceIterator; |
15 | 19 | import org.neo4j.test.rule.DbmsRule; |
16 | 20 | import org.neo4j.test.rule.ImpermanentDbmsRule; |
17 | 21 |
|
|
28 | 32 | import static apoc.util.BinaryTestUtil.getDecompressedData; |
29 | 33 | import static apoc.util.Util.map; |
30 | 34 | import static java.nio.charset.StandardCharsets.UTF_8; |
31 | | -import static org.junit.Assert.*; |
| 35 | +import static org.junit.Assert.assertEquals; |
| 36 | +import static org.junit.Assert.assertFalse; |
| 37 | +import static org.junit.Assert.assertNull; |
| 38 | +import static org.junit.Assert.assertTrue; |
32 | 39 | import static org.neo4j.configuration.SettingImpl.newBuilder; |
33 | 40 | import static org.neo4j.configuration.SettingValueParsers.BOOL; |
34 | 41 |
|
@@ -61,7 +68,7 @@ public class ExportCypherTest { |
61 | 68 | @Before |
62 | 69 | public void setUp() throws Exception { |
63 | 70 | apocConfig().setProperty(APOC_EXPORT_FILE_ENABLED, true); |
64 | | - TestUtil.registerProcedure(db, ExportCypher.class, Graphs.class); |
| 71 | + TestUtil.registerProcedure(db, ExportCypher.class, Graphs.class, Schemas.class); |
65 | 72 | db.executeTransactionally("CREATE RANGE INDEX barIndex FOR (n:Bar) ON (n.first_name, n.last_name)"); |
66 | 73 | db.executeTransactionally("CREATE RANGE INDEX fooIndex FOR (n:Foo) ON (n.name)"); |
67 | 74 | db.executeTransactionally("CREATE CONSTRAINT uniqueConstraint FOR (b:Bar) REQUIRE b.name IS UNIQUE"); |
@@ -845,6 +852,133 @@ public void shouldSaveCorrectlyRelIndexesWithNameOptimized() throws FileNotFound |
845 | 852 | db.executeTransactionally("DROP INDEX rel_index_name"); |
846 | 853 | } |
847 | 854 |
|
| 855 | + @Test |
| 856 | + public void testIssue2886OptimizationsNoneAndCypherFormatCreate() { |
| 857 | + String expected = "CREATE (:Person:`UNIQUE IMPORT LABEL` {name:\"First\", `UNIQUE IMPORT ID`:3});\n" + |
| 858 | + "CREATE (:Project:`UNIQUE IMPORT LABEL` {`UNIQUE IMPORT ID`:4});\n" + |
| 859 | + "CREATE (:Person:`UNIQUE IMPORT LABEL` {name:\"Second\", `UNIQUE IMPORT ID`:5});\n" + |
| 860 | + "CREATE (:Project:`UNIQUE IMPORT LABEL` {`UNIQUE IMPORT ID`:6});\n" + |
| 861 | + EXPECTED_2886_SCHEMA + |
| 862 | + EXPECTED_2886_RELS_WITHOUT_OPTIMIZATION + |
| 863 | + EXPECTED_2886_CLEANUP; |
| 864 | + |
| 865 | + final Map<String, Object> config = map("cypherFormat", "create"); |
| 866 | + issue2886Common(expected, withoutOptimization(config), true); |
| 867 | + } |
| 868 | + |
| 869 | + @Test |
| 870 | + public void testIssue2886OptimizationsNoneAndCypherFormatAddStructure() { |
| 871 | + String expected = "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:3}) ON CREATE SET n.name=\"First\", n:Person;\n" + |
| 872 | + "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:4}) ON CREATE SET n:Project;\n" + |
| 873 | + "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:5}) ON CREATE SET n.name=\"Second\", n:Person;\n" + |
| 874 | + "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:6}) ON CREATE SET n:Project;\n" + |
| 875 | + EXPECTED_2886_RELS_WITHOUT_OPTIMIZATION; |
| 876 | + final Map<String, Object> config = map("cypherFormat", "addStructure"); |
| 877 | + issue2886Common(expected, withoutOptimization(config), true); |
| 878 | + } |
| 879 | + |
| 880 | + @Test |
| 881 | + public void testIssue2886OptimizationsNoneAndCypherFormatUpdateStructure() { |
| 882 | + String expected = "MATCH (n1:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:3}), (n2:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:4}) MERGE (n1)-[r:WORKS_FOR]->(n2) ON CREATE SET r.id=1;\n" + |
| 883 | + "MATCH (n1:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:5}), (n2:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:6}) MERGE (n1)-[r:WORKS_FOR]->(n2) ON CREATE SET r.id=2;\n"; |
| 884 | + final Map<String, Object> config = map("cypherFormat", "updateStructure"); |
| 885 | + issue2886Common(expected, withoutOptimization(config), false); |
| 886 | + } |
| 887 | + |
| 888 | + @Test |
| 889 | + public void testIssue2886OptimizationsNoneAndCypherFormatUpdateAll() { |
| 890 | + String expected = "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:3}) SET n.name=\"First\", n:Person;\n" + |
| 891 | + "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:4}) SET n:Project;\n" + |
| 892 | + "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:5}) SET n.name=\"Second\", n:Person;\n" + |
| 893 | + "MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:6}) SET n:Project;\n" + |
| 894 | + EXPECTED_2886_SCHEMA + |
| 895 | + "MATCH (n1:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:3}), (n2:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:4}) MERGE (n1)-[r:WORKS_FOR]->(n2) SET r.id=1;\n" + |
| 896 | + "MATCH (n1:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:5}), (n2:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:6}) MERGE (n1)-[r:WORKS_FOR]->(n2) SET r.id=2;\n" + |
| 897 | + EXPECTED_2886_CLEANUP; |
| 898 | + final Map<String, Object> config = map("cypherFormat", "updateAll"); |
| 899 | + issue2886Common(expected, withoutOptimization(config), true); |
| 900 | + } |
| 901 | + |
| 902 | + @Test |
| 903 | + public void testIssue2886CypherFormatCreate() { |
| 904 | + final Map<String, Object> config = map("cypherFormat", "create"); |
| 905 | + String expected = String.format(EXPECTED_2886_UNWIND, "CREATE"); |
| 906 | + issue2886Common(expected, config, true); |
| 907 | + } |
| 908 | + |
| 909 | + @Test |
| 910 | + public void testIssue2886CypherFormatAddStructure() { |
| 911 | + final Map<String, Object> config = map("cypherFormat", "addStructure"); |
| 912 | + issue2886Common(EXPECTED_2886_ADD_STRUCTURE, config, true); |
| 913 | + } |
| 914 | + |
| 915 | + @Test |
| 916 | + public void testIssue2886CypherFormatUpdateStructure() { |
| 917 | + final Map<String, Object> config = map("cypherFormat", "updateStructure"); |
| 918 | + final String expected = String.format(EXPECTED_2886_UPDATE_STRUCTURE, "MERGE"); |
| 919 | + issue2886Common(expected, config, false); |
| 920 | + } |
| 921 | + |
| 922 | + @Test |
| 923 | + public void testIssue2886CypherFormatUpdateAll() { |
| 924 | + final Map<String, Object> config = map("cypherFormat", "updateAll"); |
| 925 | + String expected = String.format(EXPECTED_2886_UNWIND, "MERGE"); |
| 926 | + issue2886Common(expected, config, true); |
| 927 | + } |
| 928 | + |
| 929 | + private Map<String, Object> withoutOptimization(Map<String, Object> map) { |
| 930 | + map.put("useOptimizations", map("type", ExportConfig.OptimizationType.NONE.name())); |
| 931 | + return map; |
| 932 | + } |
| 933 | + |
| 934 | + private void issue2886Common(String expected, Map<String, Object> config, boolean isRountrip) { |
| 935 | + db.executeTransactionally("match (n) detach delete n"); |
| 936 | + |
| 937 | + final String startOne = "First"; |
| 938 | + final long relOne = 1L; |
| 939 | + final String startTwo = "Second"; |
| 940 | + final long relTwo = 2L; |
| 941 | + db.executeTransactionally("create (:Person {name: $name})-[:WORKS_FOR {id: $id}]->(:Project)", |
| 942 | + map("name", startOne, "id", relOne)); |
| 943 | + db.executeTransactionally("create (:Person {name: $name})-[:WORKS_FOR {id: $id}]->(:Project)", |
| 944 | + map("name", startTwo, "id", relTwo)); |
| 945 | + |
| 946 | + final Map<String, Object> conf = map("format", "plain"); |
| 947 | + conf.putAll(config); |
| 948 | + final String cypherStatements = db.executeTransactionally("CALL apoc.export.cypher.all(null, $config)", |
| 949 | + map("config", conf), |
| 950 | + r -> (String) r.next().get("cypherStatements")); |
| 951 | + |
| 952 | + beforeAfterIssue2886(startOne, relOne, startTwo, relTwo); |
| 953 | + |
| 954 | + assertEquals(expected, cypherStatements); |
| 955 | + |
| 956 | + if (!isRountrip) { |
| 957 | + return; |
| 958 | + } |
| 959 | + db.executeTransactionally("match (n) detach delete n"); |
| 960 | + db.executeTransactionally("call apoc.schema.assert({}, {})"); |
| 961 | + |
| 962 | + for (String i: cypherStatements.split(";\n")) { |
| 963 | + db.executeTransactionally(i); |
| 964 | + } |
| 965 | + |
| 966 | + beforeAfterIssue2886(startOne, relOne, startTwo, relTwo); |
| 967 | + } |
| 968 | + |
| 969 | + private void beforeAfterIssue2886(String startOne, long relOne, String startTwo, long relTwo) { |
| 970 | + TestUtil.testResult(db, "MATCH (start:Person)-[rel:WORKS_FOR]->(end:Project) RETURN rel ORDER BY rel.id", r -> { |
| 971 | + final ResourceIterator<Relationship> rels = r.columnAs("rel"); |
| 972 | + Relationship rel = rels.next(); |
| 973 | + assertEquals(startOne, rel.getStartNode().getProperty("name")); |
| 974 | + assertEquals(relOne, rel.getProperty("id")); |
| 975 | + rel = rels.next(); |
| 976 | + assertEquals(startTwo, rel.getStartNode().getProperty("name")); |
| 977 | + assertEquals(relTwo, rel.getProperty("id")); |
| 978 | + assertFalse(rels.hasNext()); |
| 979 | + }); |
| 980 | + } |
| 981 | + |
848 | 982 | private void relIndexTestCommon(String fileName, String expectedSchema, Map<String, Object> config) throws FileNotFoundException { |
849 | 983 | Map<String, Object> exportConfig = map("separateFiles", true, "format", "neo4j-shell"); |
850 | 984 | exportConfig.putAll(config); |
@@ -1410,6 +1544,50 @@ static class ExportCypherResults { |
1410 | 1544 | "MATCH (start:Person{surname: row.start.surname, name: row.start.name})%n" + |
1411 | 1545 | "MATCH (end:Person{surname: row.end.surname, name: row.end.name})%n" + |
1412 | 1546 | "CREATE (start)-[r:KNOWS]->(end) SET r += row.properties;%n"); |
| 1547 | + |
| 1548 | + static final String EXPECTED_2886_SCHEMA = """ |
| 1549 | + CREATE RANGE INDEX FOR (n:Bar) ON (n.first_name, n.last_name); |
| 1550 | + CREATE RANGE INDEX FOR (n:Foo) ON (n.name); |
| 1551 | + CREATE CONSTRAINT uniqueConstraint FOR (node:Bar) REQUIRE (node.name) IS UNIQUE; |
| 1552 | + CREATE CONSTRAINT UNIQUE_IMPORT_NAME FOR (node:`UNIQUE IMPORT LABEL`) REQUIRE (node.`UNIQUE IMPORT ID`) IS UNIQUE; |
| 1553 | + """; |
| 1554 | + |
| 1555 | + static final String EXPECTED_2886_CLEANUP = """ |
| 1556 | + MATCH (n:`UNIQUE IMPORT LABEL`) WITH n LIMIT 20000 REMOVE n:`UNIQUE IMPORT LABEL` REMOVE n.`UNIQUE IMPORT ID`; |
| 1557 | + DROP CONSTRAINT UNIQUE_IMPORT_NAME; |
| 1558 | + """; |
| 1559 | + |
| 1560 | + static final String EXPECTED_2886_UPDATE_STRUCTURE = """ |
| 1561 | + UNWIND [{start: {_id:3}, end: {_id:4}, properties:{id:1}}, {start: {_id:5}, end: {_id:6}, properties:{id:2}}] AS row |
| 1562 | + MATCH (start:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.start._id}) |
| 1563 | + MATCH (end:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.end._id}) |
| 1564 | + %1$s (start)-[r:WORKS_FOR]->(end) SET r += row.properties; |
| 1565 | + """; |
| 1566 | + |
| 1567 | + static final String EXPECTED_2886_UNWIND = EXPECTED_2886_SCHEMA + |
| 1568 | + "UNWIND [{_id:4, properties:{}}, {_id:6, properties:{}}] AS row\n" + |
| 1569 | + "%1$s (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) SET n += row.properties SET n:Project;\n" + |
| 1570 | + "UNWIND [{_id:3, properties:{name:\"First\"}}, {_id:5, properties:{name:\"Second\"}}] AS row\n" + |
| 1571 | + "%1$s (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) SET n += row.properties SET n:Person;\n" + |
| 1572 | + EXPECTED_2886_UPDATE_STRUCTURE + |
| 1573 | + EXPECTED_2886_CLEANUP; |
| 1574 | + |
| 1575 | + static final String EXPECTED_2886_ADD_STRUCTURE = """ |
| 1576 | + UNWIND [{_id:4, properties:{}}, {_id:6, properties:{}}] AS row |
| 1577 | + MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) ON CREATE SET n += row.properties SET n:Project; |
| 1578 | + UNWIND [{_id:3, properties:{name:"First"}}, {_id:5, properties:{name:"Second"}}] AS row |
| 1579 | + MERGE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) ON CREATE SET n += row.properties SET n:Person; |
| 1580 | + UNWIND [{start: {_id:3}, end: {_id:4}, properties:{id:1}}, {start: {_id:5}, end: {_id:6}, properties:{id:2}}] AS row |
| 1581 | + MATCH (start:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.start._id}) |
| 1582 | + MATCH (end:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.end._id}) |
| 1583 | + CREATE (start)-[r:WORKS_FOR]->(end) SET r += row.properties; |
| 1584 | + """; |
| 1585 | + |
| 1586 | + static final String EXPECTED_2886_RELS_WITHOUT_OPTIMIZATION = """ |
| 1587 | + MATCH (n1:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:3}), (n2:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:4}) CREATE (n1)-[r:WORKS_FOR {id:1}]->(n2); |
| 1588 | + MATCH (n1:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:5}), (n2:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`:6}) CREATE (n1)-[r:WORKS_FOR {id:2}]->(n2); |
| 1589 | + """; |
| 1590 | + |
1413 | 1591 | } |
1414 | 1592 |
|
1415 | 1593 | } |
0 commit comments