Skip to content

DIGITAL-FABRIC-AI/neo4j-temporal-graph

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

neo4j-temporal-graph

Reference implementation for NEP-001: Native Bitemporal Graph Support.

Cypher procedures and functions that implement AS OF query semantics for Neo4j — the ability to query the graph as it existed at any point in time, using relationship-level valid time properties.


The Problem

Neo4j is excellent at modelling things that change over time — ownership transfers, regulatory reassignments, equipment moves, contract changes. But querying the graph as it existed at a specific point in time requires verbose, error-prone boilerplate:

-- Current approach: manual temporal filter on every relationship
MATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company)
WHERE (r.effective_date IS NULL OR r.effective_date <= $asOf)
  AND (r.end_date IS NULL OR r.end_date >= $asOf)
RETURN c.name

At multi-hop depth this becomes unmanageable:

-- Three-hop traversal: temporal filter duplicated for every relationship
MATCH (w:Well)-[r1:OWNED_BY]->(c:Company)-[r2:SUBJECT_TO]->(a:Authority)
WHERE (r1.effective_date IS NULL OR r1.effective_date <= $asOf)
  AND (r1.end_date IS NULL OR r1.end_date >= $asOf)
  AND (r2.effective_date IS NULL OR r2.effective_date <= $asOf)
  AND (r2.end_date IS NULL OR r2.end_date >= $asOf)
RETURN w.name, c.name, a.name

This is the boilerplate that native AS OF support should eliminate. This library demonstrates what that looks like with today's Neo4j.


What This Library Provides

Three building blocks that implement AS OF semantics:

1. temporal.activeAt(relationship, date) — inline function

Drop-in replacement for the manual IS NULL OR filter in WHERE clauses:

-- Before:
MATCH (w:Well)-[r:OWNED_BY]->(c:Company)
WHERE (r.effective_date IS NULL OR r.effective_date <= '2024-06-15')
  AND (r.end_date IS NULL OR r.end_date >= '2024-06-15')

-- After:
MATCH (w:Well)-[r:OWNED_BY]->(c:Company)
WHERE temporal.activeAt(r, '2024-06-15')

2. temporal.asOf.relationships(node, types, date) — procedure

Returns all relationships of specified type(s) that were active at a given date:

MATCH (w:Well {wellId: $id})
CALL temporal.asOf.relationships(w, 'OWNED_BY', $asOf)
YIELD rel, other, effectiveDate, endDate
RETURN other.name AS owner, effectiveDate AS since

3. temporal.asOf.traverse(node, types, date, depth) — procedure

Traverses the graph following only relationships active at a given date:

-- Multi-hop AS OF traversaltemporal consistency across all hops
MATCH (w:Well {wellId: $id})
CALL temporal.asOf.traverse(w, 'OWNED_BY,SUBJECT_TO', $asOf, 3)
YIELD nodes, relationships, length
RETURN [n IN nodes | n.name] AS path

Full API Reference

Functions

Function Signature Returns Description
temporal.activeAt (rel, asOf) Boolean True if relationship was active at the date
temporal.period (rel) String Formats the valid period: "2020-01-15 → present"
temporal.overlaps (rel, from, to) Boolean True if relationship was active at any point in the range

Procedures

Procedure Description
temporal.asOf.relationships(node, types, asOf) Relationships of given type(s) active at the date
temporal.asOf.traverse(node, types, asOf, depth) Traverse via active relationships only
temporal.changepoints(node, types) All START/END events ordered chronologically

Temporal Property Convention

The library reads two relationship properties by convention:

Property Meaning Null behaviour
effective_date Date this relationship became valid Null = "from the beginning of time"
end_date Date this relationship ceased to be valid Null = "still active / open-ended"

Values may be stored as Neo4j DATE, DATETIME, or ISO-8601 strings. All three are handled transparently.

Relationships with no temporal properties are always considered active. This ensures backward compatibility — existing graphs without temporal annotations are fully supported.


Query Examples

"Who owned this well on June 15, 2024?"

MATCH (w:Well {wellId: $id})
CALL temporal.asOf.relationships(w, 'OWNED_BY', '2024-06-15')
YIELD other, effectiveDate
RETURN other.name AS owner, effectiveDate AS since

