Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AUTO: Fixes #2239: Provide query statistics for apoc.merge functions #2584

Merged
merged 1 commit into from
Apr 21, 2022
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
11 changes: 9 additions & 2 deletions core/src/main/java/apoc/cypher/Cypher.java
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,16 @@ private boolean isPeriodicOperation(String stmt) {
}

private Map<String, Object> toMap(QueryStatistics stats, long time, long rows) {
final Map<String, Object> map = map(
"rows", rows,
"time", time
);
map.putAll(toMap(stats));
return map;
}

public static Map<String, Object> toMap(QueryStatistics stats) {
return map(
"rows",rows,
"time",time,
"nodesCreated",stats.getNodesCreated(),
"nodesDeleted",stats.getNodesDeleted(),
"labelsAdded",stats.getLabelsAdded(),
Expand Down
73 changes: 65 additions & 8 deletions core/src/main/java/apoc/merge/Merge.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package apoc.merge;

import apoc.cypher.Cypher;
import apoc.result.NodeResult;
import apoc.result.NodeResultWithStats;
import apoc.result.RelationshipResultWithStats;
import apoc.result.RelationshipResult;
import apoc.util.Util;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Result;
import org.neo4j.graphdb.Transaction;
import org.neo4j.procedure.*;

Expand All @@ -31,12 +35,37 @@ public Stream<NodeResult> nodesEager(@Name("label") List<String> labelNames,
}

@Procedure(value="apoc.merge.node", mode = Mode.WRITE)
@Description("\"apoc.merge.node.eager(['Label'], identProps:{key:value, ...}, onCreateProps:{key:value,...}, onMatchProps:{key:value,...}}) - merge nodes with dynamic labels, with support for setting properties ON CREATE or ON MATCH")
@Description("\"apoc.merge.node(['Label'], identProps:{key:value, ...}, onCreateProps:{key:value,...}, onMatchProps:{key:value,...}}) - merge nodes with dynamic labels, with support for setting properties ON CREATE or ON MATCH")
public Stream<NodeResult> nodes(@Name("label") List<String> labelNames,
@Name("identProps") Map<String, Object> identProps,
@Name(value = "props",defaultValue = "{}") Map<String, Object> props,
@Name(value = "onMatchProps",defaultValue = "{}") Map<String, Object> onMatchProps) {
if (identProps==null || identProps.isEmpty()) {
@Name("identProps") Map<String, Object> identProps,
@Name(value = "props",defaultValue = "{}") Map<String, Object> props,
@Name(value = "onMatchProps",defaultValue = "{}") Map<String, Object> onMatchProps) {
final Result nodeResult = getNodeResult(labelNames, identProps, props, onMatchProps);
return nodeResult.columnAs("n").stream().map(node -> new NodeResult((Node) node));
}

@Procedure(value="apoc.merge.nodeWithStats.eager", mode = Mode.WRITE, eager = true)
@Description("apoc.merge.nodeWithStats.eager - same as apoc.merge.node.eager providing queryStatistics into result")
public Stream<NodeResultWithStats> nodeWithStatsEager(@Name("label") List<String> labelNames,
@Name("identProps") Map<String, Object> identProps,
@Name(value = "props",defaultValue = "{}") Map<String, Object> props,
@Name(value = "onMatchProps",defaultValue = "{}") Map<String, Object> onMatchProps) {
return nodeWithStats(labelNames, identProps,props,onMatchProps);
}

@Procedure(value="apoc.merge.nodeWithStats", mode = Mode.WRITE)
@Description("apoc.merge.nodeWithStats - same as apoc.merge.node providing queryStatistics into result")
public Stream<NodeResultWithStats> nodeWithStats(@Name("label") List<String> labelNames,
@Name("identProps") Map<String, Object> identProps,
@Name(value = "props",defaultValue = "{}") Map<String, Object> props,
@Name(value = "onMatchProps",defaultValue = "{}") Map<String, Object> onMatchProps) {
final Result nodeResult = getNodeResult(labelNames, identProps, props, onMatchProps);
return nodeResult.columnAs("n").stream()
.map(node -> new NodeResultWithStats((Node) node, Cypher.toMap(nodeResult.getQueryStatistics())));
}

private Result getNodeResult(List<String> labelNames, Map<String, Object> identProps, Map<String, Object> props, Map<String, Object> onMatchProps) {
if (identProps ==null || identProps.isEmpty()) {
throw new IllegalArgumentException("you need to supply at least one identifying property for a merge");
}

Expand All @@ -46,7 +75,7 @@ public Stream<NodeResult> nodes(@Name("label") List<String> labelNames,
String identPropsString = buildIdentPropsString(identProps);

final String cypher = "MERGE (n:" + labels + "{" + identPropsString + "}) ON CREATE SET n += $onCreateProps ON MATCH SET n += $onMatchProps RETURN n";
return tx.execute(cypher, params ).columnAs("n").stream().map(node -> new NodeResult((Node) node));
return tx.execute(cypher, params);
}

@Procedure(value = "apoc.merge.relationship", mode = Mode.WRITE)
Expand All @@ -56,9 +85,26 @@ public Stream<RelationshipResult> relationship(@Name("startNode") Node startNode
@Name("props") Map<String, Object> onCreateProps,
@Name("endNode") Node endNode,
@Name(value = "onMatchProps",defaultValue = "{}") Map<String, Object> onMatchProps) {
final Result execute = getRelResult(startNode, relType, identProps, onCreateProps, endNode, onMatchProps);
return execute.columnAs("r").stream().map(rel -> new RelationshipResult((Relationship) rel));
}

@Procedure(value = "apoc.merge.relationshipWithStats", mode = Mode.WRITE)
@Description("apoc.merge.relationshipWithStats - same as apoc.merge.relationship providing queryStatistics into result")
public Stream<RelationshipResultWithStats> relationshipWithStats(@Name("startNode") Node startNode, @Name("relationshipType") String relType,
@Name("identProps") Map<String, Object> identProps,
@Name("props") Map<String, Object> onCreateProps,
@Name("endNode") Node endNode,
@Name(value = "onMatchProps",defaultValue = "{}") Map<String, Object> onMatchProps) {
final Result relResult = getRelResult(startNode, relType, identProps, onCreateProps, endNode, onMatchProps);
return relResult.columnAs("r").stream()
.map(rel -> new RelationshipResultWithStats((Relationship) rel, Cypher.toMap(relResult.getQueryStatistics())));
}

private Result getRelResult(Node startNode, String relType, Map<String, Object> identProps, Map<String, Object> onCreateProps, Node endNode, Map<String, Object> onMatchProps) {
String identPropsString = buildIdentPropsString(identProps);

Map<String, Object> params = Util.map("identProps", identProps, "onCreateProps", onCreateProps==null ? emptyMap() : onCreateProps,
Map<String, Object> params = Util.map("identProps", identProps, "onCreateProps", onCreateProps ==null ? emptyMap() : onCreateProps,
"onMatchProps", onMatchProps == null ? emptyMap() : onMatchProps, "startNode", startNode, "endNode", endNode);

final String cypher =
Expand All @@ -67,8 +113,9 @@ public Stream<RelationshipResult> relationship(@Name("startNode") Node startNode
"ON CREATE SET r+= $onCreateProps " +
"ON MATCH SET r+= $onMatchProps " +
"RETURN r";
return tx.execute(cypher, params ).columnAs("r").stream().map(rel -> new RelationshipResult((Relationship) rel));
return tx.execute(cypher, params);
}

@Procedure(value = "apoc.merge.relationship.eager", mode = Mode.WRITE, eager = true)
@Description("apoc.merge.relationship(startNode, relType, identProps:{key:value, ...}, onCreateProps:{key:value, ...}, endNode, onMatchProps:{key:value, ...}) - merge relationship with dynamic type, with support for setting properties ON CREATE or ON MATCH")
public Stream<RelationshipResult> relationshipEager(@Name("startNode") Node startNode, @Name("relationshipType") String relType,
Expand All @@ -79,6 +126,16 @@ public Stream<RelationshipResult> relationshipEager(@Name("startNode") Node star
return relationship(startNode, relType, identProps, onCreateProps, endNode, onMatchProps );
}

@Procedure(value = "apoc.merge.relationshipWithStats.eager", mode = Mode.WRITE, eager = true)
@Description("apoc.merge.relationshipWithStats.eager - same as apoc.merge.relationship.eager providing queryStatistics into result")
public Stream<RelationshipResultWithStats> relationshipWithStatsEager(@Name("startNode") Node startNode, @Name("relationshipType") String relType,
@Name("identProps") Map<String, Object> identProps,
@Name("props") Map<String, Object> onCreateProps,
@Name("endNode") Node endNode,
@Name(value = "onMatchProps",defaultValue = "{}") Map<String, Object> onMatchProps) {
return relationshipWithStats(startNode, relType, identProps, onCreateProps, endNode, onMatchProps );
}


private String buildIdentPropsString(Map<String, Object> identProps) {
if (identProps == null) return "";
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/apoc/result/NodeResultWithStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package apoc.result;

import org.neo4j.graphdb.Node;

import java.util.Map;


public class NodeResultWithStats extends NodeResult {
public final Map<String, Object> stats;

public NodeResultWithStats(Node node, Map<String, Object> stats) {
super(node);
this.stats = stats;
}
}
15 changes: 15 additions & 0 deletions core/src/main/java/apoc/result/RelationshipResultWithStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package apoc.result;

import org.neo4j.graphdb.Relationship;

import java.util.Map;


public class RelationshipResultWithStats extends RelationshipResult {
public final Map<String, Object> stats;

public RelationshipResultWithStats(Relationship relationship, Map<String, Object> stats) {
super(relationship);
this.stats = stats;
}
}
80 changes: 74 additions & 6 deletions core/src/test/java/apoc/merge/MergeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,31 @@ public void setUp() throws Exception {

@Test
public void testMergeNode() throws Exception {
testCall(db, "CALL apoc.merge.node(['Person','Bastard'],{ssid:'123'}, {name:'John'}) YIELD node RETURN node",
testMergeNodeCommon(false);
}

@Test
public void testMergeNodeWithStats() {
testMergeNodeCommon(true);
}

private void testMergeNodeCommon(boolean isWithStats) {
String procName = isWithStats ? "nodeWithStats" : "node";

testCall(db, String.format("CALL apoc.merge.%s(['Person','Bastard'],{ssid:'123'}, {name:'John'})", procName),
(row) -> {
Node node = (Node) row.get("node");
assertEquals(true, node.hasLabel(Label.label("Person")));
assertEquals(true, node.hasLabel(Label.label("Bastard")));
assertEquals("John", node.getProperty("name"));
assertEquals("123", node.getProperty("ssid"));

if (isWithStats) {
Map<String, Object> stats = (Map<String, Object>) row.get("stats");
assertEquals(2, stats.get("labelsAdded"));
assertEquals(1, stats.get("nodesCreated"));
assertEquals(2, stats.get("propertiesSet"));
}
});
}

Expand Down Expand Up @@ -135,13 +153,30 @@ public void testRelationshipTypesWithSpecialCharactersShouldWork() {

@Test
public void testMergeEagerNode() throws Exception {
testCall(db, "CALL apoc.merge.node.eager(['Person','Bastard'],{ssid:'123'}, {name:'John'}) YIELD node RETURN node",
testMergeEagerCommon(false);
}

@Test
public void testMergeEagerNodeWithStats() {
testMergeEagerCommon(true);
}

private void testMergeEagerCommon(boolean isWithStats) {
String procName = isWithStats ? "nodeWithStats" : "node";
testCall(db, String.format("CALL apoc.merge.%s.eager(['Person','Bastard'],{ssid:'123'}, {name:'John'})", procName),
(row) -> {
Node node = (Node) row.get("node");
assertEquals(true, node.hasLabel(Label.label("Person")));
assertEquals(true, node.hasLabel(Label.label("Bastard")));
assertEquals("John", node.getProperty("name"));
assertEquals("123", node.getProperty("ssid"));

if (isWithStats) {
final Map<String, Object> stats = (Map<String, Object>) row.get("stats");
assertEquals(2, stats.get("labelsAdded"));
assertEquals(1, stats.get("nodesCreated"));
assertEquals(2, stats.get("propertiesSet"));
}
});
}

Expand Down Expand Up @@ -195,28 +230,61 @@ public void testMergeEagerNodesWithOnMatchCanMergeOnMultipleMatches() throws Exc

@Test
public void testMergeEagerRelationships() throws Exception {
db.executeTransactionally("create (:Person{name:'Foo'}), (:Person{name:'Bar'})");
testMergeRelsCommon(false);
}

@Test
public void testMergeEagerRelationshipsWithStats() {
testMergeRelsCommon(true);
}

testCall(db, "MERGE (s:Person{name:'Foo'}) MERGE (e:Person{name:'Bar'}) WITH s,e CALL apoc.merge.relationship.eager(s, 'KNOWS', {rid:123}, {since:'Thu'}, e) YIELD rel RETURN rel",
private void testMergeRelsCommon(boolean isWithStats) {
db.executeTransactionally("create (:Person{name:'Foo'}), (:Person{name:'Bar'})");

String procName = isWithStats ? "relationshipWithStats" : "relationship";
String returnClause = isWithStats ? "YIELD rel, stats RETURN rel, stats" : "YIELD rel RETURN rel";
testCall(db, String.format("MERGE (s:Person{name:'Foo'}) MERGE (e:Person{name:'Bar'}) WITH s,e CALL apoc.merge.%s.eager(s, 'KNOWS', {rid:123}, {since:'Thu'}, e) %s",
procName, returnClause),
(row) -> {
Relationship rel = (Relationship) row.get("rel");
assertEquals("KNOWS", rel.getType().name());
assertEquals(123l, rel.getProperty("rid"));
assertEquals("Thu", rel.getProperty("since"));

if (isWithStats) {
final Map<String, Object> stats = (Map<String, Object>) row.get("stats");
assertEquals(1, stats.get("relationshipsCreated"));
assertEquals(2, stats.get("propertiesSet"));
}
});

testCall(db, "MERGE (s:Person{name:'Foo'}) MERGE (e:Person{name:'Bar'}) WITH s,e CALL apoc.merge.relationship.eager(s, 'KNOWS', {rid:123}, {since:'Fri'}, e) YIELD rel RETURN rel",
testCall(db, String.format("MERGE (s:Person{name:'Foo'}) MERGE (e:Person{name:'Bar'}) WITH s,e CALL apoc.merge.%s.eager(s, 'KNOWS', {rid:123}, {since:'Fri'}, e) %s",
procName, returnClause),
(row) -> {
Relationship rel = (Relationship) row.get("rel");
assertEquals("KNOWS", rel.getType().name());
assertEquals(123l, rel.getProperty("rid"));
assertEquals("Thu", rel.getProperty("since"));

if (isWithStats) {
final Map<String, Object> stats = (Map<String, Object>) row.get("stats");
assertEquals(0, stats.get("relationshipsCreated"));
assertEquals(0, stats.get("propertiesSet"));
}
});
testCall(db, "MERGE (s:Person{name:'Foo'}) MERGE (e:Person{name:'Bar'}) WITH s,e CALL apoc.merge.relationship(s, 'OTHER', null, null, e) YIELD rel RETURN rel",

testCall(db, String.format("MERGE (s:Person{name:'Foo'}) MERGE (e:Person{name:'Bar'}) WITH s,e CALL apoc.merge.%s(s, 'OTHER', null, null, e) %s",
procName, returnClause),
(row) -> {
Relationship rel = (Relationship) row.get("rel");
assertEquals("OTHER", rel.getType().name());
assertTrue(rel.getAllProperties().isEmpty());

if (isWithStats) {
final Map<String, Object> stats = (Map<String, Object>) row.get("stats");
assertEquals(1, stats.get("relationshipsCreated"));
assertEquals(0, stats.get("propertiesSet"));
}
});
}

Expand Down
22 changes: 22 additions & 0 deletions docs/asciidoc/modules/ROOT/pages/graph-updates/data-creation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ include::example$generated-documentation/apoc.create.setRelProperties.adoc[]
include::example$generated-documentation/apoc.create.relationship.adoc[]
include::example$generated-documentation/apoc.nodes.link.adoc[]
include::example$generated-documentation/apoc.merge.relationship.adoc[]
¦xref::overview/apoc.merge/apoc.merge.nodeWithStats.adoc[apoc.merge.nodeWithStats icon:book[]] +

- same as apoc.merge.node providing queryStatistics into result
¦label:procedure[]
¦label:apoc-core[]
¦xref::overview/apoc.merge/apoc.merge.nodeWithStats.eager.adoc[apoc.merge.nodeWithStats.eager icon:book[]] +

- same as apoc.merge.node.eager providing queryStatistics into result
¦label:procedure[]
¦label:apoc-core[]
¦xref::overview/apoc.merge/apoc.merge.relationshipWithStats.adoc[apoc.merge.relationshipWithStats icon:book[]] +

- same as apoc.merge.relationship providing queryStatistics into result
¦label:procedure[]
¦label:apoc-core[]

¦xref::overview/apoc.merge/apoc.merge.relationshipWithStats.eager.adoc[apoc.merge.relationshipWithStats.eager icon:book[]] +

- same as apoc.merge.relationship.eager providing queryStatistics into result
¦label:procedure[]
¦label:apoc-core[]

include::example$generated-documentation/apoc.create.removeProperties.adoc[]
include::example$generated-documentation/apoc.create.removeRelProperties.adoc[]
|===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file is generated by DocsTest, so don't change it!
label:procedure[] label:apoc-core[]

[.emphasis]
"apoc.merge.node.eager(['Label'], identProps:{key:value, ...}, onCreateProps:{key:value,...}, onMatchProps:{key:value,...}}) - merge nodes with dynamic labels, with support for setting properties ON CREATE or ON MATCH
"apoc.merge.node(['Label'], identProps:{key:value, ...}, onCreateProps:{key:value,...}, onMatchProps:{key:value,...}}) - merge nodes with dynamic labels, with support for setting properties ON CREATE or ON MATCH

== Signature

Expand Down
Loading