Skip to content

Commit

Permalink
Fixes #4157: apoc.dv.queryAndLink how to specify direction
Browse files Browse the repository at this point in the history
  • Loading branch information
vga91 committed Nov 6, 2024
1 parent 7c6254a commit 7f1bc7f
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ apoc.dv.queryAndLink(node :: NODE?, relName :: STRING?, name :: STRING?, params
|path|PATH?
|===

== Configuration parameters

The procedures support the following config parameters:

.Config parameters
[opts=header]
|===
| name | type | default | description
| direction | String | "OUT" | The direction of the relationships, i.e. outgoing ("OUT") or incoming ("IN").
|===
10 changes: 10 additions & 0 deletions docs/asciidoc/modules/ROOT/pages/virtual-resource/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ phrase over the previous query.

image::apoc.dv.jdbc-queryAndLink.png[scaledwidth="100%"]

The default direction of the relationships is outgoing (i.e. `{direction: "OUT"}`), but it is possible to reverse it by the config parameters.
Example:

[source,cypher]
----
MATCH (hook:Hook) WITH hook
CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, { direction: "IN" }) yield path
RETURN path
----

=== Listing the Virtualized Resource Catalog
The apoc.dv.catalog.list procedure returns a list with all the existing Virtualized resources and their descriptions. It takes no parameters.

Expand Down
242 changes: 231 additions & 11 deletions extended-it/src/test/java/apoc/dv/DataVirtualizationCatalogTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import apoc.load.LoadCsv;
import apoc.util.TestUtil;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jetbrains.annotations.NotNull;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
Expand Down Expand Up @@ -74,6 +75,10 @@ public void testVirtualizeCSV() {
"desc", desc,
"labels", labels);

String personName = "Rana";
String personAge = "11";
String hookNodeName = "node to test linking";

