Temporal Properties & Time-Travel¶
Feature flag required for versioned properties
Temporal types (Date, Time, Duration, etc.) are always available. Temporal properties (versioned history, time-travel queries) require the temporal feature flag, which is not included in any named profile (embedded, browser, server, full). Enable it explicitly:
Or combine it with a profile: features = ["embedded", "temporal"].
Grafeo supports two complementary temporal features:
- Temporal types: Date, Time, Duration and zoned variants for storing temporal data. These are always available with no feature flags required.
- Temporal properties: opt-in versioned property and label history with time-travel queries. This requires the
temporalfeature flag (available since 0.5.24).
Temporal Types¶
Temporal types are first-class values that can be stored as properties, used in expressions and returned from queries. They are available in all query languages and all bindings.
Supported Types¶
| Type | Example | Description |
|---|---|---|
DATE | 2024-01-15 | Calendar date (year, month, day) |
TIME | 14:30:00 | Local time (hour, minute, second) |
DATETIME | 2024-01-15T14:30:00 | Local date and time |
DURATION | P1Y2M3D | ISO 8601 duration |
ZONED DATETIME | 2024-01-15T14:30:00+01:00 | Datetime with UTC offset |
ZONED TIME | 14:30:00+01:00 | Time with UTC offset |
Typed Literals¶
Inline temporal values use the typed literal syntax:
RETURN DATE '2024-01-15'
RETURN TIME '14:30:00'
RETURN DATETIME '2024-01-15T14:30:00Z'
RETURN DURATION 'P1Y2M3D'
RETURN ZONED DATETIME '2024-01-15T14:30:00+01:00'
RETURN ZONED TIME '14:30:00+01:00'
String Constructors¶
Parse temporal values from ISO 8601 strings:
RETURN date('2024-01-15')
RETURN time('14:30:00')
RETURN datetime('2024-01-15T14:30:00')
RETURN duration('P1Y2M')
Map Constructors¶
Build temporal values from named components:
RETURN date({year: 2024, month: 3})
RETURN time({hour: 14, minute: 30, second: 45})
RETURN datetime({year: 2024, month: 3, day: 15, hour: 14, minute: 30})
RETURN duration({years: 1, months: 2, days: 3})
Omitted components default to their zero value (1 for month/day, 0 for time components).
Arithmetic¶
Durations can be added to or subtracted from dates and datetimes:
-- 30 days after a date
RETURN DATE '2024-01-15' + DURATION 'P30D'
-- Result: 2024-02-14
-- Find events in the last 7 days
MATCH (e:Event)
WHERE e.created_at > now() - DURATION 'P7D'
RETURN e.title, e.created_at
Component Extraction¶
Extract individual components from temporal values:
WITH DATE '2024-06-15' AS d
RETURN year(d), month(d), day(d)
-- 2024, 6, 15
WITH TIME '14:30:45' AS t
RETURN hour(t), minute(t), second(t)
-- 14, 30, 45
For the full list of temporal functions, see the Temporal Functions reference.
Temporal Properties¶
Requires the temporal feature flag. Available since 0.5.24.
When enabled, Grafeo records an append-only version history for every property and label change. Each mutation creates a new version tagged with the current epoch, so you can later reconstruct the exact state of any node or edge at any point in time.
How It Works¶
- Every committed transaction advances the database epoch
- When a property is set or a label is added/removed, the change is recorded in an append-only
VersionLog - The version log stores the epoch and the new value for each change
- Rollbacks undo version creation, so aborted transactions leave no trace
Example: Tracking Property Changes¶
from grafeo import GrafeoDB
db = GrafeoDB()
# Create a person (epoch advances on commit)
with db.begin_transaction() as tx:
tx.execute("INSERT (:Person {name: 'Alix', city: 'Amsterdam'})")
tx.commit()
epoch_v1 = db.current_epoch()
# Update city
with db.begin_transaction() as tx:
tx.execute("MATCH (p:Person {name: 'Alix'}) SET p.city = 'Berlin'")
tx.commit()
epoch_v2 = db.current_epoch()
# Update again
with db.begin_transaction() as tx:
tx.execute("MATCH (p:Person {name: 'Alix'}) SET p.city = 'Paris'")
tx.commit()
# Current state: Paris
result = db.execute("MATCH (p:Person {name: 'Alix'}) RETURN p.city")
# => 'Paris'
# Historical state at epoch_v1: Amsterdam
result = db.execute_at_epoch(
"MATCH (p:Person {name: 'Alix'}) RETURN p.city",
epoch=epoch_v1,
)
# => 'Amsterdam'
Snapshot Persistence¶
Temporal version history survives snapshot save/load cycles. Snapshot format v4 (0.5.24+) serializes the full VersionLog for each property and label, so restoring a snapshot preserves the complete history.
Time-Travel Queries¶
Available since 0.5.13.
Time-travel lets you run any query against a historical database snapshot. The results reflect the state of nodes, edges, properties and labels as of the specified epoch.
execute_at_epoch¶
The simplest way to query a past state: pass an epoch number alongside the query.
Session-Level Viewing Epoch¶
For multiple queries against the same historical snapshot, set a viewing epoch on the session. All subsequent queries will see the database as of that epoch until the override is cleared.
Version History APIs¶
These APIs return the full version history of a node or edge, including creation and deletion epochs.
get_node_at_epoch / get_edge_at_epoch¶
Retrieve a single node or edge as it existed at a specific epoch. Returns None if the entity did not exist at that epoch.
if let Some(node) = db.get_node_at_epoch(node_id, EpochId::new(3)) {
println!("Labels: {:?}", node.labels);
println!("Properties: {:?}", node.properties);
}
if let Some(edge) = db.get_edge_at_epoch(edge_id, EpochId::new(3)) {
println!("Type: {:?}", edge.edge_type);
println!("Properties: {:?}", edge.properties);
}
get_node_history / get_edge_history¶
Return every version of a node or edge as a list of (created_epoch, deleted_epoch, entity) tuples. The deleted_epoch is None if the entity still exists.
Practical Example: Audit Trail¶
Combine temporal properties with history APIs to build an audit trail:
from grafeo import GrafeoDB
db = GrafeoDB()
# Create and update a person over several transactions
with db.begin_transaction() as tx:
tx.execute("INSERT (:Person {name: 'Gus', role: 'engineer'})")
tx.commit()
with db.begin_transaction() as tx:
tx.execute("MATCH (p:Person {name: 'Gus'}) SET p.role = 'senior engineer'")
tx.commit()
with db.begin_transaction() as tx:
tx.execute("MATCH (p:Person {name: 'Gus'}) SET p.role = 'staff engineer'")
tx.commit()
# Retrieve Gus's node ID
result = db.execute("MATCH (p:Person {name: 'Gus'}) RETURN id(p) AS nid")
gus_id = result[0]['nid']
# Walk through the full history
history = db.get_node_history(gus_id)
for created, deleted, node in history:
print(f"Epoch {created}: role = {node.properties.get('role')}")
# Epoch 1: role = engineer
# Epoch 2: role = senior engineer
# Epoch 3: role = staff engineer
Performance Considerations¶
Temporal versioning adds overhead to write operations because each mutation appends to the VersionLog. Read performance is optimized with several strategies:
| Scenario | Cost | Details |
|---|---|---|
| Current-epoch reads | O(1) | Fast path: the latest entry is checked first, skipping the binary search |
| Historical reads | O(log N) | Binary search over the version log for the target epoch |
| Write overhead | Append-only | Each property/label change appends one entry to the version log |
Overhead Benchmarks¶
| Version | Read overhead (vs. non-temporal) |
|---|---|
| 0.5.24 | ~16% |
| 0.5.25 | ~6% (optimized VersionLog::at() fast path, eliminated double HashMap lookup) |
Tips¶
- Enable only when needed: the
temporalfeature is opt-in. If you do not need property versioning, leave it disabled for zero overhead. - Epoch advances on commit: epochs only increment when explicit transactions commit. Auto-commit queries each produce one epoch.
- Snapshots preserve history: saving and loading snapshots retains the full version log, so you can time-travel across restarts.