bitemporal-runtime 0.1.0

Bitemporal truth primitives — valid_time/recorded_time tracking, append-supersede, as-of queries, temporal snapshots.
Documentation

bitemporal-runtime

Bitemporal truth primitives for append-supersede temporal data.

bitemporal-runtime is the only Rust implementation of strict bitemporal truth — separate valid_time and recorded_time, append-supersede, as-of queries. As of release, zero analogues exist on crates.io.

When to use it

Bitemporal data modeling is for domains where you need to answer "what did we believe at time T about the fact that was valid at time V" — audit, regulatory reporting, scientific reproducibility, financial historical reconstruction, version-of-truth queries.

If you have one timeline (just updated_at or just valid_from), you don't need this crate. If you have two, and you need both, you do.

Quick Start

use bitemporal_runtime::{append_supersede, as_of_query, BitemporalRecord};
use chrono::{TimeZone, Utc};

fn main() {
    // Build a fact at two points in time — first we believed one
    // thing, then a corrected version came in.
    let t0 = Utc.timestamp_opt(1000, 0).unwrap();
    let t1 = Utc.timestamp_opt(2000, 0).unwrap();
    let t2 = Utc.timestamp_opt(3000, 0).unwrap();

    let mut records: Vec<BitemporalRecord<&str>> = Vec::new();
    append_supersede(
        &mut records,
        BitemporalRecord {
            id: "us-presidents".to_string(),
            valid_time: t0,
            recorded_time: t0,
            value: "Roosevelt",
        },
    ).unwrap();
    let receipts = append_supersede(
        &mut records,
        BitemporalRecord {
            id: "us-presidents".to_string(),
            valid_time: t0,
            recorded_time: t1,
            value: "Truman",
        },
    ).unwrap();

    // Supersession receipts are the audit handle for the change.
    // SHA-256 digests bind every record field (id, temporal, value).
    assert_eq!(receipts.len(), 1);
    assert_eq!(receipts[0].superseded.superseded_id, "us-presidents");
    assert_eq!(receipts[0].superseding_id, "us-presidents");

    // "As of T=1500, what did we believe was the value of
    // `us-presidents` at T=2000?" — answer: Truman, because that
    // was the latest recorded version by 1500.
    let as_of = as_of_query(&records, t1, Utc.timestamp_opt(1500, 0).unwrap());
    assert_eq!(as_of[0].value, "Roosevelt"); // only v1 was known by T=1500

    // "As of T=2500, what did we believe was valid at T=3000?" —
    // answer: the latest known version, v2 (Truman).
    let as_of = as_of_query(&records, t2, Utc.timestamp_opt(2500, 0).unwrap());
    assert_eq!(as_of[0].value, "Truman");
}

Overview

This crate provides first-class support for bitemporal data modeling:

  • valid_time: When a fact is true in the business domain
  • recorded_time: When the system captured the fact
  • append-supersede: Updates append new rows instead of mutating existing ones

Key types

  • BitemporalRecord<T> — a temporal record with valid_time, recorded_time, and domain value
  • SupersessionReceipt — cryptographic SHA-256 receipt for supersession events
  • InMemoryDb — in-memory store for testing
  • SqliteDb (feature sqlite) — durable SQLite-backed store

Core functions

  • append_supersede() — append a new record, emit receipts for superseded prior versions
  • as_of_query() — query records valid at a given valid_time as of a given recorded_time
  • temporal_snapshot() — retrieve full state as of a given recorded_time

Cargo features

  • sqlite — enables the SqliteDb durable store (adds rusqlite dep)
  • schema — enables JSON Schema generation for the public types (adds schemars dep)

Both features are off by default to keep the dep graph small.

MSRV

Rust 1.75 (2021 edition).

Dependencies

Default: chrono, serde, serde_json, sha2, thiserror. With sqlite: adds rusqlite. With schema: adds schemars.

License

MIT OR Apache-2.0 (dual-licensed). See LICENSE-MIT and LICENSE-APACHE for the full texts.

Changelog

See CHANGELOG.md for the release history.