final Consumer<Map<String, Object>> assertCatalogContent = (row) -> {
assertEquals(name, row.get("name"));
assertEquals(url, row.get("url"));
Expand All @@ -91,8 +96,6 @@ public void testVirtualizeCSV() {
testCall(db, "CALL apoc.dv.catalog.list()",
assertCatalogContent);

String personName = "Rana";
String personAge = "11";

Map<String, Object> queryParams = Map.of("name", personName, "age", personAge);
testCall(db, "CALL apoc.dv.query($name, $queryParams, $config)",
Expand All @@ -104,14 +107,13 @@ public void testVirtualizeCSV() {
assertEquals(List.of(Label.label("Person")), node.getLabels());
});

String hookNodeName = "node to test linking";

db.executeTransactionally("create (:Hook {name: $hookNodeName})", Map.of("hookNodeName", hookNodeName));

final String relType = "LINKED_TO";
testCall(db, "MATCH (hook:Hook) WITH hook " +
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
Map.of("name", name, "queryParams", queryParams, "relType", relType, "config", Map.of("header", true)),
(row) -> {
Path path = (Path) row.get("path");
Expand All @@ -132,6 +134,75 @@ public void testVirtualizeCSV() {

}

@Test
public void testVirtualizeCSVWithCustomDirectionIN() {
final String name = "csv_vr";
final String url = getUrlFileName("test.csv").toString();
final String desc = "person's details";
final String query = "map.name = $name and map.age = $age";
List<String> labels = List.of("Person");
Map<String, Object> map = Map.of("type", "CSV",
"url", url, "query", query,
"desc", desc,
"labels", labels);

String personName = "Rana";
String personAge = "11";
String hookNodeName = "node to test linking";

final Consumer<Map<String, Object>> assertCatalogContent = (row) -> {
assertEquals(name, row.get("name"));
assertEquals(url, row.get("url"));
assertEquals("CSV", row.get("type"));
assertEquals(List.of("Person"), row.get("labels"));
assertEquals(desc, row.get("desc"));
assertEquals(query, row.get("query"));
assertEquals(List.of("$name", "$age"), row.get("params"));
};

testCall(db, "CALL apoc.dv.catalog.add($name, $map)",
Map.of("name", name, "map", map),
assertCatalogContent);

testCall(db, "CALL apoc.dv.catalog.list()",
assertCatalogContent);

Map<String, Object> queryParams = Map.of("name", personName, "age", personAge);
testCall(db, "CALL apoc.dv.query($name, $queryParams, $config)",
Map.of("name", name, "queryParams", queryParams, "config", Map.of("header", true)),
(row) -> {
Node node = (Node) row.get("node");
assertEquals(personName, node.getProperty("name"));
assertEquals(personAge, node.getProperty("age"));
assertEquals(List.of(Label.label("Person")), node.getLabels());
});

db.executeTransactionally("create (:Hook {name: $hookNodeName})", Map.of("hookNodeName", hookNodeName));

final String relType = "LINKED_TO";
testCall(db, "MATCH (hook:Hook) WITH hook " +
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
Map.of("name", name, "queryParams", queryParams, "relType", relType, "config", Map.of("header", true, "direction", "IN")),
(row) -> {
Path path = (Path) row.get("path");
Node node = path.startNode();
assertEquals(personName, node.getProperty("name"));
assertEquals(personAge, node.getProperty("age"));
assertEquals(List.of(Label.label("Person")), node.getLabels());

Node hook = path.endNode();
assertEquals(hookNodeName, hook.getProperty("name"));
assertEquals(List.of(Label.label("Hook")), hook.getLabels());

Relationship relationship = path.lastRelationship();
assertEquals(node, relationship.getStartNode());
assertEquals(hook, relationship.getEndNode());
assertEquals(relType, relationship.getType().name());
});

}

@Test
public void testVirtualizeJDBC() {
String name = "jdbc_vr";
Expand Down Expand Up @@ -177,8 +248,8 @@ public void testVirtualizeJDBC() {

final String relType = "LINKED_TO_NEW";
testCall(db, "MATCH (hook:Hook) WITH hook " +
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
Map.of("name", name, "queryParams", queryParams, "relType", relType,
"config", Map.of("credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()))),
(row) -> {
Expand All @@ -198,6 +269,84 @@ public void testVirtualizeJDBC() {
});
}

@Test
public void testVirtualizeJDBCWithCustomDirectionIN() {
Result result = getResult();

final String relType = "LINKED_TO_NEW";
testCall(db, "MATCH (hook:Hook) WITH hook " +
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
Map.of("name", result.name(), "queryParams", result.queryParams(), "relType", relType,
"config", Map.of(
"credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()),
"direction", "IN"
)),
(row) -> {
Path path = (Path) row.get("path");
Node node = path.startNode();
assertEquals(result.country(), node.getProperty("Name"));
assertEquals(result.labels(), node.getLabels());

Node hook = path.endNode();
assertEquals(result.hookNodeName(), hook.getProperty("name"));
assertEquals(List.of(Label.label("Hook")), hook.getLabels());

Relationship relationship = path.lastRelationship();
assertEquals(node, relationship.getStartNode());
assertEquals(hook, relationship.getEndNode());
assertEquals(relType, relationship.getType().name());
});
}

private @NotNull Result getResult() {
String name = "jdbc_vr";
String desc = "country details";
List<Label> labels = List.of(Label.label("Country"));
List<String> labelsAsString = List.of("Country");
final String query = "SELECT * FROM country WHERE Name = ?";
final String url = mysql.getJdbcUrl() + "?useSSL=false";
Map<String, Object> map = Map.of("type", "JDBC",
"url", url, "query", query,
"desc", desc,
"labels", labelsAsString);

testCall(db, "CALL apoc.dv.catalog.add($name, $map)",
Map.of("name", name, "map", map),
(row) -> {
assertEquals(name, row.get("name"));
assertEquals(url, row.get("url"));
assertEquals("JDBC", row.get("type"));
assertEquals(labelsAsString, row.get("labels"));
assertEquals(desc, row.get("desc"));
assertEquals(List.of("?"), row.get("params"));
});

testCallEmpty(db, "CALL apoc.dv.query($name, ['Italy'], $config)", Map.of("name", name,
"config", Map.of("credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()))));

String country = "Netherlands";
List<String> queryParams = List.of(country);

testCall(db, "CALL apoc.dv.query($name, $queryParams, $config)",
Map.of("name", name, "queryParams", queryParams,
"config", Map.of("credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()))),
(row) -> {
Node node = (Node) row.get("node");
assertEquals(country, node.getProperty("Name"));
assertEquals(labels, node.getLabels());
});

String hookNodeName = "node to test linking";

db.executeTransactionally("create (:Hook {name: $hookNodeName})", Map.of("hookNodeName", hookNodeName));
Result result = new Result(name, labels, country, queryParams, hookNodeName);
return result;
}

private record Result(String name, List<Label> labels, String country, List<String> queryParams, String hookNodeName) {
}

@Test
public void testVirtualizeJDBCWithParameterMap() {
String name = "jdbc_vr";
Expand Down Expand Up @@ -245,8 +394,8 @@ public void testVirtualizeJDBCWithParameterMap() {

final String relType = "LINKED_TO_NEW";
testCall(db, "MATCH (hook:Hook) WITH hook " +
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
Map.of("name", name, "queryParams", queryParams, "relType", relType,
"config", Map.of("credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()))),
(row) -> {
Expand All @@ -266,6 +415,77 @@ public void testVirtualizeJDBCWithParameterMap() {
});
}

@Test
public void testVirtualizeJDBCWithParameterMapAndDirectionIN() {
String name = "jdbc_vr";
String desc = "country details";
List<Label> labels = List.of(Label.label("Country"));
List<String> labelsAsString = List.of("Country");
final String query = "SELECT * FROM country WHERE Name = $name AND HeadOfState = $head_of_state AND Code2 = $CODE2";
final String url = mysql.getJdbcUrl() + "?useSSL=false";
Map<String, Object> map = Map.of("type", "JDBC",
"url", url, "query", query,
"desc", desc,
"labels", labelsAsString);

testCall(db, "CALL apoc.dv.catalog.add($name, $map)",
Map.of("name", name, "map", map),
(row) -> {
assertEquals(name, row.get("name"));
assertEquals(url, row.get("url"));
assertEquals("JDBC", row.get("type"));
assertEquals(labelsAsString, row.get("labels"));
assertEquals(desc , row.get("desc"));
assertEquals(List.of("$name", "$head_of_state", "$CODE2"), row.get("params"));
});

testCallEmpty(db, "CALL apoc.dv.query($name, {name: 'Italy', head_of_state: '', CODE2: ''}, $config)",
Map.of("name", name, "config", Map.of("credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()))));

String country = "Netherlands";
String code2 = "NL";
String headOfState = "Beatrix";
Map<String, Object> queryParams = Map.of("name", country, "CODE2", code2, "head_of_state", headOfState);

testCall(db, "CALL apoc.dv.query($name, $queryParams, $config)",
Map.of("name", name, "queryParams", queryParams,
"config", Map.of("credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()))),
(row) -> {
Node node = (Node) row.get("node");
assertEquals(country, node.getProperty("Name"));
assertEquals(labels, node.getLabels());
});

String hookNodeName = "node to test linking";

db.executeTransactionally("create (:Hook {name: $hookNodeName})", Map.of("hookNodeName", hookNodeName));

final String relType = "LINKED_TO_NEW";
testCall(db, "MATCH (hook:Hook) WITH hook " +
"CALL apoc.dv.queryAndLink(hook, $relType, $name, $queryParams, $config) yield path " +
"RETURN path ",
Map.of("name", name, "queryParams", queryParams, "relType", relType,
"config", Map.of("" +
"credentials", Map.of("user", mysql.getUsername(), "password", mysql.getPassword()),
"direction", "IN"
)),
(row) -> {
Path path = (Path) row.get("path");
Node node = path.startNode();
assertEquals(country, node.getProperty("Name"));
assertEquals(labels, node.getLabels());

Node hook = path.endNode();
assertEquals(hookNodeName, hook.getProperty("name"));
assertEquals(List.of(Label.label("Hook")), hook.getLabels());

Relationship relationship = path.lastRelationship();
assertEquals(node, relationship.getStartNode());
assertEquals(hook, relationship.getEndNode());
assertEquals(relType, relationship.getType().name());
});
}

@Test
public void testRemove() {
String name = "jdbc_vr";
Expand All @@ -280,7 +500,7 @@ public void testRemove() {

db.executeTransactionally("CALL apoc.dv.catalog.add($name, $map)",
Map.of("name", name, "map", map));

testCallEmpty(db, "CALL apoc.dv.catalog.remove($name)", Map.of("name", name));
}

Expand Down Expand Up @@ -377,4 +597,4 @@ public void testVirtualizeJDBCWithDifferentParameterMap() {
assertEquals(String.format("Expected query parameters are %s, actual are %s", sortedExpectedParams, actualParams), rootCause.getMessage());
}
}
}
}
17 changes: 16 additions & 1 deletion extended/src/main/java/apoc/dv/DataVirtualizationCatalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.neo4j.procedure.Procedure;

import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

@Extended
Expand Down Expand Up @@ -83,15 +84,29 @@ public Stream<PathResult> queryAndLink(@Name("node") Node node,
VirtualizedResource vr = new DataVirtualizationCatalogHandler(db, apocConfig.getSystemDb(), null).get(name);
final RelationshipType relationshipType = RelationshipType.withName(relName);
final Pair<String, Map<String, Object>> procedureCallWithParams = vr.getProcedureCallWithParams(params, config);

String direction = (String) config.getOrDefault("direction", Direction.OUT.name());

return tx.execute(procedureCallWithParams.getLeft(), procedureCallWithParams.getRight())
.stream()
.map(m -> (Node) m.get(("node")))
.map(n -> new VirtualRelationship(node, n, relationshipType))
.map(n -> getVirtualRelationship(node, n, direction, relationshipType))
.map(r -> {
VirtualPath virtualPath = new VirtualPath(r.getStartNode());
virtualPath.addRel(r);
return virtualPath;
})
.map(PathResult::new);
}

private VirtualRelationship getVirtualRelationship(Node node, Node n, String direction, RelationshipType relationshipType) {
if ( Objects.equals(direction.toUpperCase(), Direction.OUT.name()) ) {
return new VirtualRelationship(node, n, relationshipType);
}
return new VirtualRelationship(n, node, relationshipType);
}

enum Direction {
IN, OUT;
}
}

0 comments on commit 7f1bc7f

Please sign in to comment.