or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mddatabase-management.mdevents.mdgraph-database.mdgraph-model.mdindex.mdprocedures.mdquery-execution.mdschema.mdspatial.mdtraversal.md

procedures.mddocs/

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

```