Skip to content

Commit 490adf9

Browse files
authored
[NOID] Fixes #3945: The apoc.bolt.* procedures fail together with the apoc.path.subgraphAll one (#3949) (#3981)
* [NOID] Fixes #3945: The apoc.bolt.* procedures fail together with the apoc.path.subgraphAll one (#3949) * [NOID] changes 4.4 * [NOID] code formatting
1 parent d141160 commit 490adf9

File tree

2 files changed

+241
-7
lines changed

2 files changed

+241
-7
lines changed

full-it/src/test/java/apoc/full/it/BoltTest.java

Lines changed: 233 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@
2020

2121
import static apoc.util.TestContainerUtil.createEnterpriseDB;
2222
import static org.junit.Assert.assertEquals;
23+
import static org.junit.Assert.assertFalse;
2324
import static org.junit.Assert.assertNotNull;
25+
import static org.junit.Assert.assertNull;
2426
import static org.junit.Assert.assertTrue;
2527
import static org.neo4j.driver.Values.isoDuration;
2628
import static org.neo4j.driver.Values.point;
2729

2830
import apoc.bolt.Bolt;
2931
import apoc.cypher.Cypher;
3032
import apoc.export.cypher.ExportCypher;
33+
import apoc.path.PathExplorer;
34+
import apoc.refactor.GraphRefactoring;
3135
import apoc.util.Neo4jContainerExtension;
3236
import apoc.util.TestContainerUtil;
3337
import apoc.util.TestContainerUtil.ApocPackage;
@@ -39,14 +43,19 @@
3943
import java.util.Collections;
4044
import java.util.List;
4145
import java.util.Map;
46+
import java.util.stream.Collectors;
47+
import org.junit.After;
4248
import org.junit.AfterClass;
4349
import org.junit.Assume;
4450
import org.junit.BeforeClass;
4551
import org.junit.ClassRule;
4652
import org.junit.Test;
53+
import org.neo4j.driver.Session;
54+
import org.neo4j.graphdb.Entity;
4755
import org.neo4j.graphdb.Label;
4856
import org.neo4j.graphdb.Node;
4957
import org.neo4j.graphdb.Relationship;
58+
import org.neo4j.graphdb.RelationshipType;
5059
import org.neo4j.test.rule.DbmsRule;
5160
import org.neo4j.test.rule.ImpermanentDbmsRule;
5261

