topodb 0.0.6

Embedded, local-first memory engine for AI agents: temporal property graph + scoped recall.
Documentation
//! Committed format fixtures. `v1.redb` and `v2.redb` are FROZEN migration
//! input, each generated once by an earlier version of this crate and never
//! regenerated since: the code path that originally emitted them (a pre-dict,
//! pre-slot v1 layout for `v1.redb`; `FORMAT_VERSION == 2` for `v2.redb`) is
//! either gone or no longer reachable — `Storage::open_with` always stamps a
//! brand-new file at the CURRENT `FORMAT_VERSION` (3), never an older one, so
//! there is no code path left in this crate that can reproduce either file.
//! `v2-workload.redb` (see `generate_v2_workload.rs`/`v2_workload_fixture.rs`)
//! is a third FROZEN fixture — a larger v2 corpus used by `migrate_v3.rs`'s
//! own migration unit tests — whose one-shot `#[ignore]`d generator is
//! likewise never re-run now that the v3 cutover has happened. `v3.redb` is
//! the only fixture in this file this build can actually regenerate: it pins
//! the CURRENT native (un-migrated) v3 layout.
//!
//! - `regenerate_v3_fixture` (`#[ignore]`): (re)writes `v3.redb`, using the
//!   SAME content recipe `v2.redb` was originally generated with (this test
//!   used to target `v2.redb`; the v3 cutover retargeted it to `v3.redb`
//!   without changing what it writes). Run it explicitly with `cargo test -p
//!   topodb --test format_fixture -- --ignored regenerate` whenever the v3
//!   native layout intentionally changes (and bump `FORMAT_VERSION` in
//!   `storage.rs` + `FORMAT.md` alongside it — this test does not do that for
//!   you).
//! - `v1_fixture_opens_and_reads` / `v2_fixture_opens_and_reads` /
//!   `v3_fixture_opens_and_reads` (run in the normal suite): each copies its
//!   committed fixture to a tempdir and asserts the documented queries
//!   (`nodes_by_prop`, `search_text`, `search_vector`, `current_seq`,
//!   `format_version`) still see the expected content. The committed file is
//!   NEVER opened read-write in place — `open_with` can write to META
//!   (format_version/index_spec stamping), which would dirty the committed
//!   bytes on every test run.
//!
//! Node/scope ids use `NodeId::from_u128`/`ScopeId::from_u128`
//! (`#[doc(hidden)]` debug-seam constructors added in `ids.rs` for exactly
//! this purpose) rather than `Ulid::new()`, so the fixture's *content* is
//! reproducible across regenerations. The raw `.redb` bytes are NOT
//! guaranteed byte-for-byte stable (redb's on-disk layout has padding/free
//! space that isn't part of the content contract) — only the query results
//! below are.

use topodb::*;

/// Regenerate with: cargo test -p topodb --test format_fixture -- --ignored regenerate
#[test]
#[ignore]
fn regenerate_v3_fixture() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v3.redb");
    let _ = std::fs::remove_file(&path);
    let spec = IndexSpec {
        equality: vec![PropIndex {
            label: "Entity".into(),
            prop: "name".into(),
        }],
        text: vec![PropIndex {
            label: "Memory".into(),
            prop: "content".into(),
        }],
    };
    {
        let db = Db::open_with(&path, spec).unwrap();
        // Fixed ids so the fixture's CONTENT is reproducible across
        // regenerations (the raw bytes are not — see the module doc comment).
        let s = ScopeId::from_u128(1);
        let n1 = NodeId::from_u128(10);
        let n2 = NodeId::from_u128(11);
        let mut p1 = Props::new();
        p1.insert("name".into(), PropValue::Str("ada".into()));
        let mut p2 = Props::new();
        p2.insert(
            "content".into(),
            PropValue::Str("fixture memory about databases".into()),
        );
        db.submit(vec![
            Op::CreateNode {
                id: n1,
                scope: Scope::Id(s),
                label: "Entity".into(),
                props: p1,
            },
            Op::CreateNode {
                id: n2,
                scope: Scope::Id(s),
                label: "Memory".into(),
                props: p2,
            },
        ])
        .unwrap();
        db.submit(vec![Op::SetEmbedding {
            id: n2,
            model: "m1".into(),
            vector: vec![1.0, 0.0],
        }])
        .unwrap();
        // `db` drops here: `Drop for Inner` joins the applier/bumper threads
        // and closes the redb file handle, so the raw reopen+compact below
        // gets exclusive access.
    }

    // redb pre-allocates generously as a file grows; a handful of tiny write
    // transactions leaves several MB of free space committed to disk. Compact
    // before shipping the fixture so the checked-in binary reflects only the
    // handful of rows above, not incidental redb bookkeeping overhead.
    {
        let mut raw = redb::Database::open(&path).unwrap();
        raw.compact().unwrap();
    }

    assert!(
        path.exists(),
        "regenerate_v3_fixture: fixture file was not created at {path:?}"
    );
}

