diff --git a/.gitmodules b/.gitmodules index ea53a5a847..332dec2c03 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "apoc-core"] path = apoc-core url = https://github.com/neo4j/apoc - branch = dev + branch = dev-fix-trigger-procedures diff --git a/apoc-core b/apoc-core index 2438fe0837..d4608d419f 160000 --- a/apoc-core +++ b/apoc-core @@ -1 +1 @@ -Subproject commit 2438fe083734b0ba58ae631785c5ad9539494b51 +Subproject commit d4608d419f0bb12991e2f2eda9f3cec3c43cdaa5 diff --git a/extended/src/test/java/apoc/trigger/TriggerClusterTest.java b/extended/src/test/java/apoc/trigger/TriggerClusterTest.java index ae5f1a3596..c37bee7db0 100644 --- a/extended/src/test/java/apoc/trigger/TriggerClusterTest.java +++ b/extended/src/test/java/apoc/trigger/TriggerClusterTest.java @@ -9,6 +9,7 @@ import org.junit.Ignore; import org.junit.Test; import org.neo4j.driver.types.Node; +import org.neo4j.driver.Session; import java.util.Collections; import java.util.List; @@ -16,6 +17,10 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.neo4j.configuration.GraphDatabaseSettings.SYSTEM_DATABASE_NAME; +import static org.neo4j.driver.SessionConfig.forDatabase; +import static org.neo4j.test.assertion.Assert.assertEventually; public class TriggerClusterTest { @@ -103,4 +108,105 @@ public void testTxIdAfterAsync() throws Exception { org.neo4j.test.assertion.Assert.assertEventually(() -> TestContainerUtil.singleResultFirstColumn(cluster.getSession(), "MATCH p = ()-[r:GENERATED]->() RETURN count(p) AS count"), (value) -> value == 2L, 30, TimeUnit.SECONDS); } + + // + // test cases duplicated, regarding new procedures + // + + @Test + public void testTimeStampTriggerForUpdatedPropertiesNewProcedures() throws Exception { + final String name = "timestampUpdate"; + try (final Session session = cluster.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) { + session.run("CALL apoc.trigger.install('neo4j', $name,'UNWIND apoc.trigger.nodesByLabel($assignedNodeProperties,null) AS n SET n.ts = timestamp()',{})", + Map.of("name", name)); + } + try (final Session session = cluster.getSession()) { + awaitProcedureInstalled(session, "timestampUpdate"); + session.run("CREATE (f:Foo) SET f.foo='bar'"); + TestContainerUtil.testCall(session, "MATCH (f:Foo) RETURN f", (row) -> { + assertTrue(((Node) row.get("f")).containsKey("ts")); + }); + } + } + + + @Test + public void testReplicationNewProcedures() throws Exception { + try (final Session session = cluster.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) { + session.run("CALL apoc.trigger.install('neo4j', 'timestamp','UNWIND apoc.trigger.nodesByLabel($assignedNodeProperties,null) AS n SET n.ts = timestamp()',{})"); + } + // Test that the trigger is present in another instance + awaitProcedureInstalled(cluster.getDriver().session(), "timestamp"); + } + + @Test + public void testLowerCaseNameNewProcedures() { + final String name = "lowercase"; + try (final Session session = cluster.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) { + session.run("CALL apoc.trigger.install('neo4j', $name, 'UNWIND apoc.trigger.nodesByLabel($assignedLabels,\"Person\") AS n SET n.id = toLower(n.name)',{})", + Map.of("name", name)); + } + try (final Session session = cluster.getSession()) { + session.run("create constraint on (p:Person) assert p.id is unique"); + awaitProcedureInstalled(session, name); + + session.run("CREATE (f:Person {name:'John Doe'})"); + TestContainerUtil.testCall(session, "MATCH (f:Person) RETURN f", (row) -> { + assertEquals("john doe", ((Node) row.get("f")).get("id").asString()); + assertEquals("John Doe", ((Node) row.get("f")).get("name").asString()); + }); + } + } + + @Test + public void testSetLabelsNewProcs() throws Exception { + final String name = "testSetLabels"; + try (final Session session = cluster.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) { + session.run("CALL apoc.trigger.install('neo4j', $name,'UNWIND apoc.trigger.nodesByLabel($assignedLabels,\"Person\") AS n SET n:Man',{})", + Map.of("name", name)); + } + try (final Session session = cluster.getSession()) { + session.run("CREATE (f:Test {name:'John Doe'})"); + + awaitProcedureInstalled(session, name); + + session.run("MATCH (f:Test) SET f:Person"); + TestContainerUtil.testCall(session, "MATCH (f:Man) RETURN f", (row) -> { + assertEquals("John Doe", ((Node) row.get("f")).get("name").asString()); + assertTrue(((Node) row.get("f")).hasLabel("Person")); + }); + + long count = TestContainerUtil.singleResultFirstColumn(session, "MATCH (f:Man) RETURN count(*) as c"); + assertEquals(1L, count); + } + } + + @Test + public void testTxIdAfterAsyncNewProcedures() throws Exception { + final String name = "testTxIdAfterAsync"; + try (final Session session = cluster.getDriver().session(forDatabase(SYSTEM_DATABASE_NAME))) { + session.run("CALL apoc.trigger.install('neo4j', $name, 'UNWIND apoc.trigger.propertiesByKey($assignedNodeProperties, \"_executed\") as prop " + + " WITH prop.node as n " + + " CREATE (z:SON {father:id(n)}) " + + " CREATE (n)-[:GENERATED]->(z)', " + + "{phase:'afterAsync'})", + Map.of("name", name)); + } + try (final Session session = cluster.getSession()) { + awaitProcedureInstalled(session, name); + + session.run("CREATE (:TEST {name:'x', _executed:0})"); + session.run("CREATE (:TEST {name:'y', _executed:0})"); + assertEventually(() -> TestContainerUtil.singleResultFirstColumn(session, "MATCH p = ()-[r:GENERATED]->() RETURN count(p) AS count"), + (value) -> value == 2L, 30, TimeUnit.SECONDS); + } + } + + private static void awaitProcedureInstalled(Session session, String name) { + assertEventually(() -> session + .readTransaction(tx -> tx.run("CALL apoc.trigger.list") + .single().get("name").asString()), + name::equals, + 30, TimeUnit.SECONDS); + } } diff --git a/extended/src/test/java/apoc/trigger/TriggerNewProceduresExtendedTest.java b/extended/src/test/java/apoc/trigger/TriggerNewProceduresExtendedTest.java new file mode 100644 index 0000000000..f219ef174f --- /dev/null +++ b/extended/src/test/java/apoc/trigger/TriggerNewProceduresExtendedTest.java @@ -0,0 +1,232 @@ +package apoc.trigger; + +import apoc.create.Create; +import apoc.nodes.Nodes; +import apoc.util.TestUtil; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.neo4j.configuration.GraphDatabaseSettings; +import org.neo4j.dbms.api.DatabaseManagementService; +import org.neo4j.graphdb.Entity; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Label; +import org.neo4j.graphdb.Node; +import org.neo4j.test.TestDatabaseManagementServiceBuilder; + +import java.io.File; +import java.io.FileWriter; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static apoc.ApocConfig.SUN_JAVA_COMMAND; +import static apoc.util.TestUtil.testCallCountEventually; +import static apoc.util.TestUtil.testCallEventually; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.neo4j.configuration.GraphDatabaseSettings.procedure_unrestricted; + +public class TriggerNewProceduresExtendedTest { + private static final long TIMEOUT = 30L; + + private static final File directory = new File("target/conf"); + static { //noinspection ResultOfMethodCallIgnored + directory.mkdirs(); + } + + @ClassRule + public static TemporaryFolder storeDir = new TemporaryFolder(); + + private static GraphDatabaseService sysDb; + private static GraphDatabaseService db; + private static DatabaseManagementService databaseManagementService; + + @BeforeClass + public static void beforeClass() throws Exception { + // we cannot set via ApocConfig.apocConfig().setProperty("apoc.trigger.refresh", "100") in `setUp`, because is too late + final File conf = new File(directory, "apoc.conf"); + try (FileWriter writer = new FileWriter(conf)) { + writer.write(String.join("\n", + "apoc.trigger.refresh=100", + "apoc.trigger.enabled=true")); + } + System.setProperty(SUN_JAVA_COMMAND, "config-dir=" + directory.getAbsolutePath()); + + databaseManagementService = new TestDatabaseManagementServiceBuilder(storeDir.getRoot().toPath()) + .setConfig(procedure_unrestricted, List.of("apoc*")) + .build(); + db = databaseManagementService.database(GraphDatabaseSettings.DEFAULT_DATABASE_NAME); + sysDb = databaseManagementService.database(GraphDatabaseSettings.SYSTEM_DATABASE_NAME); + TestUtil.registerProcedure(sysDb, TriggerNewProcedures.class, Trigger.class, TriggerExtended.class, + Nodes.class, Create.class); + TestUtil.registerProcedure(db, Trigger.class); + } + + @AfterClass + public static void afterClass() throws Exception { + databaseManagementService.shutdown(); + } + + @After + public void after() throws Exception { + sysDb.executeTransactionally("CALL apoc.trigger.dropAll('neo4j')"); + testCallCountEventually(db, "CALL apoc.trigger.list", 0, TIMEOUT); + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + + private void awaitProcedureUpdated(String name, String query) { + testCallEventually(db, "CALL apoc.trigger.list() YIELD name, query WHERE name = $name RETURN query", + Map.of("name", name), + row -> assertEquals(query, row.get("query")), + TIMEOUT); + } + + // + // test cases taken and adapted from TriggerExtendedTest.java + // + + @Test + public void testTimeStampTriggerForUpdatedProperties() { + final String name = "timestamp"; + final String query = "UNWIND apoc.trigger.nodesByLabel($assignedNodeProperties,null) AS n SET n.ts = timestamp()"; + sysDb.executeTransactionally("CALL apoc.trigger.install('neo4j', $name, $query, {})", + Map.of("name", name, "query", query)); + awaitProcedureUpdated(name, query); + db.executeTransactionally("CREATE (f:Foo) SET f.foo='bar'"); + TestUtil.testCall(db, "MATCH (f:Foo) RETURN f", + (row) -> assertTrue(((Node) row.get("f")).hasProperty("ts"))); + } + + @Test + public void testLowerCaseName() { + db.executeTransactionally("create constraint on (p:Person) assert p.id is unique"); + final String name = "lowercase"; + final String query = "UNWIND apoc.trigger.nodesByLabel($assignedLabels,\"Person\") AS n SET n.id = toLower(n.name)"; + sysDb.executeTransactionally("CALL apoc.trigger.install('neo4j', $name, $query, {})", + Map.of("name", name, "query", query)); + awaitProcedureUpdated(name, query); + db.executeTransactionally("CREATE (f:Person {name:'John Doe'})"); + TestUtil.testCall(db, "MATCH (f:Person) RETURN f", (row) -> { + assertEquals("john doe", ((Node)row.get("f")).getProperty("id")); + assertEquals("John Doe", ((Node)row.get("f")).getProperty("name")); + }); + } + + @Test + public void testSetLabels() { + db.executeTransactionally("CREATE (f {name:'John Doe'})"); + final String name = "setlabels"; + final String query = "UNWIND apoc.trigger.nodesByLabel($assignedLabels,\"Person\") AS n SET n:Man"; + sysDb.executeTransactionally("CALL apoc.trigger.install('neo4j', $name, $query, {})", + Map.of("name", name, "query", query)); + awaitProcedureUpdated(name, query); + db.executeTransactionally("MATCH (f) SET f:Person"); + TestUtil.testCall(db, "MATCH (f:Man) RETURN f", (row) -> { + assertEquals("John Doe", ((Node)row.get("f")).getProperty("name")); + assertTrue(((Node) row.get("f")).hasLabel(Label.label("Person"))); + }); + + long count = TestUtil.singleResultFirstColumn(db, "MATCH (f:Man) RETURN count(*) as c"); + assertEquals(1L, count); + } + + @Test + public void testTxIdAfterAsync() { + final String name = "triggerTest"; + final String query = "UNWIND apoc.trigger.propertiesByKey($assignedNodeProperties, \"_executed\") as prop " + + " WITH prop.node as n " + + " CREATE (z:SON {father:id(n)}) " + + " CREATE (n)-[:GENERATED]->(z)"; + sysDb.executeTransactionally("CALL apoc.trigger.install('neo4j', $name, $query, {phase:'afterAsync'})", + Map.of("name", name, "query", query)); + awaitProcedureUpdated(name, query); + db.executeTransactionally("CREATE (:TEST {name:'x', _executed:0})"); + db.executeTransactionally("CREATE (:TEST {name:'y', _executed:0})"); + org.neo4j.test.assertion.Assert.assertEventually(() -> db.executeTransactionally("MATCH p = ()-[r:GENERATED]->() RETURN count(p) AS count", + Collections.emptyMap(), (r) -> r.columnAs("count").next()), + (value) -> value == 2L, 30, TimeUnit.SECONDS); + } + + @Test + public void testIssue1152Before() { + testIssue1152Common("before"); + } + + @Test + public void testIssue1152After() { + testIssue1152Common("after"); + } + + private void testIssue1152Common(String phase) { + db.executeTransactionally("CREATE (n:To:Delete {prop1: 'val1', prop2: 'val2'}) RETURN id(n) as id"); + + // we check also that we can execute write operation (through virtualNode functions, e.g. apoc.create.addLabels) + final String name = "issue1152"; + final String query = "UNWIND $deletedNodes as deletedNode " + + "WITH apoc.trigger.toNode(deletedNode, $removedLabels, $removedNodeProperties) AS deletedNode " + + "CREATE (r:Report {id: id(deletedNode)}) WITH r, deletedNode " + + "CALL apoc.create.addLabels(r, apoc.node.labels(deletedNode)) yield node with node, deletedNode " + + "set node+=apoc.any.properties(deletedNode)"; + + sysDb.executeTransactionally("CALL apoc.trigger.install('neo4j', $name, $query,{phase: $phase})", + Map.of("name", name, "query", query, "phase", phase)); + awaitProcedureUpdated(name, query); + + db.executeTransactionally("MATCH (f:To:Delete) DELETE f"); + + TestUtil.testCall(db, "MATCH (n:Report:To:Delete) RETURN n", (row) -> { + final Node n = (Node) row.get("n"); + assertEquals("val1", n.getProperty("prop1")); + assertEquals("val2", n.getProperty("prop2")); + }); + } + + @Test + public void testRetrievePropsDeletedRelationship() { + db.executeTransactionally("CREATE (s:Start)-[r:MY_TYPE {prop1: 'val1', prop2: 'val2'}]->(e:End), (s)-[:REMAINING_REL]->(e)"); + + final String query = "UNWIND $deletedRelationships as deletedRel " + + "WITH apoc.trigger.toRelationship(deletedRel, $removedRelationshipProperties) AS deletedRel " + + "MATCH (s)-[r:REMAINING_REL]->(e) WITH r, deletedRel " + + "set r+=apoc.any.properties(deletedRel), r.type= type(deletedRel)"; + + final String assertionQuery = "MATCH (:Start)-[n:REMAINING_REL]->(:End) RETURN n"; + testRetrievePropsDeletedRelationshipCommon("before", query, assertionQuery); + testRetrievePropsDeletedRelationshipCommon("after", query, assertionQuery); + } + + @Test + public void testRetrievePropsDeletedRelationshipWithQueryCreation() { + db.executeTransactionally("CREATE (:Start)-[r:MY_TYPE {prop1: 'val1', prop2: 'val2'}]->(:End)"); + + final String query = "UNWIND $deletedRelationships as deletedRel " + + "WITH apoc.trigger.toRelationship(deletedRel, $removedRelationshipProperties) AS deletedRel " + + "CREATE (r:Report {type: type(deletedRel)}) WITH r, deletedRel " + + "set r+=apoc.any.properties(deletedRel)"; + + final String assertionQuery = "MATCH (n:Report) RETURN n"; + testRetrievePropsDeletedRelationshipCommon("before", query, assertionQuery); + testRetrievePropsDeletedRelationshipCommon("after", query, assertionQuery); + } + + private void testRetrievePropsDeletedRelationshipCommon(String phase, String triggerQuery, String assertionQuery) { + final String name = UUID.randomUUID().toString(); + sysDb.executeTransactionally("CALL apoc.trigger.install('neo4j', $name, $query,{phase: $phase})", + Map.of("name", name, "query", triggerQuery, "phase", phase)); + awaitProcedureUpdated(name, triggerQuery); + db.executeTransactionally("MATCH (:Start)-[r:MY_TYPE]->(:End) DELETE r"); + + TestUtil.testCall(db, assertionQuery, (row) -> { + final Entity n = (Entity) row.get("n"); + assertEquals("MY_TYPE", n.getProperty("type")); + assertEquals("val1", n.getProperty("prop1")); + assertEquals("val2", n.getProperty("prop2")); + }); + } +} \ No newline at end of file