@@ -55,18 +64,23 @@
5564
* @since 29.08.17
5665
*/
5766
public class BoltTest {
67+
public static String BOLT_URL;
5868

5969
@ClassRule
6070
public static DbmsRule db = new ImpermanentDbmsRule();
6171

6272
private static Neo4jContainerExtension neo4jContainer;
73+
private static Session session;
6374

6475
@BeforeClass
6576
public static void setUp() {
6677
neo4jContainer = createEnterpriseDB(List.of(ApocPackage.FULL), !TestUtil.isRunningInCI())
6778
.withInitScript("init_neo4j_bolt.cypher");
6879
neo4jContainer.start();
69-
TestUtil.registerProcedure(db, Bolt.class, ExportCypher.class, Cypher.class);
80+
TestUtil.registerProcedure(
81+
db, Bolt.class, ExportCypher.class, Cypher.class, PathExplorer.class, GraphRefactoring.class);
82+
BOLT_URL = getBoltUrl().replaceAll("'", "");
83+
session = neo4jContainer.getSession();
7084
}
7185

7286
@AfterClass
@@ -75,6 +89,223 @@ public static void tearDown() {
7589
db.shutdown();
7690
}
7791

92+
@After
93+
public void after() {
94+
db.executeTransactionally("MATCH (n) DETACH DELETE n");
95+
session.writeTransaction(tx -> tx.run("MATCH (n:BoltStart), (m:Other) DETACH DELETE n, m"));
96+
}
97+
98+
@Test
99+
public void testBoltLoadWithSubgraphAllQuery() {
100+
session.writeTransaction(
101+
tx -> tx.run("CREATE (rootA:BoltStart {foobar: 'foobar'})-[:VIEWED]->(:Other {id: 1})"));
102+
103+
// procedure with config virtual: false
104+
String boltQuery = "MATCH (rootA:BoltStart {foobar: 'foobar'})\n" + "WITH rootA\n"
105+
+ "CALL apoc.path.subgraphAll(rootA, {relationshipFilter:'VIEWED>'})\n"
106+
+ "YIELD nodes, relationships\n"
107+
+ "RETURN nodes, relationships, rootA";
108+
109+
String boltLoadQueryVirtualFalse =
110+
"CALL apoc.bolt.load($boltUrl, $boltQuery, {}, {virtual: false})\n" + "YIELD row\n" + "RETURN row";
111+
112+
TestUtil.testCall(
113+
db,
114+
boltLoadQueryVirtualFalse,
115+
Map.of("boltUrl", BOLT_URL, "boltQuery", boltQuery, "virtual", false),
116+
this::virtualFalseEntitiesAssertions);
117+
118+
// procedure with config virtual: true
119+
String boltLoadQueryVirtualTrue =
120+
"CALL apoc.bolt.load($boltUrl, $boltQuery, {}, {virtual: true}) YIELD row\n" + "WITH row\n"
121+
+ "WITH row.nodes AS nodes, row.relationships AS relationships, row.rootA AS rootA\n"
122+
+ "CALL apoc.refactor.cloneSubgraph(nodes, relationships)\n"
123+
+ "YIELD input, output, error\n"
124+
+ "RETURN input, output, error;";
125+
126+
TestUtil.testResult(
127+
db,
128+
boltLoadQueryVirtualTrue,
129+
Map.of("boltUrl", BOLT_URL, "boltQuery", boltQuery, "virtual", true),
130+
r -> {
131+
graphRefactorAssertions(r.next());
132+
graphRefactorAssertions(r.next());
133+
assertFalse(r.hasNext());
134+
});
135+
136+
// check that `apoc.refactor.cloneSubgraph` after `apoc.bolt.load` creates entities correctly
137+
TestUtil.testCallCount(
138+
db, "MATCH (rootA:BoltStart {foobar: 'foobar'})-[:VIEWED]->(:Other {id: 1}) RETURN *", 1);
139+
}
140+
141+
@Test
142+
public void testBoltFromLocalWithSubgraphAllQuery() {
143+
String localStatement = "RETURN 'foobar' AS foobar";
144+
145+
String remoteStatement =
146+
"MERGE (rootA:BoltStart {foobar: foobar})-[:VIEWED]->(:Other {id: 1})\n" + "WITH rootA\n"
147+
+ "CALL apoc.path.subgraphAll(rootA, {relationshipFilter:'VIEWED>'})\n"
148+
+ "YIELD nodes, relationships\n"
149+
+ "RETURN nodes, relationships, rootA";
150+
151+
String query =
152+
"CALL apoc.bolt.load.fromLocal($boltUrl, $localStatement, $remoteStatement, {virtual: $virtual, readOnly: false}) YIELD row\n"
153+
+ "WITH row\n"
154+
+ "RETURN row";
155+
156+
// procedure with config virtual: true
157+
TestUtil.testCall(
158+
db,
159+
query,
160+
Map.of(
161+
"boltUrl",
162+
BOLT_URL,
163+
"localStatement",
164+
localStatement,
165+
"remoteStatement",
166+
remoteStatement,
167+
"virtual",
168+
true),
169+
this::virtualTrueEntitiesAssertions);
170+
171+
// procedure with config virtual: false
172+
TestUtil.testCall(
173+
db,
174+
query,
175+
Map.of(
176+
"boltUrl",
177+
BOLT_URL,
178+
"localStatement",
179+
localStatement,
180+
"remoteStatement",
181+
remoteStatement,
182+
"virtual",
183+
false),
184+
this::virtualFalseEntitiesAssertions);
185+
}
186+
187+
private void virtualTrueEntitiesAssertions(Map<String, Object> r) {
188+
Map<String, Object> row = (Map<String, Object>) r.get("row");
189+
List<Node> nodes = (List<Node>) row.get("nodes");
190+
assertEquals(2, nodes.size());
191+
List<Long> ids = nodes.stream().map(i -> i.getId()).collect(Collectors.toList());
192+
193+
List<Relationship> relationships = (List<Relationship>) row.get("relationships");
194+
assertEquals(1, relationships.size());
195+
196+
Relationship rel = relationships.get(0);
197+
assertTrue(ids.contains(rel.getStartNodeId()));
198+
assertTrue(ids.contains(rel.getEndNodeId()));
199+
assertEquals(RelationshipType.withName("VIEWED"), rel.getType());
200+
201+
Node rootA = (Node) row.get("rootA");
202+
assertEquals(List.of(Label.label("BoltStart")), rootA.getLabels());
203+
assertEquals(Map.of("foobar", "foobar"), rootA.getAllProperties());
204+
}
205+
206+
private void virtualFalseEntitiesAssertions(Map<String, Object> r) {
207+
Map<String, Object> row = (Map<String, Object>) r.get("row");
208+
List<Map> nodes = (List<Map>) row.get("nodes");
209+
assertEquals(2, nodes.size());
210+
List<Long> ids = nodes.stream().map(i -> (Long) i.get("id")).collect(Collectors.toList());
211+
212+
List<Map> relationships = (List<Map>) row.get("relationships");
213+
assertEquals(1, relationships.size());
214+
215+
Map rel = relationships.get(0);
216+
assertTrue(ids.contains((Long) rel.get("start")));
217+
assertTrue(ids.contains((Long) rel.get("end")));
218+
assertEquals("VIEWED", rel.get("type"));
219+
220+
Map rootA = (Map) row.get("rootA");
221+
assertEquals(List.of("BoltStart"), rootA.get("labels"));
222+
assertEquals(Map.of("foobar", "foobar"), rootA.get("properties"));
223+
}
224+
225+
private void graphRefactorAssertions(Map<String, Object> r) {
226+
assertNull(r.get("error"));
227+
assertTrue(r.get("input") instanceof Long);
228+
}
229+
230+
@Test
231+
public void testBoltLoadReturningMapAndList() {
232+
session.writeTransaction(
233+
tx -> tx.run("CREATE (rootA:BoltStart {foobar: 'foobar'})-[:VIEWED {id: 2}]->(:Other {id: 1})"));
234+
235+
// procedure with config virtual: false
236+
String boltQuery = "MATCH (start:BoltStart {foobar: 'foobar'})-[rel:VIEWED]->(end:Other)\n"
237+
+ "WITH start, rel, end, [start, end, rel] as list\n"
238+
+ "RETURN start, rel, end, {keyOne: start, keyTwo: {innerKey: list}} as map, list";
239+
240+
String boltLoadQuery =
241+
"CALL apoc.bolt.load($boltUrl, $boltQuery, {}, {virtual: $virtual})\n" + "YIELD row\n" + "RETURN row";
242+
243+
TestUtil.testCall(
244+
db,
245+
boltLoadQuery,
246+
Map.of("boltUrl", BOLT_URL, "boltQuery", boltQuery, "virtual", true),
247+
this::virtualTrueWithMapAndListAssertions);
248+
249+
TestUtil.testCall(
250+
db,
251+
boltLoadQuery,
252+
Map.of("boltUrl", BOLT_URL, "boltQuery", boltQuery, "virtual", false),
253+
this::virtualFalseWithMapAndListAssertions);
254+
}
255+
256+
private void virtualFalseWithMapAndListAssertions(Map<String, Object> r) {
257+
Map<String, Object> row = (Map<String, Object>) r.get("row");
258+
259+
Map start = (Map) row.get("start");
260+
assertEquals("NODE", start.get("entityType"));
261+
Map end = (Map) row.get("end");
262+
assertEquals("NODE", end.get("entityType"));
263+
Map rel = (Map) row.get("rel");
264+
assertEquals("RELATIONSHIP", rel.get("entityType"));
265+
266+
List<Map> list = (List<Map>) row.get("list");
267+
assertEquals(3, list.size());
268+
269+
assertEquals(start, list.get(0));
270+
assertEquals(end, list.get(1));
271+
assertEquals(rel, list.get(2));
272+
273+
Map map = (Map) row.get("map");
274+
assertEquals(start, map.get("keyOne"));
275+
276+
Map mapKeyTwo = (Map) map.get("keyTwo");
277+
assertEquals(list, mapKeyTwo.get("innerKey"));
278+
}
279+
280+
private void virtualTrueWithMapAndListAssertions(Map<String, Object> r) {
281+
Map<String, Object> row = (Map<String, Object>) r.get("row");
282+
283+
Node start = (Node) row.get("start");
284+
assertEquals(List.of(Label.label("BoltStart")), start.getLabels());
285+
assertEquals(Map.of("foobar", "foobar"), start.getAllProperties());
286+
287+
Node end = (Node) row.get("end");
288+
assertEquals(List.of(Label.label("Other")), end.getLabels());
289+
assertEquals(Map.of("id", 1L), end.getAllProperties());
290+
291+
Relationship rel = (Relationship) row.get("rel");
292+
assertEquals(RelationshipType.withName("VIEWED"), rel.getType());
293+
assertEquals(Map.of("id", 2L), rel.getAllProperties());
294+
295+
List<Entity> list = (List<Entity>) row.get("list");
296+
assertEquals(3, list.size());
297+
298+
assertEquals(start, list.get(0));
299+
assertEquals(end, list.get(1));
300+
assertEquals(rel, list.get(2));
301+
302+
Map map = (Map) row.get("map");
303+
assertEquals(start, map.get("keyOne"));
304+
305+
Map mapKeyTwo = (Map) map.get("keyTwo");
306+
assertEquals(list, mapKeyTwo.get("innerKey"));
307+
}
308+
78309
@Test
79310
public void testNeo4jBolt() {
80311
final String uriDbBefore4 = System.getenv("URI_DB_BEFORE_4");
@@ -531,7 +762,7 @@ public void testLoadFromLocalStream() {
531762
assertEquals(1L, remoteCount);
532763
}
533764

534-
private String getBoltUrl() {
765+
private static String getBoltUrl() {
535766
return String.format(
536767
"'bolt://neo4j:%s@%s:%s'",
537768
TestContainerUtil.password, neo4jContainer.getContainerIpAddress(), neo4jContainer.getMappedPort(7687));

full/src/main/java/apoc/bolt/BoltConnection.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,11 @@ private Stream<RowResult> buildRowResult(
148148
}
149149

150150
private RowResult buildRowResult(Transaction tx, Record record, Map<Long, org.neo4j.graphdb.Node> nodesCache) {
151-
return new RowResult(record.asMap(value -> convert(tx, value, nodesCache)));
151+
return new RowResult(record.asMap(value -> convertRecursive(tx, value, nodesCache)));
152152
}
153153

154-
private Object convert(Transaction tx, Object entity, Map<Long, org.neo4j.graphdb.Node> nodesCache) {
155-
if (entity instanceof Value) return convert(tx, ((Value) entity).asObject(), nodesCache);
154+
private Object convertRecursive(Transaction tx, Object entity, Map<Long, org.neo4j.graphdb.Node> nodesCache) {
155+
if (entity instanceof Value) return convertRecursive(tx, ((Value) entity).asObject(), nodesCache);
156156
if (entity instanceof Node) return toNode(entity, nodesCache);
157157
if (entity instanceof Relationship) return toRelationship(tx, entity, nodesCache);
158158
if (entity instanceof Path) return toPath(tx, entity, nodesCache);
@@ -163,12 +163,15 @@ private Object convert(Transaction tx, Object entity, Map<Long, org.neo4j.graphd
163163

164164
private Object toMap(Transaction tx, Map<String, Object> entity, Map<Long, org.neo4j.graphdb.Node> nodeCache) {
165165
return entity.entrySet().stream()
166-
.map(entry -> new AbstractMap.SimpleEntry(entry.getKey(), convert(tx, entry.getValue(), nodeCache)))
166+
.map(entry ->
167+
new AbstractMap.SimpleEntry(entry.getKey(), convertRecursive(tx, entry.getValue(), nodeCache)))
167168
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
168169
}
169170

170171
private Object toCollection(Transaction tx, Collection entity, Map<Long, org.neo4j.graphdb.Node> nodeCache) {
171-
return entity.stream().map(elem -> convert(tx, elem, nodeCache)).collect(Collectors.toList());
172+
return entity.stream()
173+
.map(elem -> convertRecursive(tx, elem, nodeCache))
174+
.collect(Collectors.toList());
172175
}
173176

174177
private Stream<RowResult> getRowResultStream(Session session, Map<String, Object> params, String statement) {

0 commit comments

Comments
 (0)