#[test]
fn v1_fixture_opens_and_reads() {
    let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v1.redb");
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("v1.redb");
    std::fs::copy(&src, &path).unwrap(); // never open the committed file read-write
    let spec = IndexSpec {
        equality: vec![PropIndex {
            label: "Entity".into(),
            prop: "name".into(),
        }],
        text: vec![PropIndex {
            label: "Memory".into(),
            prop: "content".into(),
        }],
    };
    let db = Db::open_with(&path, spec).unwrap();
    let s = ScopeId::from_u128(1);
    let scopes = ScopeSet::of(&[s]);
    assert_eq!(
        db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
            .unwrap()
            .len(),
        1
    );
    assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
    assert_eq!(
        db.search_vector(&VectorQuery {
            scopes: scopes.clone(),
            model: "m1".into(),
            vector: vec![1.0, 0.0],
            k: 1,
            candidates: None,
        })
        .unwrap()
        .len(),
        1
    );
    assert_eq!(db.current_seq().unwrap(), 3);
    assert_eq!(db.format_version(), 3);
    drop(db);
    // The second open takes the v2 fast path; migration is idempotent.
    let reopened = Db::open_with(
        &path,
        IndexSpec {
            equality: vec![PropIndex {
                label: "Entity".into(),
                prop: "name".into(),
            }],
            text: vec![PropIndex {
                label: "Memory".into(),
                prop: "content".into(),
            }],
        },
    )
    .unwrap();
    assert_eq!(reopened.current_seq().unwrap(), 3);
}

#[test]
fn v2_fixture_opens_and_reads() {
    let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v2.redb");
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("v2.redb");
    std::fs::copy(&src, &path).unwrap();
    let spec = IndexSpec {
        equality: vec![PropIndex {
            label: "Entity".into(),
            prop: "name".into(),
        }],
        text: vec![PropIndex {
            label: "Memory".into(),
            prop: "content".into(),
        }],
    };
    let db = Db::open_with(&path, spec).unwrap();
    let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
    assert_eq!(
        db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
            .unwrap()
            .len(),
        1
    );
    assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
    assert_eq!(db.current_seq().unwrap(), 3);
    assert_eq!(db.format_version(), 3);
}

#[test]
fn v3_fixture_opens_and_reads() {
    let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v3.redb");
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("v3.redb");
    std::fs::copy(&src, &path).unwrap(); // never open the committed file read-write
    let spec = IndexSpec {
        equality: vec![PropIndex {
            label: "Entity".into(),
            prop: "name".into(),
        }],
        text: vec![PropIndex {
            label: "Memory".into(),
            prop: "content".into(),
        }],
    };
    let db = Db::open_with(&path, spec).unwrap();
    let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
    assert_eq!(
        db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
            .unwrap()
            .len(),
        1
    );
    assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
    assert_eq!(
        db.search_vector(&VectorQuery {
            scopes: scopes.clone(),
            model: "m1".into(),
            vector: vec![1.0, 0.0],
            k: 1,
            candidates: None,
        })
        .unwrap()
        .len(),
        1
    );
    assert_eq!(db.current_seq().unwrap(), 3);
    assert_eq!(db.format_version(), 3);
}