Packages
reckon_db
2.3.2
5.11.0
5.10.4
5.10.3
5.10.1
5.10.0
5.9.1
5.9.0
5.8.3
5.8.2
5.8.1
5.8.0
5.7.0
5.6.1
5.6.0
5.5.5
5.5.4
5.5.3
5.5.2
5.5.1
5.5.0
5.4.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.0.0
3.1.2
3.1.1
3.0.0
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.2
2.2.0
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
BEAM-native Event Store built on Khepri/Ra with Raft consensus. Event sourcing, persistent subscriptions, snapshots, and automatic cluster formation via UDP multicast discovery. Ships embedded Rust NIFs for 3-15x acceleration of crypto, hashing, compression, aggregation, filter matching, and grap...
Current section
Files
Jump to
Current section
Files
guides/temporal_queries.md
# Temporal Queries
Temporal queries enable point-in-time reconstruction of aggregate state and time-range analytics. This guide covers the server-side implementation, storage mechanics, and cluster behavior.
## Overview
The `reckon_db_temporal` module provides three core operations:
| Function | Purpose |
|----------|---------|
| `read_until/3,4` | Read events up to a timestamp |
| `read_range/4,5` | Read events within a time range |
| `version_at/3` | Get stream version at a timestamp |
## Architecture

## How Temporal Filtering Works
### Timestamp Storage
Every event stored in reckon-db includes an `epoch_us` field - microseconds since Unix epoch:
```erlang
#event{
event_id = <<"evt-123">>,
stream_id = <<"orders-456">>,
version = 5,
epoch_us = 1735689600000000, %% Jan 1, 2025 00:00:00 UTC
event_type = <<"OrderPlaced">>,
data = #{...},
metadata = #{...}
}
```
This timestamp is set at append time using `erlang:system_time(microsecond)`.
### Query Execution
Temporal queries follow this execution path:
1. **Stream Existence Check**: Verify stream exists in Khepri
2. **Event Retrieval**: Read all events from the stream
3. **Timestamp Filtering**: Filter events by `epoch_us` field
4. **Options Application**: Apply direction and limit options
```erlang
%% Internal filtering for read_until
filter_events_until(Events, Timestamp) ->
[E || E <- Events, E#event.epoch_us =< Timestamp].
%% Internal filtering for read_range
filter_events_range(Events, FromTimestamp, ToTimestamp) ->
[E || E <- Events,
E#event.epoch_us >= FromTimestamp,
E#event.epoch_us =< ToTimestamp].
```
## Khepri Storage Path
Events are stored at:
```
[streams, StreamId, PaddedVersion] -> #event{}
```
The 12-character zero-padded version enables lexicographic ordering:
```
[streams, <<"orders-123">>, <<"000000000000">>] -> Event v0
[streams, <<"orders-123">>, <<"000000000001">>] -> Event v1
[streams, <<"orders-123">>, <<"000000000002">>] -> Event v2
```
## Cluster Behavior
### Consistency Guarantees
Temporal queries inherit Khepri/Ra's consistency model:
| Aspect | Behavior |
|--------|----------|
| **Read Consistency** | Strongly consistent (reads go through Raft leader) |
| **Cross-Node** | Same results on any cluster node |
| **Partition Tolerance** | Queries fail if no quorum available |
### Leader Routing
All reads are routed to the Ra leader:
```
Client Request
│
▼
┌─────────────┐
│ Any Node │
│ (Follower) │
└──────┬──────┘
│ Forward to leader
▼
┌─────────────┐
│ Ra Leader │
│ (Reads) │
└──────┬──────┘
│ Raft log consistency
▼
Query Result
```
## Performance Considerations
### Current Implementation
The current implementation reads all events, then filters in memory:
```erlang
read_all_events(StoreId, StreamId) ->
Version = reckon_db_streams:get_version(StoreId, StreamId),
case Version of
?NO_STREAM -> {ok, []};
_ -> reckon_db_streams:read(StoreId, StreamId, 0, Version + 1, forward)
end.
```
**Implications:**
- O(n) memory usage where n = total events in stream
- Suitable for streams with < 10,000 events
- For larger streams, consider snapshots + temporal queries
### Optimization Opportunities
Future versions could add:
1. **Khepri Timestamp Index**: Secondary index on `epoch_us`
2. **Binary Search**: If events are sorted by timestamp (they are by version, usually correlated)
3. **Streaming API**: Process events in batches to reduce memory
## Use Cases
### 1. Point-in-Time State Reconstruction
```erlang
%% Reconstruct order state as it was on Dec 31, 2024
Timestamp = 1735603200000000, %% Dec 31, 2024 00:00:00 UTC
{ok, Events} = reckon_db_temporal:read_until(my_store, <<"order-123">>, Timestamp),
State = lists:foldl(fun apply_event/2, initial_state(), Events).
```
### 2. Audit Queries
```erlang
%% What was the account balance at end of fiscal year?
FiscalYearEnd = 1735689599999999, %% Dec 31, 2024 23:59:59.999999 UTC
{ok, Events} = reckon_db_temporal:read_until(my_store, <<"account-456">>, FiscalYearEnd),
Balance = calculate_balance(Events).
```
### 3. Time-Range Analytics
```erlang
%% Analyze orders from Q4 2024
Q4Start = 1727740800000000, %% Oct 1, 2024
Q4End = 1735689599999999, %% Dec 31, 2024
{ok, Events} = reckon_db_temporal:read_range(my_store, <<"orders-*">>, Q4Start, Q4End),
analyze_quarterly_orders(Events).
```
### 4. Version Discovery
```erlang
%% What version should I replay to for a snapshot at timestamp T?
{ok, Version} = reckon_db_temporal:version_at(my_store, <<"user-789">>, Timestamp),
%% Now load snapshot at Version, or replay events 0..Version
```
## Options
Both `read_until/4` and `read_range/5` accept options:
```erlang
-type opts() :: #{
direction => forward | backward, %% Event order (default: forward)
limit => pos_integer() %% Max events to return
}.
```
**Direction**: Controls the order of returned events
- `forward`: Oldest first (ascending by version)
- `backward`: Newest first (descending by version)
**Limit**: Truncates result after applying direction
## Telemetry
Temporal queries emit telemetry events:
```erlang
%% Event: [reckon_db, temporal, read_until]
%% Measurements: #{duration => integer(), event_count => integer()}
%% Metadata: #{store_id => atom(), stream_id => binary(), timestamp => integer()}
%% Event: [reckon_db, temporal, read_range]
%% Measurements: #{duration => integer(), event_count => integer()}
%% Metadata: #{store_id => atom(), stream_id => binary(), timestamp => {From, To}}
```
## Error Handling
| Error | Cause | Resolution |
|-------|-------|------------|
| `{error, {stream_not_found, StreamId}}` | Stream does not exist | Verify stream ID |
| `{error, timeout}` | Khepri/Ra timeout | Check cluster health |
| `{error, no_quorum}` | Ra cluster partitioned | Wait for partition heal |
## Best Practices
1. **Use Snapshots**: For frequently queried historical points, save snapshots
2. **Limit Result Size**: Use the `limit` option for large time ranges
3. **Monitor Duration**: Track telemetry for slow queries
4. **Consider Stream Size**: Temporal queries load full streams; partition large streams
## See Also
- [Snapshots](snapshots.md) - State caching for performance
- [Scavenging](scavenging.md) - Event lifecycle management
- [Storage Internals](storage_internals.md) - Khepri path structure