Chapter 07 · Graph Data Modeling: Aggregates, Relationships, Hyperedges, Hierarchies, and Temporal Graphs
Temporal Validity, Event Histories, Versioned Relationships, Bitemporal Questions, and Snapshot Queries
Model time explicitly when AtlasMart must answer current, historical, corrected, and bitemporal questions without overwriting evidence.
Learning outcomes
AtlasMart must answer both “which supplier is valid for this
product today?” and “what did we believe the agreement was on a
prior audit date?” A single mutable
currentSupplierId cannot answer both. Temporal
modeling separates business validity from recording/history.
Distinguish valid time from transaction/recorded time in bitemporal questions.
Use native DATE/DATETIME values rather than opaque strings for temporal properties.
Model effective-dated facts with half-open intervals and snapshot predicates.
Preserve superseded history instead of overwriting current state when audit questions require it.
Detect interval overlaps and explain what application logic must enforce under concurrency.
The mandatory lab continues the accepted course baseline:
Neo4j Community 2026.07.1, database
neo4j, explicit CYPHER 25 for
version-sensitive examples, authentication enabled, no
mandatory APOC/GDS plugin, and stable AtlasMart domain
identifiers from Chapters 01–06. Neo4j
5.26.30 remains the LTS comparison line. Modeling
examples use only Community-compatible graph and uniqueness
features.
This generation environment does not run Neo4j or Docker.
Commands were checked against current official documentation
but were not executed here. Expected outputs are deterministic
fixture invariants, not fabricated captures. All
destructive/refactoring steps are scoped to Chapter 07
identifiers or labTag='ch07'; never replace them
with unconstrained production matches.
Re-establish the Chapter 07 modeling fixture
The lab deliberately creates a small supply-chain slice whose questions require explicit semantics: two suppliers, one product, one store, dated supply agreements, and a component hierarchy. Stable domain IDs remain the durable identity; internal element IDs are not used as business keys.
CYPHER 25CREATE CONSTRAINT supplier_id IF NOT EXISTS FOR (s:Supplier) REQUIRE s.supplierId IS UNIQUE;CREATE CONSTRAINT store_id IF NOT EXISTS FOR (s:Store) REQUIRE s.storeId IS UNIQUE;CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.productId IS UNIQUE;CREATE CONSTRAINT category_id IF NOT EXISTS FOR (c:Category) REQUIRE c.categoryId IS UNIQUE;CREATE CONSTRAINT agreement_id IF NOT EXISTS FOR (a:SupplyAgreement) REQUIRE a.agreementId IS UNIQUE;CREATE CONSTRAINT component_id IF NOT EXISTS FOR (c:Component) REQUIRE c.componentId IS UNIQUE;
CYPHER 25MERGE (sup1:Supplier {supplierId:'S-7001'}) SET sup1.name='Northstar Components', sup1.labTag='ch07'MERGE (sup2:Supplier {supplierId:'S-7002'}) SET sup2.name='BlueRiver Plastics', sup2.labTag='ch07'MERGE (store:Store {storeId:'ST-7001'}) SET store.name='AtlasMart Central', store.labTag='ch07'MERGE (prod:Product {productId:'P-7001'}) SET prod.name='Trail Camera Kit', prod.labTag='ch07'MERGE (cat:Category {categoryId:'CAT-7001'}) SET cat.name='Outdoor Imaging', cat.labTag='ch07'MERGE (prod)-[:IN_CATEGORY]->(cat)MERGE (a1:SupplyAgreement {agreementId:'SA-7001'})SET a1.validFrom=date('2026-01-01'), a1.validTo=date('2026-07-01'), a1.recordedFrom=datetime('2026-01-02T09:00:00Z'), a1.recordedTo=datetime('9999-12-31T00:00:00Z'), a1.unitPrice=84.0, a1.currency='USD', a1.labTag='ch07'MERGE (sup1)-[:PARTY_TO]->(a1)MERGE (a1)-[:SUPPLIES]->(prod)MERGE (a1)-[:DELIVERS_TO]->(store)MERGE (a2:SupplyAgreement {agreementId:'SA-7002'})SET a2.validFrom=date('2026-07-01'), a2.validTo=date('2027-01-01'), a2.recordedFrom=datetime('2026-06-15T10:00:00Z'), a2.recordedTo=datetime('9999-12-31T00:00:00Z'), a2.unitPrice=79.0, a2.currency='USD', a2.labTag='ch07'MERGE (sup2)-[:PARTY_TO]->(a2)MERGE (a2)-[:SUPPLIES]->(prod)MERGE (a2)-[:DELIVERS_TO]->(store);
CYPHER 25MERGE (kit:Component {componentId:'CMP-KIT'}) SET kit.name='Trail Camera Kit', kit.labTag='ch07'MERGE (cam:Component {componentId:'CMP-CAM'}) SET cam.name='Camera Module', cam.labTag='ch07'MERGE (case:Component {componentId:'CMP-CASE'}) SET case.name='Weather Case', case.labTag='ch07'MERGE (lens:Component {componentId:'CMP-LENS'}) SET lens.name='Lens Assembly', lens.labTag='ch07'MERGE (kit)-[:CONTAINS_COMPONENT {quantity:1}]->(cam)MERGE (kit)-[:CONTAINS_COMPONENT {quantity:1}]->(case)MERGE (cam)-[:CONTAINS_COMPONENT {quantity:1}]->(lens);
Use SHOW CONSTRAINTS and targeted counts before
refactoring. A successful query proves only the fixture state,
not that the model scales to production degree distributions or
workload volume.
1. Valid time and recorded time answer different questions
Valid time describes when a business fact applies in the modeled world. Recorded time describes when the database/system knew or asserted that version. Bitemporal modeling stores both dimensions when audit/reconstruction requires them. Not every application needs bitemporality; the cost is more rows/nodes/relationships, write logic and query predicates.
| Question | Time dimension |
|---|---|
| Who supplies P-7001 on 2026-05-01? | Valid time |
| What supplier record did the system contain on 2026-06-10? | Recorded time |
| What did we believe on 2026-06-10 about business date 2026-07-10? | Both valid and recorded time |
2. Use native temporal values and one interval convention
Cypher supports native DATE, local/zoned time and
datetime values, plus durations. The fixture uses half-open
intervals [validFrom, validTo): start inclusive,
end exclusive. Adjacent agreements can therefore meet at the
same date without overlapping.
CYPHER 25WITH date($asOf) AS tMATCH (s:Supplier)-[:PARTY_TO]->(a:SupplyAgreement)-[:SUPPLIES]->(p:Product {productId:'P-7001'})WHERE a.validFrom <= t AND t < a.validToRETURN s.supplierId, a.agreementId, a.unitPriceORDER BY a.agreementId;
3. Bitemporal snapshot uses both dimensions
CYPHER 25WITH date($businessAsOf) AS vt, datetime($recordedAsOf) AS rtMATCH (s:Supplier)-[:PARTY_TO]->(a:SupplyAgreement)-[:SUPPLIES]->(:Product {productId:'P-7001'})WHERE a.validFrom <= vt AND vt < a.validTo AND a.recordedFrom <= rt AND rt < a.recordedToRETURN s.supplierId, a.agreementId, a.unitPrice;
The fixture uses a far-future recordedTo sentinel
for an open recorded-time interval. A null-ended interval is
also possible, but then predicates must handle null explicitly.
Pick one convention and document it.
4. Deliberately wrong: overwrite current state only
CYPHER 25MATCH (p:Product {productId:'P-7001'})SET p.currentSupplierId='S-7002', p.currentUnitPrice=79.0;
This update answers only the latest state and destroys the
ability to reconstruct the previous supplier from the product
alone. The dated SupplyAgreement nodes preserve
each business fact version with explicit participants and
validity.
5. Overlap and correction are separate concerns
Two active intervals for the same supplier/product/store may be
invalid; alternatively, overlapping offers may be legitimate.
Define the invariant before enforcing it. A correction to what
was previously recorded should close the old
recordedTo and insert a new recorded version rather
than rewriting history if audit reconstruction matters.
CYPHER 25MATCH (a1:SupplyAgreement)-[:SUPPLIES]->(p:Product {productId:'P-7001'}), (a2:SupplyAgreement)-[:SUPPLIES]->(p)WHERE a1.agreementId < a2.agreementId AND a1.validFrom < a2.validTo AND a2.validFrom < a1.validToRETURN a1.agreementId,a2.agreementId;
Hands-on temporal lab
Run snapshots for 2026-05-01, 2026-07-01 and 2026-12-31; verify exactly one expected agreement in the fixture. Then create an overlapping scratch agreement, detect it, and delete only that scratch node. Finally run one bitemporal query with both parameters.
CYPHER 25MERGE (a:SupplyAgreement {agreementId:'SA-OVERLAP'})SET a.validFrom=date('2026-06-15'), a.validTo=date('2026-08-01'), a.recordedFrom=datetime('2026-06-16T00:00:00Z'), a.recordedTo=datetime('9999-12-31T00:00:00Z'), a.labTag='ch07';
Check your understanding
- What does valid time represent?
- What does recorded time represent?
- Why use [from,to) intervals?
- Why are native dates preferable to arbitrary date strings?
- Does an overlap query itself enforce concurrency-safe non-overlap?
Review the answers
1. When a fact applies in the business/domain world.
2. When the system knew or recorded a particular version.
3. Adjacent intervals can share a boundary without overlapping, and predicates stay consistent.
4. They preserve temporal typing, comparison and operations instead of relying on lexical conventions.
5. No; production writes need an invariant-control strategy if concurrent edits can race.
Summary and next step
Temporal correctness is part of the model. The final lesson reviews a deliberately poor AtlasMart graph and refactors it using the chapter's identity, cardinality, hierarchy and temporal rules.
Authoritative references
- Current Neo4j versions — Release/LTS snapshot used for the chapter baseline.
- Cypher Manual — patterns — Current property-graph pattern semantics.
- Temporal values — Native temporal value types that can be stored on nodes and relationships.
- Temporal values — Native DATE/TIME/DATETIME/DURATION semantics and storage.
- Temporal functions — Creation and manipulation of temporal instants.
- WHERE — Predicate semantics used by snapshot interval queries.