0
# Stored Procedures and Functions
1
2
Framework for creating custom Cypher-callable procedures and functions with dependency injection, supporting READ, WRITE, SCHEMA, and DBMS execution modes for extending Neo4j functionality.
3
4
## Capabilities
5
6
### Procedure Annotation
7
8
Annotation for declaring methods as Cypher-callable procedures with mode specification and metadata.
9
10
```java { .api }
11
/**
12
* Declares methods as Cypher-callable procedures
13
*/
14
@Target(ElementType.METHOD)
15
@Retention(RetentionPolicy.RUNTIME)
16
public @interface Procedure {
17
18
/**
19
* Name of the procedure in Cypher (defaults to method name)
20
* @return Procedure name
21
*/
22
String name() default "";
23
24
/**
25
* Execution mode for the procedure
26
* @return Procedure execution mode
27
*/
28
Mode mode() default Mode.READ;
29
30
/**
31
* Whether this procedure is deprecated
32
* @return true if deprecated
33
*/
34
boolean deprecated() default false;
35
36
/**
37
* Deprecation message if deprecated
38
* @return Deprecation message
39
*/
40
String deprecatedBy() default "";
41
}
42
```
43
44
**Usage Examples:**
45
46
```java
47
import org.neo4j.procedure.Procedure;
48
import org.neo4j.procedure.Mode;
49
import org.neo4j.procedure.Name;
50
import org.neo4j.procedure.Description;
51
import java.util.stream.Stream;
52
53
public class UserProcedures {
54
55
@Procedure(name = "user.create", mode = Mode.WRITE)
56
@Description("Create a new user with the given name and email")
57
public Stream<UserResult> createUser(
58
@Name("name") String name,
59
@Name("email") String email) {
60
61
// Create user node with transaction from context
62
Node userNode = tx.createNode(Label.label("User"));
63
userNode.setProperty("name", name);
64
userNode.setProperty("email", email);
65
userNode.setProperty("createdAt", Instant.now().toString());
66
67
return Stream.of(new UserResult(userNode.getId(), name, email));
68
}
69
70
@Procedure(name = "user.findByEmail", mode = Mode.READ)
71
@Description("Find a user by email address")
72
public Stream<UserResult> findUserByEmail(@Name("email") String email) {
73
74
ResourceIterable<Node> users = tx.findNodes(Label.label("User"), "email", email);
75
76
return StreamSupport.stream(users.spliterator(), false)
77
.map(node -> new UserResult(
78
node.getId(),
79
(String) node.getProperty("name"),
80
(String) node.getProperty("email")
81
));
82
}
83
84
@Procedure(name = "user.delete", mode = Mode.WRITE)
85
@Description("Delete a user and all their relationships")
86
public Stream<DeleteResult> deleteUser(@Name("userId") Long userId) {
87
88
Node user = tx.getNodeById(userId);
89
90
// Delete all relationships
91
int relationshipsDeleted = 0;
92
for (Relationship rel : user.getRelationships()) {
93
rel.delete();
94
relationshipsDeleted++;
95
}
96
97
// Delete the node
98
user.delete();
99
100
return Stream.of(new DeleteResult(userId, relationshipsDeleted));
101
}
102
103
// Result classes
104
public static class UserResult {
105
public final Long id;
106
public final String name;
107
public final String email;
108
109
public UserResult(Long id, String name, String email) {
110
this.id = id;
111
this.name = name;
112
this.email = email;
113
}
114
}
115
116
public static class DeleteResult {
117
public final Long userId;
118
public final int relationshipsDeleted;
119
120
public DeleteResult(Long userId, int relationshipsDeleted) {
121
this.userId = userId;
122
this.relationshipsDeleted = relationshipsDeleted;
123
}
124
}
125
}
126
```
127
128
### User Function Annotation
129
130
Annotation for declaring methods as user-defined functions callable from Cypher expressions.
131
132
```java { .api }
133
/**
134
* Declares methods as user-defined functions
135
*/
136
@Target(ElementType.METHOD)
137
@Retention(RetentionPolicy.RUNTIME)
138
public @interface UserFunction {
139
140
/**
141
* Name of the function in Cypher (defaults to method name)
142
* @return Function name
143
*/
144
String name() default "";
145
146
/**
147
* Whether this function is deprecated
148
* @return true if deprecated
149
*/
150
boolean deprecated() default false;
151
152
/**
153
* Deprecation message if deprecated
154
* @return Deprecation message
155
*/
156
String deprecatedBy() default "";
157
}
158
```
159
160
**Usage Examples:**
161
162
```java
163
import org.neo4j.procedure.UserFunction;
164
import java.time.LocalDate;
165
import java.time.Period;
166
import java.util.List;
167
import java.util.Map;
168
169
public class UtilityFunctions {
170
171
@UserFunction(name = "util.calculateAge")
172
@Description("Calculate age from birth date")
173
public Long calculateAge(@Name("birthDate") LocalDate birthDate) {
174
if (birthDate == null) return null;
175
return (long) Period.between(birthDate, LocalDate.now()).getYears();
176
}
177
178
@UserFunction(name = "util.formatName")
179
@Description("Format a name with proper capitalization")
180
public String formatName(@Name("name") String name) {
181
if (name == null || name.trim().isEmpty()) return null;
182
183
return Arrays.stream(name.trim().toLowerCase().split("\\s+"))
184
.map(word -> word.substring(0, 1).toUpperCase() + word.substring(1))
185
.collect(Collectors.joining(" "));
186
}
187
188
@UserFunction(name = "util.distance")
189
@Description("Calculate distance between two points")
190
public Double calculateDistance(
191
@Name("lat1") Double lat1, @Name("lon1") Double lon1,
192
@Name("lat2") Double lat2, @Name("lon2") Double lon2) {
193
194
if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) {
195
return null;
196
}
197
198
// Haversine distance calculation
199
double R = 6371; // Earth's radius in kilometers
200
double dLat = Math.toRadians(lat2 - lat1);
201
double dLon = Math.toRadians(lon2 - lon1);
202
203
double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
204
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
205
Math.sin(dLon/2) * Math.sin(dLon/2);
206
207
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
208
return R * c;
209
}
210
211
@UserFunction(name = "util.jsonExtract")
212
@Description("Extract value from JSON string by key path")
213
public Object extractFromJson(@Name("json") String json, @Name("path") String path) {
214
// Implementation would parse JSON and extract value by path
215
// This is a simplified example
216
return parseJsonPath(json, path);
217
}
218
}
219
220
// Usage in Cypher:
221
// MATCH (p:Person)
222
// RETURN p.name, util.calculateAge(p.birthDate) as age
223
//
224
// MATCH (p1:Person), (p2:Person)
225
// WHERE util.distance(p1.lat, p1.lon, p2.lat, p2.lon) < 10
226
// RETURN p1.name, p2.name
227
```
228
229
### Context Annotation
230
231
Annotation for injecting Neo4j resources into procedure and function classes.
232
233
```java { .api }
234
/**
235
* Inject Neo4j resources into procedure classes
236
*/
237
@Target(ElementType.FIELD)
238
@Retention(RetentionPolicy.RUNTIME)
239
public @interface Context {
240
}
241
```
242
243
**Usage Examples:**
244
245
```java
246
import org.neo4j.procedure.Context;
247
import org.neo4j.graphdb.GraphDatabaseService;
248
import org.neo4j.graphdb.Transaction;
249
import org.neo4j.logging.Log;
250
251
public class DataAnalysisProcedures {
252
253
@Context
254
public GraphDatabaseService db;
255
256
@Context
257
public Transaction tx;
258
259
@Context
260
public Log log;
261
262
@Procedure(name = "analysis.nodeStats", mode = Mode.READ)
263
@Description("Get statistics about nodes in the database")
264
public Stream<NodeStatsResult> getNodeStatistics() {
265
266
log.info("Starting node statistics analysis");
267
268
Map<String, Long> labelCounts = new HashMap<>();
269
270
// Count nodes by label
271
for (Label label : GlobalGraphOperations.at(db).getAllLabels()) {
272
long count = 0;
273
try (ResourceIterable<Node> nodes = tx.findNodes(label)) {
274
for (Node node : nodes) {
275
count++;
276
}
277
}
278
labelCounts.put(label.name(), count);
279
log.debug("Label " + label.name() + ": " + count + " nodes");
280
}
281
282
return labelCounts.entrySet().stream()
283
.map(entry -> new NodeStatsResult(entry.getKey(), entry.getValue()));
284
}
285
286
@Procedure(name = "analysis.relationshipStats", mode = Mode.READ)
287
@Description("Get statistics about relationships in the database")
288
public Stream<RelationshipStatsResult> getRelationshipStatistics() {
289
290
Map<String, Long> typeCounts = new HashMap<>();
291
292
// Count relationships by type
293
for (RelationshipType type : GlobalGraphOperations.at(db).getAllRelationshipTypes()) {
294
long count = 0;
295
for (Relationship rel : GlobalGraphOperations.at(db).getAllRelationships()) {
296
if (rel.isType(type)) {
297
count++;
298
}
299
}
300
typeCounts.put(type.name(), count);
301
}
302
303
return typeCounts.entrySet().stream()
304
.map(entry -> new RelationshipStatsResult(entry.getKey(), entry.getValue()));
305
}
306
307
public static class NodeStatsResult {
308
public final String label;
309
public final Long count;
310
311
public NodeStatsResult(String label, Long count) {
312
this.label = label;
313
this.count = count;
314
}
315
}
316
317
public static class RelationshipStatsResult {
318
public final String type;
319
public final Long count;
320
321
public RelationshipStatsResult(String type, Long count) {
322
this.type = type;
323
this.count = count;
324
}
325
}
326
}
327
```
328
329
### Execution Mode Enum
330
331
Enum defining the execution modes for procedures with different permission levels.
332
333
```java { .api }
334
/**
335
* Execution modes for procedures
336
*/
337
public enum Mode {
338
/** Read-only operations that don't modify the database */
339
READ,
340
341
/** Operations that can modify the database data */
342
WRITE,
343
344
/** Operations that can modify the database schema */
345
SCHEMA,
346
347
/** Database management operations (system-level) */
348
DBMS
349
}
350
```
351
352
### Parameter Annotations
353
354
Annotations for documenting procedure and function parameters.
355
356
```java { .api }
357
/**
358
* Specify the name of a procedure/function parameter
359
*/
360
@Target(ElementType.PARAMETER)
361
@Retention(RetentionPolicy.RUNTIME)
362
public @interface Name {
363
/**
364
* Parameter name as it appears in Cypher
365
* @return Parameter name
366
*/
367
String value();
368
369
/**
370
* Default value for optional parameters
371
* @return Default value
372
*/
373
String defaultValue() default "";
374
}
375
376
/**
377
* Provide description for procedures and functions
378
*/
379
@Target({ElementType.METHOD, ElementType.PARAMETER})
380
@Retention(RetentionPolicy.RUNTIME)
381
public @interface Description {
382
/**
383
* Description text
384
* @return Description
385
*/
386
String value();
387
}
388
```
389
390
### Advanced Procedure Examples
391
392
```java
393
import org.neo4j.procedure.*;
394
import org.neo4j.graphdb.*;
395
import java.util.concurrent.TimeUnit;
396
397
public class AdvancedProcedures {
398
399
@Context
400
public GraphDatabaseService db;
401
402
@Context
403
public Transaction tx;
404
405
@Context
406
public Log log;
407
408
@Procedure(name = "graph.batchCreate", mode = Mode.WRITE)
409
@Description("Create multiple nodes and relationships in batches")
410
public Stream<BatchResult> batchCreateNodes(
411
@Name("nodeData") List<Map<String, Object>> nodeData,
412
@Name(value = "batchSize", defaultValue = "1000") Long batchSize) {
413
414
int created = 0;
415
int processed = 0;
416
417
for (Map<String, Object> data : nodeData) {
418
String labelName = (String) data.get("label");
419
Map<String, Object> properties = (Map<String, Object>) data.get("properties");
420
421
Node node = tx.createNode(Label.label(labelName));
422
for (Map.Entry<String, Object> prop : properties.entrySet()) {
423
node.setProperty(prop.getKey(), prop.getValue());
424
}
425
426
created++;
427
processed++;
428
429
// Commit in batches to avoid memory issues
430
if (processed % batchSize == 0) {
431
tx.commit();
432
tx = db.beginTx();
433
}
434
}
435
436
return Stream.of(new BatchResult(created, processed));
437
}
438
439
@Procedure(name = "graph.shortestPath", mode = Mode.READ)
440
@Description("Find shortest path between two nodes")
441
public Stream<PathResult> findShortestPath(
442
@Name("startNodeId") Long startId,
443
@Name("endNodeId") Long endId,
444
@Name(value = "relationshipTypes", defaultValue = "") List<String> relTypes,
445
@Name(value = "maxDepth", defaultValue = "15") Long maxDepth) {
446
447
Node startNode = tx.getNodeById(startId);
448
Node endNode = tx.getNodeById(endId);
449
450
PathFinder<Path> finder = GraphAlgoFactory.shortestPath(
451
PathExpanders.forTypesAndDirections(
452
relTypes.stream()
453
.map(RelationshipType::withName)
454
.toArray(RelationshipType[]::new)
455
),
456
maxDepth.intValue()
457
);
458
459
Path path = finder.findSinglePath(startNode, endNode);
460
461
if (path != null) {
462
return Stream.of(new PathResult(path.length(),
463
StreamSupport.stream(path.nodes().spliterator(), false)
464
.map(Node::getId)
465
.collect(Collectors.toList())
466
));
467
}
468
469
return Stream.empty();
470
}
471
472
@UserFunction(name = "graph.degree")
473
@Description("Get the degree of a node")
474
public Long getNodeDegree(@Name("nodeId") Long nodeId) {
475
try {
476
Node node = tx.getNodeById(nodeId);
477
return (long) node.getDegree();
478
} catch (NotFoundException e) {
479
return null;
480
}
481
}
482
483
// Result classes
484
public static class BatchResult {
485
public final int nodesCreated;
486
public final int totalProcessed;
487
488
public BatchResult(int nodesCreated, int totalProcessed) {
489
this.nodesCreated = nodesCreated;
490
this.totalProcessed = totalProcessed;
491
}
492
}
493
494
public static class PathResult {
495
public final int length;
496
public final List<Long> nodeIds;
497
498
public PathResult(int length, List<Long> nodeIds) {
499
this.length = length;
500
this.nodeIds = nodeIds;
501
}
502
}
503
}
504
505
// Usage in Cypher:
506
// CALL graph.batchCreate([
507
// {label: "Person", properties: {name: "Alice", age: 30}},
508
// {label: "Person", properties: {name: "Bob", age: 25}}
509
// ], 500)
510
//
511
// CALL graph.shortestPath(123, 456, ["FRIENDS", "KNOWS"], 10)
512
//
513
// RETURN graph.degree(123) as nodeDegree
514
```