Time-travel: as_of reads the past
Area: Bitemporal time Teaches: the surface
as_ofclause on apub query— the same query, snapshotted at different transaction-times, returns different answers. Every mutation stamps a monotonic transaction-time, andas_of <tx>reads the state visible at that instant. Prerequisites: refinement (iff), andmutatebodies. Run:ox build examples/temporal_as_of_surface && ox run-scenario examples/temporal_as_of_surface
A database that only knows “now” cannot answer “what did we believe last Tuesday”. Argon’s substrate is bitemporal: each mutation emits events with a monotonically-increasing transaction-time stamp, and a query can read as of any past stamp. The history is queryable, not overwritten.
What to read in chronicle.ar
A Person carries a mutable age, and Adult is the iff-refinement that classifies anyone 18 or older:
pub type Person { mut age: Int }
pub type Adult <: Person iff { self.age >= 18 };
Two declared queries pin the same Adult extent to two past transaction-times — the surface as_of <int> clause is the whole point:
pub query current_adults() -> Adult; // latest snapshot
pub query adults_at_2() -> Adult as_of 2; // right after the hire
pub query adults_at_4() -> Adult as_of 4; // right after set_age
The driver routes a query carrying as_of through the time-travel read path automatically; the modeler never touches the runtime API.
Running it
The demo.toml scenario hires alice as a minor (age 12), then later sets her age to 25. Each mutation advances transaction-time, so the same Adult query gives different answers at different stamps:
query chronicle::adults_at_2: 0 row(s) — alice is 12 here, not an Adult
query chronicle::adults_at_4: 1 row(s) alice — alice is 25 here, now an Adult
query chronicle::current_adults: 1 row(s) alice — latest state agrees with tx=4
The decisive contrast is adults_at_2 vs adults_at_4: it is one query against one individual, but the answer flips from empty to {alice} purely because the read snapshot moved past the set_age event. The refinement is re-evaluated against the field value as of each instant.
Honest caveats (what runs today)
- v0 admits only integer literals as the
as_ofargument (a raw transaction-time stamp). Forms likeas_of now() - 1 dayor a parameterized argument are reserved for a later RFD. - This is the transaction-time axis (when the system learned a fact). The valid-time axis — when a fact is true in the modelled world — is shown in effective_dated_tax_v0, which reads
as_of <#date#>.
This example is compiled and run in CI; its as_of snapshot behaviour is pinned by a corpus test (oxc-runtime/tests/examples_corpus.rs), so the time-travel surface can’t drift from the language.
Source
The package’s real Argon source, transcluded from the files this corpus compiles — what you read here is exactly what CI builds.
root.ar
//! Bitemporal time-travel via SURFACE syntax (`as_of <int>`).
mod chronicle;
chronicle.ar
//! Bitemporal time-travel via SURFACE syntax (`as_of <int>`).
//!
//! Demonstrates the surface-level half of the temporal wall. The
//! runtime API (`Store::query_extent_as_of`) was opened in a prior
//! commit; this example uses the modeler-facing `as_of` clause:
//!
//! pub query at_t1() -> Adult as_of 2;
//! pub query at_t3() -> Adult as_of 3;
//!
//! Driver dispatch routes through `query_extent_as_of` automatically
//! based on the `as_of_tx_nanos` field stamped on the QueryDecl.
//!
//! V0.1 admits only integer literals as `as_of` arguments. Future
//! RFDs may add `as_of now() - 1 day` or parameterized form.
pub type Person {
mut age: Int,
}
pub type Adult <: Person iff { self.age >= 18 };
pub mutate hire(p: Person, age: Int) {
insert iof(p, Person);
update p: Person set { age = age }
}
pub mutate set_age(p: Person, age: Int) {
update p: Person set { age = age }
}
// ANCHOR: as_of_queries
// Latest snapshot — the current Adult extent.
pub query current_adults() -> Adult;
// Snapshot at tx=2 (right after the hire).
pub query adults_at_2() -> Adult as_of 2;
// Snapshot at tx=4 (right after set_age).
pub query adults_at_4() -> Adult as_of 4;
// ANCHOR_END: as_of_queries