Skip to content

Commit c2379c4

Browse files
committed
feat(knowable-from): VART backend — Phase 3 reference impl
New optional feature `vart-backend` on `ogar-knowable-from`. Wires `VartKnowableFromStore` — a `KnowableFromStore` impl backed by AdaWorldAPI/vart (the versioned adaptive radix trie from the SurrealKV ecosystem). Closes PR #25's "reference backend" promise. # Mechanism Each `register` call: - locks the internal `Mutex<Tree<VariableSizeKey, u64>>` - advances the trie's logical version: new_version = tree.version() + 1 - inserts (key=NULL-terminated class_identity, value=new_version, version=new_version, ts=0) via `insert_or_replace` (upsert) - returns new_version as the `knowable_from` stamp Each `knowable_from(class_identity)`: - locks the trie - looks up at the latest version snapshot - returns the value (Option<u64>) # Why this matches the substrate's invariants - NiblePath-shaped class_identity (e.g. `ogit-erp/sale.order`) is prefix-radix-indexed natively — same primitive bardioc PR #18 / lance-graph PR #470 describe for `inv.object_instance`. - Immutable / copy-on-write — every register produces a new logical version; readers at any prior version see the world-as- of-that-version (audit-as-version per ADR-008 / ADR-013). - Append-only by construction — VART doesn't expose mutate-in-place semantics for past versions, so the registry log is forensically queryable. # Why the schema_ddl_hint is discarded here (v1) VART's value type is `u64` to keep the trie homogeneous. The hint parameter is accepted for trait conformance and explicitly noted as discarded in v1 code comments. The `surrealql-hint` feature (PR #33) renders DDL at the helper layer (`register_class_knowable_from`), upstream of the backend — the loop closes at the wrapper, not the backend. A future PR can wire a parallel `Tree<VariableSizeKey, String>` for hints if a real consumer needs them. # NULL-termination for prefix safety Per VART's variable-length-key discipline (src/lib.rs L42-48 of the upstream), class identities like `ogit-op/Work` and `ogit-op/WorkPackage` would address overlapping subtrees without NULL termination. The `make_key` helper appends \0 before constructing the `VariableSizeKey`. # Tests (6 new; 10 prior → 16 total) vart_empty_returns_none_and_version_zero vart_register_returns_monotonic_versions vart_knowable_from_returns_latest_for_key vart_re_register_same_class_advances_version vart_prefix_keys_do_not_collide [NULL-term receipt] vart_same_name_different_prefixes_do_not_collide [end-to-end through register_class_knowable_from; covers the PR #31 P2 case against the real backend] # Cargo deps + CI - `vart` as `optional = true` behind `vart-backend` feature. Pinned to AdaWorldAPI/vart fork (git dep); zero runtime sub-deps per its Cargo.toml. - CI: `cargo test -p ogar-knowable-from --features vart-backend` added — same crate-scoped pattern as the other feature-gated test steps. # Verification cargo test --workspace -> clean cargo test -p ogar-knowable-from -> 10/10 (default) cargo test -p ogar-knowable-from --features vart-backend -> 16/16 cargo test -p ogar-knowable-from --features surrealql-hint -> 10/10 cargo check --workspace --all-targets -> clean PII abort-guard (word-boundary): CLEAN on all touched files. # Position in sequencing Per `docs/RDF-OWL-ALIGNMENT.md §10`: Phase 1 (#30): RDF-OWL-ALIGNMENT doc MERGED Phase 2a (#37): ogar-adapter-ttl MERGED Phase 2b (#38): ogar-adapter-clickhouse-ddl MERGED Phase 2c ogar-from-osm-pbf QUEUED (gates on runtime D-OSM-3) Phase 3 (this): vart-backend on ogar-knowable-from OPENS Phase 4+: ogar-pattern / ogar-actionable / ... QUEUED https://claude.ai/code/session_01PBTGaPCSnnt6u3pjXpbLwY
1 parent f8297bc commit c2379c4

3 files changed

Lines changed: 242 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,10 @@ jobs:
5353
# gating pattern as the other adapter feature tests.
5454
- name: cargo test -p ogar-adapter-clickhouse-ddl --features clickhouse-parser
5555
run: cargo test -p ogar-adapter-clickhouse-ddl --features clickhouse-parser
56+
# Exercise the `vart-backend` feature on ogar-knowable-from —
57+
# pulls in AdaWorldAPI/vart (versioned adaptive radix trie) as
58+
# the reference KnowableFromStore impl. Same crate-scoped
59+
# pattern as the other feature-gated test steps. Per-PR #25's
60+
# "reference backend" promise, now real.
61+
- name: cargo test -p ogar-knowable-from --features vart-backend
62+
run: cargo test -p ogar-knowable-from --features vart-backend

