Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AsOf semantics — bitemporal point

Every query reads facts at a specific bitemporal point. The AsOf type carries both axes:

#![allow(unused)]
fn main() {
pub enum AsOf {
    /// Default: VT = now, TT = now — "what is currently true, as currently believed."
    Now,
    /// "What was valid at VT = t, as currently believed?" Historical-fact view.
    AtVt(DateTime),
    /// "What was believed at TT = t about VT = now?" Audit view.
    AtTt(DateTime),
    /// Full bitemporal: "what was believed at TT = tt about VT = vt?"
    At { vt: DateTime, tt: DateTime },
}
}

The default AsOf::Now answers about current state; historical and audit queries are opt-in.

The data point and the rule valid-time

A read of derived facts names two valid-time points over one shared transaction time: the data point — which facts held — and the rule valid-time — which rules were in effect (see Rule validity over time). The transaction time is not split: it is a single viewpoint applied to facts and rule records alike.

By default the rule point tracks the data’s valid time. Reading facts valid at t evaluates them under the rules in effect at t — “as of that point, under the rules as they then stood.” This is the reconstruction default and needs no extra annotation. An explicit rule valid-time decouples the two for counterfactuals:

#![allow(unused)]
fn main() {
pub struct ReadPoint {
    pub data: AsOf,
    /// Rule valid-time. `None` ⇒ track the data point's valid time
    /// (the reconstruction default). `Some(t)` ⇒ evaluate under the
    /// rules in effect at `t` — e.g. past facts under today's rules.
    pub rule_vt: Option<DateTime>,
}
}

A bare read is the snapshot at now: facts valid now (VT_start ≤ now < VT_end), believed now (the current transaction-time watermark), under the rules in effect now. A derived read carries the same bitemporal read-point a base-fact extent does — it is not a separate mode. Reading the past pins the data point (AtVt/AtTt/At) and, by the default coupling above, the rule point with it. So future-dated and expired facts and rules do not appear in a bare read; they appear exactly at the points whose window contains them.

rule_vt = now over a past data point asks the counterfactual “what would the current rules derive over the facts as they then stood?”; tracking (the default) asks the faithful “what did we derive then, under the rules then in effect?”. The engine realizes a read-point by filtering the base facts (event_visible_at(vt, tt)) and selecting the rules in effect (rule_vt) before evaluation.

Now is frozen at the boundary. Valid-time now is the wall clock; transaction-time now is the published watermark. The serving boundary (ox runtime serve, the embeddable Connection, the ox driver) captures both once per request and threads a concrete point into the runtime — the runtime reads no clock. A recorded read-point therefore replays identically, which is what keeps incremental maintenance, checkpoint recovery, and the embedded↔standalone equivalence reproducible. A bare read’s snapshot is valid until the next validity boundary; the read-model cache rebuilds when wall-clock time crosses one, so a now read is never served a stale model.

Forks interact with AsOf through the copy-on-write invariant. A child fork inherits parent state up to fork_point_tt. A query against the child at AsOf::AtTt(tt): if tt < fork_point_tt it reads the parent’s events at tt; if tt ≥ fork_point_tt it reads the parent’s pre-fork events plus the child’s own events up to tt. The parent’s pre-fork state is shared with all children; each child sees its own divergence. A backend MAY materialize the inheritance at fork-creation or compute it lazily on read; both satisfy the contract.