Neo4j Community Edition - The world's leading Graph Database with Cypher query language and ACID transactions.
—
Framework for creating custom Cypher-callable procedures and functions with dependency injection, supporting READ, WRITE, SCHEMA, and DBMS execution modes for extending Neo4j functionality.
Annotation for declaring methods as Cypher-callable procedures with mode specification and metadata.
/**
* Declares methods as Cypher-callable procedures
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Procedure {
/**
* Name of the procedure in Cypher (defaults to method name)
* @return Procedure name
*/
String name() default "";
/**
* Execution mode for the procedure
* @return Procedure execution mode
*/
Mode mode() default Mode.READ;
/**
* Whether this procedure is deprecated
* @return true if deprecated
*/
boolean deprecated() default false;
/**
* Deprecation message if deprecated
* @return Deprecation message
*/
String deprecatedBy() default "";
}Usage Examples:
import org.neo4j.procedure.Procedure;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Description;
import java.util.stream.Stream;
public class UserProcedures {
@Procedure(name = "user.create", mode = Mode.WRITE)
@Description("Create a new user with the given name and email")
public Stream<UserResult> createUser(
@Name("name") String name,
@Name("email") String email) {
// Create user node with transaction from context
Node userNode = tx.createNode(Label.label("User"));
userNode.setProperty("name", name);
userNode.setProperty("email", email);
userNode.setProperty("createdAt", Instant.now().toString());
return Stream.of(new UserResult(userNode.getId(), name, email));
}
@Procedure(name = "user.findByEmail", mode = Mode.READ)
@Description("Find a user by email address")
public Stream<UserResult> findUserByEmail(@Name("email") String email) {
ResourceIterable<Node> users = tx.findNodes(Label.label("User"), "email", email);
return StreamSupport.stream(users.spliterator(), false)
.map(node -> new UserResult(
node.getId(),
(String) node.getProperty("name"),
(String) node.getProperty("email")
));
}
@Procedure(name = "user.delete", mode = Mode.WRITE)
@Description("Delete a user and all their relationships")
public Stream<DeleteResult> deleteUser(@Name("userId") Long userId) {
Node user = tx.getNodeById(userId);
// Delete all relationships
int relationshipsDeleted = 0;
for (Relationship rel : user.getRelationships()) {
rel.delete();
relationshipsDeleted++;
}
// Delete the node
user.delete();
return Stream.of(new DeleteResult(userId, relationshipsDeleted));
}
// Result classes
public static class UserResult {
public final Long id;
public final String name;
public final String email;
public UserResult(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
}
public static class DeleteResult {
public final Long userId;
public final int relationshipsDeleted;
public DeleteResult(Long userId, int relationshipsDeleted) {
this.userId = userId;
this.relationshipsDeleted = relationshipsDeleted;
}
}
}Annotation for declaring methods as user-defined functions callable from Cypher expressions.
/**
* Declares methods as user-defined functions
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserFunction {
/**
* Name of the function in Cypher (defaults to method name)
* @return Function name
*/
String name() default "";
/**
* Whether this function is deprecated
* @return true if deprecated
*/
boolean deprecated() default false;
/**
* Deprecation message if deprecated
* @return Deprecation message
*/
String deprecatedBy() default "";
}Usage Examples:
import org.neo4j.procedure.UserFunction;
import java.time.LocalDate;
import java.time.Period;
import java.util.List;
import java.util.Map;
public class UtilityFunctions {
@UserFunction(name = "util.calculateAge")
@Description("Calculate age from birth date")
public Long calculateAge(@Name("birthDate") LocalDate birthDate) {
if (birthDate == null) return null;
return (long) Period.between(birthDate, LocalDate.now()).getYears();
}
@UserFunction(name = "util.formatName")
@Description("Format a name with proper capitalization")
public String formatName(@Name("name") String name) {
if (name == null || name.trim().isEmpty()) return null;
return Arrays.stream(name.trim().toLowerCase().split("\\s+"))
.map(word -> word.substring(0, 1).toUpperCase() + word.substring(1))
.collect(Collectors.joining(" "));
}
@UserFunction(name = "util.distance")
@Description("Calculate distance between two points")
public Double calculateDistance(
@Name("lat1") Double lat1, @Name("lon1") Double lon1,
@Name("lat2") Double lat2, @Name("lon2") Double lon2) {
if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) {
return null;
}
// Haversine distance calculation
double R = 6371; // Earth's radius in kilometers
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
@UserFunction(name = "util.jsonExtract")
@Description("Extract value from JSON string by key path")
public Object extractFromJson(@Name("json") String json, @Name("path") String path) {
// Implementation would parse JSON and extract value by path
// This is a simplified example
return parseJsonPath(json, path);
}
}
// Usage in Cypher:
// MATCH (p:Person)
// RETURN p.name, util.calculateAge(p.birthDate) as age
//
// MATCH (p1:Person), (p2:Person)
// WHERE util.distance(p1.lat, p1.lon, p2.lat, p2.lon) < 10
// RETURN p1.name, p2.nameAnnotation for injecting Neo4j resources into procedure and function classes.
/**
* Inject Neo4j resources into procedure classes
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Context {
}Usage Examples:
import org.neo4j.procedure.Context;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Transaction;
import org.neo4j.logging.Log;
public class DataAnalysisProcedures {
@Context
public GraphDatabaseService db;
@Context
public Transaction tx;
@Context
public Log log;
@Procedure(name = "analysis.nodeStats", mode = Mode.READ)
@Description("Get statistics about nodes in the database")
public Stream<NodeStatsResult> getNodeStatistics() {
log.info("Starting node statistics analysis");
Map<String, Long> labelCounts = new HashMap<>();
// Count nodes by label
for (Label label : GlobalGraphOperations.at(db).getAllLabels()) {
long count = 0;
try (ResourceIterable<Node> nodes = tx.findNodes(label)) {
for (Node node : nodes) {
count++;
}
}
labelCounts.put(label.name(), count);
log.debug("Label " + label.name() + ": " + count + " nodes");
}
return labelCounts.entrySet().stream()
.map(entry -> new NodeStatsResult(entry.getKey(), entry.getValue()));
}
@Procedure(name = "analysis.relationshipStats", mode = Mode.READ)
@Description("Get statistics about relationships in the database")
public Stream<RelationshipStatsResult> getRelationshipStatistics() {
Map<String, Long> typeCounts = new HashMap<>();
// Count relationships by type
for (RelationshipType type : GlobalGraphOperations.at(db).getAllRelationshipTypes()) {
long count = 0;
for (Relationship rel : GlobalGraphOperations.at(db).getAllRelationships()) {
if (rel.isType(type)) {
count++;
}
}
typeCounts.put(type.name(), count);
}
return typeCounts.entrySet().stream()
.map(entry -> new RelationshipStatsResult(entry.getKey(), entry.getValue()));
}
public static class NodeStatsResult {
public final String label;
public final Long count;
public NodeStatsResult(String label, Long count) {
this.label = label;
this.count = count;
}
}
public static class RelationshipStatsResult {
public final String type;
public final Long count;
public RelationshipStatsResult(String type, Long count) {
this.type = type;
this.count = count;
}
}
}Enum defining the execution modes for procedures with different permission levels.
/**
* Execution modes for procedures
*/
public enum Mode {
/** Read-only operations that don't modify the database */
READ,
/** Operations that can modify the database data */
WRITE,
/** Operations that can modify the database schema */
SCHEMA,
/** Database management operations (system-level) */
DBMS
}Annotations for documenting procedure and function parameters.
/**
* Specify the name of a procedure/function parameter
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
/**
* Parameter name as it appears in Cypher
* @return Parameter name
*/
String value();
/**
* Default value for optional parameters
* @return Default value
*/
String defaultValue() default "";
}
/**
* Provide description for procedures and functions
*/
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Description {
/**
* Description text
* @return Description
*/
String value();
}import org.neo4j.procedure.*;
import org.neo4j.graphdb.*;
import java.util.concurrent.TimeUnit;
public class AdvancedProcedures {
@Context
public GraphDatabaseService db;
@Context
public Transaction tx;
@Context
public Log log;
@Procedure(name = "graph.batchCreate", mode = Mode.WRITE)
@Description("Create multiple nodes and relationships in batches")
public Stream<BatchResult> batchCreateNodes(
@Name("nodeData") List<Map<String, Object>> nodeData,
@Name(value = "batchSize", defaultValue = "1000") Long batchSize) {
int created = 0;
int processed = 0;
for (Map<String, Object> data : nodeData) {
String labelName = (String) data.get("label");
Map<String, Object> properties = (Map<String, Object>) data.get("properties");
Node node = tx.createNode(Label.label(labelName));
for (Map.Entry<String, Object> prop : properties.entrySet()) {
node.setProperty(prop.getKey(), prop.getValue());
}
created++;
processed++;
// Commit in batches to avoid memory issues
if (processed % batchSize == 0) {
tx.commit();
tx = db.beginTx();
}
}
return Stream.of(new BatchResult(created, processed));
}
@Procedure(name = "graph.shortestPath", mode = Mode.READ)
@Description("Find shortest path between two nodes")
public Stream<PathResult> findShortestPath(
@Name("startNodeId") Long startId,
@Name("endNodeId") Long endId,
@Name(value = "relationshipTypes", defaultValue = "") List<String> relTypes,
@Name(value = "maxDepth", defaultValue = "15") Long maxDepth) {
Node startNode = tx.getNodeById(startId);
Node endNode = tx.getNodeById(endId);
PathFinder<Path> finder = GraphAlgoFactory.shortestPath(
PathExpanders.forTypesAndDirections(
relTypes.stream()
.map(RelationshipType::withName)
.toArray(RelationshipType[]::new)
),
maxDepth.intValue()
);
Path path = finder.findSinglePath(startNode, endNode);
if (path != null) {
return Stream.of(new PathResult(path.length(),
StreamSupport.stream(path.nodes().spliterator(), false)
.map(Node::getId)
.collect(Collectors.toList())
));
}
return Stream.empty();
}
@UserFunction(name = "graph.degree")
@Description("Get the degree of a node")
public Long getNodeDegree(@Name("nodeId") Long nodeId) {
try {
Node node = tx.getNodeById(nodeId);
return (long) node.getDegree();
} catch (NotFoundException e) {
return null;
}
}
// Result classes
public static class BatchResult {
public final int nodesCreated;
public final int totalProcessed;
public BatchResult(int nodesCreated, int totalProcessed) {
this.nodesCreated = nodesCreated;
this.totalProcessed = totalProcessed;
}
}
public static class PathResult {
public final int length;
public final List<Long> nodeIds;
public PathResult(int length, List<Long> nodeIds) {
this.length = length;
this.nodeIds = nodeIds;
}
}
}
// Usage in Cypher:
// CALL graph.batchCreate([
// {label: "Person", properties: {name: "Alice", age: 30}},
// {label: "Person", properties: {name: "Bob", age: 25}}
// ], 500)
//
// CALL graph.shortestPath(123, 456, ["FRIENDS", "KNOWS"], 10)
//
// RETURN graph.degree(123) as nodeDegreeInstall with Tessl CLI
npx tessl i tessl/maven-org-neo4j--neo4j