Skip to content

Instantly share code, notes, and snippets.

@michael-simons
Created March 31, 2023 15:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michael-simons/d921f5a96f6f3416c1e2b54060322107 to your computer and use it in GitHub Desktop.
Save michael-simons/d921f5a96f6f3416c1e2b54060322107 to your computer and use it in GitHub Desktop.
//usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.neo4j.driver:neo4j-java-driver:5.7.0
//DEPS com.fasterxml.jackson.core:jackson-databind:2.14.1
package ac.simons;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ExecuteQueryApiDemo {
public static void main(String... a) throws IOException {
var start = Paths.get(a.length == 0 ? System.getProperty("user.dir") : a[0]);
try (
var driver = GraphDatabase.driver("neo4j://localhost:7687", AuthTokens.basic("neo4j", "verysecret"));
) {
createData(start, driver);
System.out.println("--");
// printWithApocAndComplexStatement(start, driver);
System.out.println("--");
// intoHierachyAndPrint(driver);
List<String> names = driver.executableQuery("MATCH (n:Path) RETURN n.name AS name")
.execute(Collectors.mapping(r -> r.get("name").asString(), Collectors.toList()));
System.out.println();
System.out.println(names);
}
}
private static void createData(Path root, Driver driver) throws IOException {
var paths = Files.walk(root)
.map(p -> Map.of(
"parent_id", p.getParent().toString(),
"id", p.toString(),
"name", p.getFileName().toString()))
.toList();
driver
// creates an executable query
.executableQuery("MATCH (n) DETACH DELETE n")
// and executes it. There is no need to consume or close the result
.execute();
// Using an eager result
var result = driver
// Again, creating the executable query
.executableQuery("""
UNWIND $paths AS path WITH path
MERGE (c:Path {id: path.id, name: path.name})
MERGE (p:Path {id: path.parent_id})
MERGE (c)-[r:HAS_PARENT]->(p)
RETURN c, r, p
""")
// but enriching it with parameters
.withParameters(Map.of("paths", paths))
.execute();
// Gives you access to the result summary, including counters and more,
// no need to consume something upfront
var counters = result.summary().counters();
System.out.println(
counters.nodesCreated() + " nodes and " +
counters.relationshipsCreated() + " relationships have been created");
// The returned records are already materialized, iterating them multiple
// times is safe and does not involve multiple round trips
var c1 = result.records().stream()
.mapToInt(r -> r.get("c").get("name").asString().length())
.summaryStatistics().getMax();
var c2 = result.records().stream()
.mapToInt(r -> r.get("p").get("name").asString().length())
.summaryStatistics().getMax();
var format = "| %1$-" + c1 + "s | %2$-" + c2 + "s |%n";
System.out.printf((format), "Name", "Parent");
System.out.println("|" + "-".repeat(c1 + 2) + "|" + "-".repeat(c2 + 2) + "|");
result.records().forEach(r -> {
var c = r.get("c").asNode();
var p = r.get("p").asNode();
System.out.printf(format, c.get("name").asString(), p.get("name").asString());
});
}
private static void printWithApocAndComplexStatement(Path root, Driver driver) {
var result = driver.executableQuery("""
MATCH (r:Path {name: $nameOfRoot})
MATCH (l:Path) WHERE NOT (EXISTS {MATCH (l)<-[:HAS_PARENT]-(:Path)})
MATCH path=(r) <-[:HAS_PARENT*]-(l)
WITH collect(path) AS paths
CALL apoc.convert.toTree(paths, false, {nodes: {Path: ['-id']}}) YIELD value
RETURN apoc.convert.toJson(value) AS result
""")
.withParameters(Map.of("nameOfRoot", root.getFileName().toString()))
.execute();
System.out.println(result.records().get(0).get("result").asString());
}
record File(
@JsonIgnore String id,
String name,
@JsonInclude(JsonInclude.Include.NON_EMPTY) List<File> children) {
}
public static <K, E, R extends Record> Collector<R, ?, List<E>> intoHierarchy(
Function<? super R, ? extends K> keyMapper,
Function<? super R, ? extends K> parentKeyMapper,
Function<? super R, ? extends E> nodeMapper,
BiConsumer<? super E, ? super E> parentChildAppender
) {
return Collectors.collectingAndThen(
Collectors.toMap(keyMapper, r -> new AbstractMap.SimpleImmutableEntry<R, E>(
r, nodeMapper.apply(r)
)),
m -> {
List<E> r = new ArrayList<>();
m.forEach((k, v) -> {
Map.Entry<R, E> parent = m.get(
parentKeyMapper.apply(v.getKey())
);
if (parent != null) {
parentChildAppender.accept(
parent.getValue(), v.getValue()
);
} else {
r.add(v.getValue());
}
});
return r;
}
);
}
private static void intoHierachyAndPrint(Driver driver) throws IOException {
var result = driver
.executableQuery("""
MATCH (p:Path) <-[:HAS_PARENT]-(c:Path)
RETURN
elementId(c) AS id,
elementId(p) AS parentId,
c.name AS name
""")
// This will take care of iterating a non-eager-result-set
// for us plus all the added benefits of using retries internally
// It won't allow us to take the non-eager-result set out of
// transaction scope which is an excellent thing
.execute(intoHierarchy(
r -> r.get("id").asString(),
r -> r.get("parentId").asString(),
r -> new File(r.get("id").asString(), r.get("name").asString(), new ArrayList<>()),
(p, c) -> p.children().add(c)
));
new ObjectMapper()
.writerWithDefaultPrettyPrinter()
.writeValue(System.out, result);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment