Skip to content

Commit

Permalink
[gRNAet7B] Fixes trigger procedures in clusters for neo4j 5.x (extend…
Browse files Browse the repository at this point in the history
…ed part)
  • Loading branch information
vga91 committed Nov 22, 2022
1 parent d37f5fd commit 8d12d51
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "apoc-core"]
path = apoc-core
url = https://github.com/neo4j/apoc
branch = dev
branch = dev-fix-trigger-procedures
106 changes: 106 additions & 0 deletions extended/src/test/java/apoc/trigger/TriggerClusterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
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;
import java.util.Map;
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 {

Expand Down Expand Up @@ -103,4 +108,105 @@ public void testTxIdAfterAsync() throws Exception {
org.neo4j.test.assertion.Assert.assertEventually(() -> TestContainerUtil.<Long>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.<Long>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);
}
}
Original file line number Diff line number Diff line change
@@ -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.<Long>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"));
});
}
}

0 comments on commit 8d12d51

Please sign in to comment.