crates/ogar-knowable-from/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,21 @@ serde = ["dep:serde", "ogar-vocab/serde"]
1717
# adapter pulls the SurrealDB-related dep graph; the default path
1818
# stays lightweight (only `ogar-vocab` + optional `serde`).
1919
surrealql-hint = ["dep:ogar-adapter-surrealql"]
20+
# Wires `VartKnowableFromStore` — a `KnowableFromStore` impl backed
21+
# by AdaWorldAPI/vart (the versioned adaptive radix trie from the
22+
# SurrealKV ecosystem). Each `register` call advances the trie's
23+
# global version and returns it as the `knowable_from` stamp; the
24+
# NiblePath-shaped `class_identity` is prefix-radix-indexed natively.
25+
# The runtime-session's "reference backend" promise from PR #25's
26+
# crate docs lands here.
27+
vart-backend = ["dep:vart"]
2028

2129
[dependencies]
2230
ogar-vocab = { path = "../ogar-vocab" }
2331
ogar-adapter-surrealql = { path = "../ogar-adapter-surrealql", optional = true }
2432
serde = { workspace = true, optional = true }
33+
# AdaWorldAPI/vart — versioned adaptive radix trie, MIT/Apache-2 dual.
34+
# Zero runtime deps of its own (per its Cargo.toml `[dependencies]`
35+
# is empty); pure-Rust, std-only. Pinned to the AdaWorldAPI mirror to
36+
# inherit any fork patches without going via crates.io.
37+
vart = { git = "https://github.com/AdaWorldAPI/vart", optional = true }