"What was the full ownership history of this well?"

MATCH (w:Well {wellId: $id})
CALL temporal.changepoints(w, 'OWNED_BY')
YIELD changeDate, changeType, other
RETURN changeDate, changeType, other.name AS company
ORDER BY changeDate

"Which companies owned this well at any point during 2023?"

MATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company)
WHERE temporal.overlaps(r, '2023-01-01', '2023-12-31')
RETURN c.name, temporal.period(r) AS period

"Trace this well's full regulatory chain as of Q4 2022"

MATCH (w:Well {wellId: $id})
CALL temporal.asOf.traverse(w, 'OWNED_BY,SUBJECT_TO_JURISDICTION,GRANTED_BY', '2022-10-01', 4)
YIELD nodes, length
RETURN [n IN nodes | coalesce(n.name, n.wellId, n.facilityId)] AS chain, length
ORDER BY length

"Show me the valid period of every ownership relationship for this well"

MATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company)
RETURN c.name AS company, temporal.period(r) AS validPeriod
ORDER BY r.effective_date

Installation

cp target/neo4j-temporal-graph-1.0.0.jar $NEO4J_HOME/plugins/
# Restart Neo4j

Verify:

MATCH (n)-[r]->(m) WHERE temporal.activeAt(r, date()) RETURN count(r) AS activeNow

Building from Source

Requirements: JDK 21+, Maven 3.9+

git clone https://github.com/DIGITAL-FABRIC-AI/neo4j-temporal-graph.git
cd neo4j-temporal-graph
mvn clean package
# → target/neo4j-temporal-graph-1.0.0.jar

Without a local JDK:

./build.sh   # Docker-based build

Testing

mvn clean test

The test suite uses a well ownership graph with three sequential ownership periods:

Well ──[OWNED_BY 2018-01-01 → 2020-06-30]──► Alpha Corp
Well ──[OWNED_BY 2020-07-01 → 2022-12-31]──► Beta Inc
Well ──[OWNED_BY 2023-01-01 → (present) ]──► Gamma Ltd
Well ──[SUBJECT_TO (no dates)            ]──► Alberta (Province)

Tests cover: point-in-time ownership, open-ended relationships, boundary dates, relationships with no temporal properties, multi-type traversal, change history, period formatting, and range overlap.


The Bigger Picture: NEP-001

This library is the reference implementation for NEP-001: Native Bitemporal Graph Support — a proposal to add AS OF to Cypher as a first-class language construct:

-- Proposed native syntax (NEP-001):
MATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company) AS OF $asOf
RETURN c.name

-- Equivalent using this library today:
MATCH (w:Well {wellId: $id})
CALL temporal.asOf.relationships(w, 'OWNED_BY', $asOf)
YIELD other
RETURN other.name

The proposal covers:

  • AS OF Cypher syntax for single and multi-hop patterns
  • Valid time (user-defined) and transaction time (system-managed) dimensions
  • Temporal index support for period overlap queries
  • SQL:2011 alignment

See NEP-001.md for the full proposal.


Relationship to Production Use

This library was developed and validated against a production Neo4j graph (2026.01.4 Community Edition) with:

  • 3,143,576 SUBJECT_TO_JURISDICTION relationships (entities → administrative areas)
  • 943,763 GRANTED_BY relationships (licences → regulatory authorities)
  • 94.6% of jurisdiction relationships with temporal annotations
  • ~94,000 relationships with end_date (representing licence abandonments)

Every temporal query in that system previously required the manual IS NULL OR filter. This library eliminates that boilerplate and demonstrates the semantics are correct on real data.


Compatibility

Component Version
Neo4j 2026.01.x (Community and Enterprise)
Java 21+

License

Apache License 2.0 — see LICENSE

Author

Ashley Dunfield github.com/DIGITAL-FABRIC-AI

About

Reference implementation for NEP-001: native bitemporal graph support for Neo4j. AS OF query semantics via temporal.activeAt, temporal.asOf.relationships, and temporal.asOf.traverse.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors