Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions common/src/main/java/apoc/util/EntityUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package apoc.util;

import org.neo4j.graphalgo.impl.util.PathImpl;
import org.neo4j.graphdb.Entity;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Transaction;
import org.neo4j.internal.helpers.collection.Iterables;

import java.util.Map;
import java.util.stream.Collectors;

public class EntityUtil {

public static <T> T anyRebind(Transaction tx, T any) {
if (any instanceof Map) {
return (T) ((Map<String, Object>) any).entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> anyRebind(tx, e.getValue())));
}
if (any instanceof Path) {
final Path path = (Path) any;
PathImpl.Builder builder = new PathImpl.Builder(Util.rebind(tx, path.startNode()));
for (Relationship rel: path.relationships()) {
builder = builder.push(Util.rebind(tx, rel));
}
return (T) builder.build();
}
if (any instanceof Iterable) {
return (T) Iterables.stream((Iterable) any)
.map(i -> anyRebind(tx, i)).collect(Collectors.toList());
}
if (any instanceof Entity) {
return (T) Util.rebind(tx, (Entity) any);
}
return any;
}
}
4 changes: 3 additions & 1 deletion core/src/main/java/apoc/cypher/Cypher.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import apoc.Pools;
import apoc.result.MapResult;
import apoc.util.EntityUtil;
import apoc.util.QueueBasedSpliterator;
import apoc.util.Util;
import org.neo4j.graphdb.GraphDatabaseService;
Expand Down Expand Up @@ -175,7 +176,8 @@ private Object consumeResult(Result result, BlockingQueue<RowResult> queue, bool
int row = 0;
while (result.hasNext()) {
terminationGuard.check();
queue.put(new RowResult(row++, result.next()));
Map<String, Object> mapResult = EntityUtil.anyRebind(tx, result.next());
queue.put(new RowResult(row++, mapResult));
}
if (addStatistics) {
queue.put(new RowResult(-1, toMap(result.getQueryStatistics(), System.currentTimeMillis() - time, row)));
Expand Down
173 changes: 173 additions & 0 deletions it/src/test/java/apoc/it/core/CypherEnterpriseTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package apoc.it.core;

import apoc.util.Neo4jContainerExtension;
import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.neo4j.driver.Session;
import org.neo4j.driver.types.Node;

import java.util.List;
import java.util.Map;

import static apoc.cypher.CypherTestUtil.CREATE_RESULT_NODES;
import static apoc.cypher.CypherTestUtil.CREATE_RETURNQUERY_NODES;
import static apoc.cypher.CypherTestUtil.SET_AND_RETURN_QUERIES;
import static apoc.cypher.CypherTestUtil.SET_NODE;
import static apoc.cypher.CypherTestUtil.SIMPLE_RETURN_QUERIES;
import static apoc.cypher.CypherTestUtil.assertResultNode;
import static apoc.cypher.CypherTestUtil.assertReturnQueryNode;
import static apoc.cypher.CypherTestUtil.testRunProcedureWithSetAndReturnResults;
import static apoc.cypher.CypherTestUtil.testRunProcedureWithSimpleReturnResults;
import static apoc.util.TestContainerUtil.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;

public class CypherEnterpriseTest {
private static Neo4jContainerExtension neo4jContainer;
private static Session session;

@BeforeClass
public static void beforeAll() {
neo4jContainer = createEnterpriseDB(List.of(ApocPackage.CORE), true);
neo4jContainer.start();
session = neo4jContainer.getSession();
}

@AfterClass
public static void afterAll() {
session.close();
neo4jContainer.close();
}

@After
public void after() {
session.writeTransaction(tx -> tx.run("MATCH (n) DETACH DELETE n"));
}

@Test
public void testRunManyWithSetAndResults() {
String query = "CALL apoc.cypher.runMany($statement, {})";
Map<String, Object> params = Map.of("statement", SET_AND_RETURN_QUERIES);

testRunProcedureWithSetAndReturnResults(session, query, params);
}

@Test
public void testRunManyWithResults() {
String query = "CALL apoc.cypher.runMany($statement, {})";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have tests for the other apoc.cypher.* procedures, I assume this also fails for apoc.cypher.runManyReadOnly perhaps? and maybe others e.g apoc.cypher.doIt (Not sure, I haven't tested them out)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.
I added test for other procedure, i.e. apoc.cypher.runManyReadOnly, apoc.cypher. doIt, apoc.cypher. runWrite and apoc.cypher. run.

Map<String, Object> params = Map.of("statement", SIMPLE_RETURN_QUERIES);

testRunProcedureWithSimpleReturnResults(session, query, params);
}

@Test
public void testRunManyReadOnlyWithSetAndResults() {
String query = "CALL apoc.cypher.runManyReadOnly($statement, {})";
Map<String, Object> params = Map.of("statement", SET_AND_RETURN_QUERIES);

session.writeTransaction(tx -> tx.run(CREATE_RESULT_NODES));

// even if this procedure is read-only and execute a write operation, it doesn't fail but just skip the statements
testCallEmpty(session, query, params);
}

@Test
public void testRunManyReadOnlyWithResults() {
String query = "CALL apoc.cypher.runManyReadOnly($statement, {})";
Map<String, Object> params = Map.of("statement", SIMPLE_RETURN_QUERIES);

testRunProcedureWithSimpleReturnResults(session, query, params);
}

@Test
public void testRunWriteWithSetAndResults() {
String query = "CALL apoc.cypher.runWrite($statement, {})";
Map<String, Object> params = Map.of("statement", SET_NODE);

testRunSingleStatementProcedureWithSetAndResults(query, params);
}

@Test
public void testRunWriteWithResults() {
String query = "CALL apoc.cypher.runWrite($statement, {})";
Map<String, Object> params = Map.of("statement", SIMPLE_RETURN_QUERIES);

testRunSingleStatementProcedureWithResults(query, params);
}

@Test
public void testDoItWithSetAndResults() {
String query = "CALL apoc.cypher.doIt($statement, {})";
Map<String, Object> params = Map.of("statement", SET_NODE);

testRunSingleStatementProcedureWithSetAndResults(query, params);
}

@Test
public void testDoItWithResults() {
String query = "CALL apoc.cypher.doIt($statement, {})";
Map<String, Object> params = Map.of("statement", SIMPLE_RETURN_QUERIES);

testRunSingleStatementProcedureWithResults(query, params);
}

@Test
public void testRunWithSetAndResults() {
String query = "CALL apoc.cypher.run($statement, {})";
Map<String, Object> params = Map.of("statement", SET_NODE);

session.writeTransaction(tx -> tx.run(CREATE_RESULT_NODES));

RuntimeException e = assertThrows(RuntimeException.class,
() -> testCall(session, query, params, (res) -> {})
);
String expectedMessage = "Set property for property 'updated' on database 'neo4j' is not allowed for user 'neo4j' with roles [PUBLIC, admin] overridden by READ.";
Assertions.assertThat(e.getMessage()).contains(expectedMessage);
}

@Test
public void testRunWithResults() {
String query = "CALL apoc.cypher.run($statement, {})";
Map<String, Object> params = Map.of("statement", SIMPLE_RETURN_QUERIES);

testRunSingleStatementProcedureWithResults(query, params);
}

private static void testRunSingleStatementProcedureWithResults(String query, Map<String, Object> params) {
session.writeTransaction(tx -> tx.run(CREATE_RETURNQUERY_NODES));

testResult(session, query, params, r -> {
Map<String, Object> next = r.next();
assertReturnQueryNode(0L, (Map<String, Node>) next.get("value"));
next = r.next();
assertReturnQueryNode(1L, (Map<String, Node>) next.get("value"));
next = r.next();
assertReturnQueryNode(2L, (Map<String, Node>) next.get("value"));
next = r.next();
assertReturnQueryNode(3L, (Map<String, Node>) next.get("value"));

assertFalse(r.hasNext());
});
}

private static void testRunSingleStatementProcedureWithSetAndResults(String query, Map<String, Object> params) {
session.writeTransaction(tx -> tx.run(CREATE_RESULT_NODES));

testResult(session, query, params, r -> {
Map<String, Object> next = r.next();
assertResultNode(0L, (Map<String, Node>) next.get("value"));
next = r.next();
assertResultNode(1L, (Map<String, Node>) next.get("value"));
next = r.next();
assertResultNode(2L, (Map<String, Node>) next.get("value"));
next = r.next();
assertResultNode(3L, (Map<String, Node>) next.get("value"));

assertFalse(r.hasNext());
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@
*/
public class ExportCypherEnterpriseFeaturesTest {

private static File directory = new File("import"); // it's the directory bounded to the /import dir inside the Neo4jContainer

private static Neo4jContainerExtension neo4jContainer;
private static Session session;

Expand Down Expand Up @@ -152,7 +150,7 @@ public void shouldHandleTwoLabelsWithOneCompoundConstraintEach() {

private void assertExportStatement(String expectedStatement, String fileName) {
// The constraints are exported in arbitrary order, so we cannot assert on the entire file
String actual = readFileToString(new File(directory, fileName));
String actual = readFileToString(new File(importFolder, fileName));
MatcherAssert.assertThat(actual, Matchers.containsString(expectedStatement));
EXPECTED_CONSTRAINTS.forEach(
constraint -> assertTrue(String.format("Constraint '%s' not in result", constraint), actual.contains(constraint))
Expand Down
Loading