Skip to content

Commit

Permalink
Add apoc.diff user functions (#760)
Browse files Browse the repository at this point in the history
  • Loading branch information
binarycoded authored and jexp committed Jul 22, 2018
1 parent 71c938b commit ffd4d23
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 0 deletions.
33 changes: 33 additions & 0 deletions docs/diff.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
= Diff

Diff is a user function to return a detailed diff between two nodes.

*apoc.diff.nodes([leftNode],[rightNode])*

Example
[source,cypher]
CREATE
(n:Person{name:'Steve',age:34, eyes:'blue'}),
(m:Person{name:'Jake',hair:'brown',age:34})
WITH n,m
return apoc.diff.nodes(n,m)

Resulting JSON body:
[source,json]
{
"leftOnly": {
"eyes": "blue"
},
"inCommon": {
"age": 34
},
"different": {
"name": {
"left": "Steve",
"right": "Jake"
}
},
"rightOnly": {
"hair": "brown"
}
}
2 changes: 2 additions & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ include::number.adoc[leveloffset=2]

include::exact.adoc[leveloffset=2]

include::diff.adoc[leveloffset=2]

== Graph Algorithms

include::algo.adoc[leveloffset=2]
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/apoc/diff/Diff.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package apoc.diff;

import apoc.Description;
import apoc.util.MapUtil;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.procedure.*;

import java.util.HashMap;
import java.util.Map;

/**
* @author Benjamin Clauss
* @since 15.06.2018
*/
public class Diff {

@Context
public GraphDatabaseService db;

@UserFunction()
@Description("apoc.diff.nodes([leftNode],[rightNode]) returns a detailed diff of both nodes")
public Map<String, Object> nodes(@Name("leftNode") Node leftNode, @Name("rightNode") Node rightNode) {
Map<String, Object> allLeftProperties = leftNode.getAllProperties();
Map<String, Object> allRightProperties = rightNode.getAllProperties();

Map<String, Object> result = new HashMap<>();
result.put("leftOnly", getPropertiesOnlyLeft(allLeftProperties, allRightProperties));
result.put("rightOnly", getPropertiesOnlyLeft(allRightProperties, allLeftProperties));
result.put("inCommon", getPropertiesInCommon(allLeftProperties, allRightProperties));
result.put("different", getPropertiesDiffering(allLeftProperties, allRightProperties));

return result;
}

private Map<String, Object> getPropertiesOnlyLeft(Map<String, Object> left, Map<String, Object> right) {
Map<String, Object> leftOnly = new HashMap<>();
leftOnly.putAll(left);
leftOnly.keySet().removeAll(right.keySet());
return leftOnly;
}

private Map<String, Object> getPropertiesInCommon(Map<String, Object> left, Map<String, Object> right) {
Map<String, Object> inCommon = new HashMap<>(left);
inCommon.entrySet().retainAll(right.entrySet());
return inCommon;
}

private Map<String, Map<String, Object>> getPropertiesDiffering(Map<String, Object> left, Map<String, Object> right) {
Map<String, Map<String, Object>> different = new HashMap<>();
Map<String, Object> keyPairs = new HashMap<>();
keyPairs.putAll(left);
keyPairs.keySet().retainAll(right.keySet());

for (Map.Entry<String, Object> entry : keyPairs.entrySet()) {
if (!left.get(entry.getKey()).equals(right.get(entry.getKey()))) {
Map<String, Object> pairs = new HashMap<>();
pairs.put("left", left.get(entry.getKey()));
pairs.put("right", right.get(entry.getKey()));
different.put(entry.getKey(), pairs);
}
}
return different;
}
}
113 changes: 113 additions & 0 deletions src/test/java/apoc/diff/DiffTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package apoc.diff;

import apoc.util.TestUtil;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;
import org.neo4j.test.TestGraphDatabaseFactory;

import java.util.HashMap;
import java.util.Map;

import static org.junit.Assert.*;

/**
* @author Benjamin Clauss
* @since 15.06.2018
*/
public class DiffTest {

private static Node node1;
private static Node node2;
private static Node node3;

private static GraphDatabaseService db;

@BeforeClass
public static void setup() throws Exception {
db = new TestGraphDatabaseFactory().newImpermanentDatabase();
TestUtil.registerProcedure(db, Diff.class);

try (Transaction tx = db.beginTx()) {
node1 = db.createNode();
node1.setProperty("prop1", "val1");
node1.setProperty("prop2", 2L);

node2 = db.createNode();
node2.setProperty("prop1", "val1");
node2.setProperty("prop2", 2L);
node2.setProperty("prop4", "four");

node3 = db.createNode();
node3.setProperty("prop1", "val1");
node3.setProperty("prop3", "3");
node3.setProperty("prop4", "for");
tx.success();
}
}

@Test
public void nodesSame() {
Map<String, Object> params = new HashMap<>();
params.put("leftNode", node1);
params.put("rightNode", node1);

Map<String, Object> result =
(Map<String, Object>) db.execute(
"RETURN apoc.diff.nodes($leftNode, $rightNode) as diff", params).next().get("diff");

assertNotNull(result);

HashMap<String, Object> leftOnly = (HashMap<String, Object>) result.get("leftOnly");
assertTrue(leftOnly.isEmpty());

HashMap<String, Object> rightOnly = (HashMap<String, Object>) result.get("rightOnly");
assertTrue(rightOnly.isEmpty());

HashMap<String, Object> different = (HashMap<String, Object>) result.get("different");
assertTrue(different.isEmpty());

HashMap<String, Object> inCommon = (HashMap<String, Object>) result.get("inCommon");
assertEquals(2, inCommon.size());
assertEquals("val1", inCommon.get("prop1"));
assertEquals(2L, inCommon.get("prop2"));
}

@Test
public void nodesDiffering() {
Map<String, Object> params = new HashMap<>();
params.put("leftNode", node2);
params.put("rightNode", node3);
Map<String, Object> result =
(Map<String, Object>) db.execute(
"RETURN apoc.diff.nodes($leftNode, $rightNode) as diff", params).next().get("diff");

assertNotNull(result);

HashMap<String, Object> leftOnly = (HashMap<String, Object>) result.get("leftOnly");
assertEquals(1, leftOnly.size());
assertEquals(2L, leftOnly.get("prop2"));

HashMap<String, Object> rightOnly = (HashMap<String, Object>) result.get("rightOnly");
assertEquals(1, rightOnly.size());
assertEquals("3", rightOnly.get("prop3"));

HashMap<String, HashMap<String, Object>> different = (HashMap<String, HashMap<String, Object>>) result.get("different");
assertEquals(1, different.size());
HashMap<String, Object> pairs = different.get("prop4");
assertEquals("four", pairs.get("left"));
assertEquals("for", pairs.get("right"));

HashMap<String, Object> inCommon = (HashMap<String, Object>) result.get("inCommon");
assertEquals(1, inCommon.size());
assertEquals("val1", inCommon.get("prop1"));
}

@AfterClass
public static void tearDown() {
db.shutdown();
}
}

0 comments on commit ffd4d23

Please sign in to comment.