crates/ogar-knowable-from/src/lib.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,137 @@ impl std::fmt::Display for KnowableFromError {
312312

313313
impl std::error::Error for KnowableFromError {}
314314

315+
// ─────────────────────────────────────────────────────────────────────
316+
// VART backend (feature `vart-backend`)
317+
// ─────────────────────────────────────────────────────────────────────
318+
//
319+
// Reference backend impl named in PR #25's crate docs and ADR-024's
320+
// "VART pinned to the AdaWorldAPI mirror" cross-reference. Each
321+
// `register` call advances the trie's global version monotonically
322+
// and returns the new version as the `knowable_from` stamp.
323+
//
324+
// Architecture alignment:
325+
// - The NiblePath-shaped `class_identity` (e.g. `ogit-erp/sale.order`)
326+
// is prefix-radix-indexed natively by VART — same routing primitive
327+
// the runtime side uses (bardioc PR #18 / lance-graph PR #470
328+
// described the same trie-append pattern for `inv.object_instance`).
329+
// - VART is immutable / copy-on-write with snapshot isolation — every
330+
// register produces a new logical version; readers at any prior
331+
// version see the world-as-of-that-version. Suits the "audit-as-
332+
// version" discipline ADR-008 / ADR-013 already pin.
333+
// - The `schema_ddl_hint` parameter is intentionally discarded in v1
334+
// (VART's value type is `u64` to keep the trie homogeneous). A
335+
// follow-up could wire a parallel `Tree<VariableSizeKey, String>`
336+
// for hints if a real consumer needs them; today's `surrealql-hint`
337+
// feature already renders DDL at the helper layer, so the hint
338+
// lives upstream of the backend.
339+
/// VART-backed `KnowableFromStore` implementation — feature-gated
340+
/// (`vart-backend`). See [`vart_backend::VartKnowableFromStore`] for
341+
/// the full impl + design notes.
342+
#[cfg(feature = "vart-backend")]
343+
pub mod vart_backend {
344+
use super::{KnowableFromError, KnowableFromStore};
345+
use std::sync::Mutex;
346+
use vart::art::Tree;
347+
use vart::VariableSizeKey;
348+
349+
/// `KnowableFromStore` impl backed by an in-memory versioned
350+
/// adaptive radix trie. Each [`register`] call advances the trie's
351+
/// global version; the new version IS the `knowable_from` stamp
352+
/// (value stored = version, so lookup returns it directly).
353+
///
354+
/// The `class_identity` is encoded as a NULL-terminated byte
355+
/// sequence before being keyed into VART. NULL termination prevents
356+
/// prefix collisions per the variable-length-key discipline noted
357+
/// in VART's `src/lib.rs` documentation — without it, `ogit-op/Work`
358+
/// and `ogit-op/WorkPackage` would address overlapping subtrees.
359+
///
360+
/// Thread-safe via internal [`Mutex`]; the trait bound
361+
/// `Send + Sync` is satisfied.
362+
///
363+
/// # Persistence (v1: in-memory only)
364+
///
365+
/// v1 keeps the trie in memory. VART itself is *structurally*
366+
/// persistable (immutable copy-on-write); wiring it to a Lance /
367+
/// surrealkv / disk store is the natural next step but lives
368+
/// behind a separate feature gate once a real consumer needs it.
369+
///
370+
/// [`register`]: KnowableFromStore::register
371+
pub struct VartKnowableFromStore {
372+
tree: Mutex<Tree<VariableSizeKey, u64>>,
373+
}
374+
375+
impl VartKnowableFromStore {
376+
/// Build a new in-memory VART-backed store with an empty trie.
377+
/// The first `register` call returns version `1` (VART's
378+
/// `version()` starts at `0` for an empty trie).
379+
#[must_use]
380+
pub fn new() -> Self {
381+
Self { tree: Mutex::new(Tree::new()) }
382+
}
383+
384+
/// Current max version across the trie. `0` if no `register`
385+
/// has been called yet.
386+
#[must_use]
387+
pub fn current_version(&self) -> u64 {
388+
self.tree.lock().map(|t| t.version()).unwrap_or(0)
389+
}
390+
391+
/// Build a VART `VariableSizeKey` from a class identity string,
392+
/// appending the NULL byte per the variable-length-key
393+
/// discipline (see struct-level doc).
394+
fn make_key(class_identity: &str) -> VariableSizeKey {
395+
let mut bytes = class_identity.as_bytes().to_vec();
396+
bytes.push(0);
397+
// VART's inherent `from(Vec<u8>)` constructor (the trait
398+
// `From<&[u8]>` impl exists but is shadowed by the
399+
// inherent method).
400+
VariableSizeKey::from(bytes)
401+
}
402+
}
403+
404+
impl Default for VartKnowableFromStore {
405+
fn default() -> Self {
406+
Self::new()
407+
}
408+
}
409+
410+
impl KnowableFromStore for VartKnowableFromStore {
411+
fn register(
412+
&self,
413+
class_identity: &str,
414+
_schema_ddl_hint: Option<&str>,
415+
) -> Result<u64, KnowableFromError> {
416+
let mut tree = self.tree.lock().map_err(|e| {
417+
KnowableFromError::Backend(format!("vart mutex poisoned: {e}"))
418+
})?;
419+
// Advance the trie's logical version monotonically — the new
420+
// version IS the knowable_from stamp we return. saturating_add
421+
// guards the theoretical wrap (registering 2^64 times).
422+
let new_version = tree.version().saturating_add(1);
423+
let key = Self::make_key(class_identity);
424+
// insert_or_replace is the upsert path: re-registering the same
425+
// class identity advances its version (the registry behaviour
426+
// the runtime side's `inv.object_instance` trie-append also uses
427+
// — every commit is a new version of the entry).
428+
tree.insert_or_replace(&key, new_version, new_version, 0)
429+
.map_err(|e| {
430+
KnowableFromError::Backend(format!("vart insert: {e:?}"))
431+
})?;
432+
Ok(new_version)
433+
}
434+
435+
fn knowable_from(&self, class_identity: &str) -> Option<u64> {
436+
let tree = self.tree.lock().ok()?;
437+
let key = Self::make_key(class_identity);
438+
// Latest snapshot — pass the trie's current version so we get
439+
// the freshest stamp for the key.
440+
let latest = tree.version();
441+
tree.get(&key, latest).map(|(v, _ts, _vsn)| v)
442+
}
443+
}
444+
}
445+
315446
#[cfg(test)]
316447
mod tests {
317448
use super::*;
@@ -548,4 +679,95 @@ mod tests {
548679
"expected DEFINE FIELD in the hint, got: {hint}"
549680
);
550681
}
682+
683+
// ── VART backend tests (feature `vart-backend`) ─────────────────────
684+
// Exercise the reference-backend impl named in PR #25's crate docs.
685+
// Verifies: monotonic version advance, lookup after register,
686+
// prefix-collision safety (the NULL-byte termination discipline),
687+
// and the composition with `register_class_knowable_from` (the
688+
// PR #31 canonical-identity helper).
689+
// ────────────────────────────────────────────────────────────────────
690+
691+
#[cfg(feature = "vart-backend")]
692+
#[test]
693+
fn vart_empty_returns_none_and_version_zero() {
694+
let store = crate::vart_backend::VartKnowableFromStore::new();
695+
assert_eq!(store.current_version(), 0);
696+
assert!(store.knowable_from("ogit-erp/Account").is_none());
697+
}
698+
699+
#[cfg(feature = "vart-backend")]
700+
#[test]
701+
fn vart_register_returns_monotonic_versions() {
702+
let store = crate::vart_backend::VartKnowableFromStore::new();
703+
let v1 = store.register("ogit-erp/A", None).unwrap();
704+
let v2 = store.register("ogit-erp/B", None).unwrap();
705+
let v3 = store.register("ogit-erp/C", None).unwrap();
706+
assert!(v1 < v2 && v2 < v3, "versions not monotonic: {v1} {v2} {v3}");
707+
// Empty trie's version() starts at 0, so first register lands at 1.
708+
assert_eq!(v1, 1);
709+
}
710+
711+
#[cfg(feature = "vart-backend")]
712+
#[test]
713+
fn vart_knowable_from_returns_latest_for_key() {
714+
let store = crate::vart_backend::VartKnowableFromStore::new();
715+
let v = store.register("ogit-op/WorkPackage", None).unwrap();
716+
assert_eq!(store.knowable_from("ogit-op/WorkPackage"), Some(v));
717+
// Unrelated class returns None.
718+
assert!(store.knowable_from("ogit-op/Issue").is_none());
719+
}
720+
721+
#[cfg(feature = "vart-backend")]
722+
#[test]
723+
fn vart_re_register_same_class_advances_version() {
724+
// Upsert semantics: re-registering bumps the version (the trie's
725+
// immutable-versioned shape — each register is a new logical
726+
// moment in the registry).
727+
let store = crate::vart_backend::VartKnowableFromStore::new();
728+
let v_first = store.register("ogit-erp/Account", None).unwrap();
729+
let v_second = store.register("ogit-erp/Account", None).unwrap();
730+
assert!(v_second > v_first);
731+
// Latest snapshot returns the most recent version.
732+
assert_eq!(store.knowable_from("ogit-erp/Account"), Some(v_second));
733+
}
734+
735+
#[cfg(feature = "vart-backend")]
736+
#[test]
737+
fn vart_prefix_keys_do_not_collide() {
738+
// The NULL-byte-termination discipline: `ogit-op/Work` and
739+
// `ogit-op/WorkPackage` differ only in suffix; without
740+
// termination, the trie could conflate them at the radix
741+
// boundary. With termination, distinct stamps.
742+
let store = crate::vart_backend::VartKnowableFromStore::new();
743+
let v_work = store.register("ogit-op/Work", None).unwrap();
744+
let v_pkg = store.register("ogit-op/WorkPackage", None).unwrap();
745+
assert_ne!(v_work, v_pkg);
746+
assert_eq!(store.knowable_from("ogit-op/Work"), Some(v_work));
747+
assert_eq!(store.knowable_from("ogit-op/WorkPackage"), Some(v_pkg));
748+
}
749+
750+
#[cfg(feature = "vart-backend")]
751+
#[test]
752+
fn vart_same_name_different_prefixes_do_not_collide() {
753+
// The Codex P2 motivating case from PR #31 — same class name
754+
// (`WorkPackage`) under different OGIT prefixes must register
755+
// as distinct entries when the canonical identity differs.
756+
// VART-backed end-to-end through `register_class_knowable_from`.
757+
use ogar_vocab::Class;
758+
let store = crate::vart_backend::VartKnowableFromStore::new();
759+
let v_op = register_class_knowable_from(
760+
&Class::new("WorkPackage"),
761+
"ogit-op/WorkPackage",
762+
&store,
763+
).unwrap();
764+
let v_erp = register_class_knowable_from(
765+
&Class::new("WorkPackage"),
766+
"ogit-erp/WorkPackage",
767+
&store,
768+
).unwrap();
769+
assert_ne!(v_op, v_erp);
770+
assert_eq!(store.knowable_from("ogit-op/WorkPackage"), Some(v_op));
771+
assert_eq!(store.knowable_from("ogit-erp/WorkPackage"), Some(v_erp));
772+
}
551773
}

0 commit comments

Comments
 (0)