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.

Advanced135–165 minutesTemporal/bitemporal snapshot labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

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.

01

Distinguish valid time from transaction/recorded time in bitemporal questions.

02

Use native DATE/DATETIME values rather than opaque strings for temporal properties.

03

Model effective-dated facts with half-open intervals and snapshot predicates.

04

Preserve superseded history instead of overwriting current state when audit questions require it.

05

Detect interval overlaps and explain what application logic must enforce under concurrency.

Chapter 07 baseline · reviewed 9 September 2026

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.

Evidence and safety note

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 · Community-compatible identity constraints
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 · deterministic supply-agreement fixture
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 · deterministic bill-of-materials fixture
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 · valid-time snapshot
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 · what the system knew at recordedAsOf about businessAsOf
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

Wrong when audit/history questions are mandatory
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 · overlap audit using half-open intervals
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 · temporary overlap failure injection
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

  1. What does valid time represent?
  2. What does recorded time represent?
  3. Why use [from,to) intervals?
  4. Why are native dates preferable to arbitrary date strings?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.