pub struct TraversalBuilder {
pub start_node: String,
pub max_depth: usize,
pub edge_types: Vec<String>,
pub min_weight: f64,
pub attribute_mode: Option<AttributeMode>,
pub as_of_valid: Option<String>,
pub as_of_recorded: Option<String>,
pub content: bool,
}Expand description
Recursive CTE traversal query builder (§5.2).
Fields§
§start_node: String§max_depth: usize§edge_types: Vec<String>§min_weight: f64§attribute_mode: Option<AttributeMode>None means defaulted, not Current (T3.2, D-085).
The distinction is the whole mechanism. Current chosen by a caller who
knows what it means is a legitimate, fast answer; Current arrived at by
never touching the setting, on a query about the past, is a wrong answer
nobody asked for. Those two produce identical behaviour and must not be
stored identically, so the field records which happened.
Public for construction-by-struct-literal, which is why it is an Option
here rather than a private bool beside the mode: a caller building the
struct directly should have to write down the same thing the builder
method records.
as_of_valid: Option<String>The valid-time instant to traverse at, if it is not the present.
Added in 0.6.0 as as_of so “as of Tuesday” is expressible. Before
that, the instant arrived as execute’s now_ts parameter and a
historical traversal was indistinguishable from a live one — which is why
the mismatch with AttributeMode::Current could only ever be a warn!:
nothing in the call had the information needed to raise an error.
Renamed from as_of in 0.13.2 (W7.1, D-174). The old name carried
one instant onto two clocks; the method’s own docs are where that is
argued out.
as_of_recorded: Option<String>The transaction-time instant to traverse at: what did we believe then (0.13.2, W7.1, D-174).
None — the default — means current belief, and the walk reads
links_current exactly as it always has. Some(t) folds
transaction_log to t instead, so the topology is the one the ledger
held at t rather than the one it holds now. See
Self::as_of_recorded.
content: boolWhether crate::Database::load_subgraph_with should fetch
concepts.content (0.8.0, B3, D-116).
Default false, which is a change in what a load returns. No
algorithm reads document text, and at realistic document sizes it is
most of the byte budget, so the default was spending the budget on bytes
nothing would look at. A caller who needs it asks; one who does not gets
NodeData::content() == None, which is distinguishable from an empty
document.
Ignored by crate::Database::load_subgraph, which has no builder and
never loads content.
Implementations§
Source§impl TraversalBuilder
impl TraversalBuilder
pub fn new(start_node: impl Into<String>) -> Self
pub fn max_depth(self, depth: usize) -> Self
pub fn edge_types(self, types: Vec<String>) -> Self
pub fn min_weight(self, weight: f64) -> Self
Sourcepub fn attribute_mode(self, mode: AttributeMode) -> Self
pub fn attribute_mode(self, mode: AttributeMode) -> Self
State the attribute mode explicitly.
Calling this is what turns Current from a default into a decision, and
Self::execute treats the two differently on a historical traversal —
see Self::as_of_valid.
Sourcepub fn content(self, content: bool) -> Self
pub fn content(self, content: bool) -> Self
Fetch concepts.content into every hydrated node (0.8.0, B3, D-116).
Off by default. Turning it on is what the byte budget is then spent on: at 20 KB per concept, document text is the large majority of a loaded graph, and none of the six algorithms reads it.
Sourcepub fn as_of_valid(self, ts: impl Into<String>) -> Self
pub fn as_of_valid(self, ts: impl Into<String>) -> Self
Traverse the graph as it was in the world at ts — the valid-time
axis (§5.2, W7.1).
§This was as_of, and the rename is the fix (0.13.2, W7.1, D-174)
Doctrine VIII
says a query that mixes the two clocks says so in its signature. as_of
did not: one timestamp reached links.valid_from/valid_to on the
valid-time axis and transaction_log.recorded_at on the
transaction-time axis, so as_of(t).attribute_mode(AtTime) answered
“the edges valid at t, labelled with what we believed at t” — two
questions under one word. §3.1
named it; 0.12.17 (W5.6, D-160) wrote the semantics down without changing
them, precisely so this change could be reviewed against a stated
position; this is that change.
The two axes are now two parameters, and they compose:
| set | topology comes from | attributes come from |
|---|---|---|
| neither | links_current, at now_ts | live concepts |
as_of_valid(v) | links_current, bounded at v | concepts valid at v |
as_of_recorded(r) | transaction_log folded to r, bounded at now_ts | the payload believed at r |
| both | folded to r, bounded at v | believed at r, valid at v |
The last row is the cell Jensen and Snodgrass’s BCDM defines a bitemporal
database as answering — what did we believe at r about what was true at
v — and before this it was not expressible on any surface in the crate.
§Setting either instant makes the attribute mode a required decision (T3.2, D-085)
A historical traversal has two independent questions and until 0.6.0 only
one of them was asked. The topology comes from the instants. The node
attributes — titles, content — come from wherever AttributeMode
says, and the default said Current, which is live text. So a historical
traversal returned the past’s graph wearing today’s titles, and reported
that through a tracing::warn! — invisible in any application that has
not configured a subscriber, which is most of them at first run.
So: with either instant set and no Self::attribute_mode call,
Self::execute returns
DbError::AttributeModeUnstated
rather than guessing. Both answers stay available and neither is silent:
// What was true on Tuesday, as best we now know.
let then = TraversalBuilder::new("a")
.as_of_valid("2026-01-06T00:00:00.000000Z")
.attribute_mode(AttributeMode::AtTime)
.execute(conn, now)
.await?;
// Tuesday's topology with today's titles — legitimate, and now stated.
let mixed = TraversalBuilder::new("a")
.as_of_valid("2026-01-06T00:00:00.000000Z")
.attribute_mode(AttributeMode::Current)
.execute(conn, now)
.await?;A traversal with neither instant is a query about now, where Current
and AtTime agree about which text to return, so the default stands and
no caller has to change.
§What the rename buys, concretely
Suppose a concept’s title is corrected today, fixing a typo made in 2020.
Under the old as_of("2020-06-01") with AtTime the answer was the
uncorrected title, because the correction was recorded after ts —
the right answer to what did we believe in 2020 and the wrong one to
what was true in 2020, which is what the name promised. Now
as_of_valid("2020-06-01") alone gives the corrected title,
as_of_recorded("2020-06-01") gives the uncorrected one, and a caller
asking for either says which.
The second, smaller mismatch W5.6 recorded is closed by the same change:
AtTime hydration consulted the payload’s retired flag and never the
concept’s own valid interval, so a concept whose validity had ended
still hydrated. It is now bounded by whichever instants are set, so the
two halves of the answer agree about what “existed then” means.
Sourcepub fn as_of_recorded(self, ts: impl Into<String>) -> Self
pub fn as_of_recorded(self, ts: impl Into<String>) -> Self
Traverse the graph as the ledger believed it at ts — the
transaction-time axis (0.13.2, W7.1, D-174).
Where Self::as_of_valid asks what was true, this asks what did we
think was true. Setting it moves the walk off links_current and onto a
fold of transaction_log bounded at ts — the same fold
crate::temporal::reconstruct performs. That operation still exists
and still returns the whole state; this makes the same instant reachable
from a traversal, which is what lets the two axes be set on one query.
§This reads the hot log, and refuses rather than guessing
A fold can only answer for instants the hot log still covers.
crate::Database::archive removes superseded rows, so an instant below
what remains is not before history, it is history that is in the other
file — and this surface takes a connection, not an archive path, so it
cannot go and get it. It returns
DbError::RecordedInstantUnreachable
naming the instant and pointing at reconstruct, which does take the
path. Answering from a partial fold would return nearly the right
topology, which is the worst failure available to a ledger.
§Cost, stated rather than discovered
links_current is a projection maintained for exactly this read and
indexed for it (idx_lc_traversal_cover). The fold is a window function
over transaction_log with a json_extract per column, materialised
once per query and joined per hop. It is not the fast path and is not
meant to be. W10.6 measures it and decides whether anything should be
built for it.
Sourcepub fn build_sql(&self) -> String
pub fn build_sql(&self) -> String
Compile the recursive CTE query string as specified in §5.2.
Edge types become bind placeholders, not quoted literals. An earlier
version spliced them in with format!("'{t}'"), which made any caller
string a SQL fragment on the read path — and the only validation in the
crate, super::edge::validate_edge_type, runs in
super::EdgeAssertion::normalized on the write path, so a traversal
never passed through it. Binding removes the question rather than
answering it: unlike a table name, an edge type is a value, and values
can be parameters.
Sourcepub async fn execute_ids(
&self,
conn: &Connection,
now_ts: &str,
) -> Result<Vec<String>>
pub async fn execute_ids( &self, conn: &Connection, now_ts: &str, ) -> Result<Vec<String>>
Node ids reachable under this traversal, in id order (§5.2).
Reads at Self::as_of_valid when set, else at now_ts, and under the
belief Self::as_of_recorded names when set, else current belief. No
attribute mode is involved, so this never returns
DbError::AttributeModeUnstated:
topology at an instant is unambiguous, and it is only the pairing with
live attributes that needed a decision.
§Errors
DbError::RecordedInstantUnreachable
when Self::as_of_recorded is below what the hot log still covers.
Sourcepub async fn execute(
&self,
conn: &Connection,
now_ts: &str,
) -> Result<Vec<NodeAttributes>>
pub async fn execute( &self, conn: &Connection, now_ts: &str, ) -> Result<Vec<NodeAttributes>>
Execute the traversal and hydrate attributes per Self::attribute_mode
(§5.2).
The hydration is a second step rather than a join in the CTE because the
three modes read from two different places: Current and Omit from
concepts, AtTime from transaction_log. The previous version always
emitted the concepts join, so attribute_mode was stored, exposed by a
builder method, and never read — a caller asking for AtTime got live
attributes with no indication that the mode had been ignored. That is the
exact failure Doctrine II exists to prevent, arriving as a silent wrong
answer rather than as an error.
AttributeMode::Omit returns Ok(vec![]) here, which is
indistinguishable from a traversal that reached nothing. That is a
limitation of this method’s return type rather than of the mode; callers
wanting topology only should use Self::execute_ids, which says what it
found.
§Errors
DbError::AttributeModeUnstated
when either instant is set and Self::attribute_mode is not — see
Self::as_of_valid for why that combination is a question rather than
a default (T3.2, D-085).
DbError::RecordedInstantUnreachable
when Self::as_of_recorded is below what the hot log still covers.
now_ts is the caller’s present, and it is the fallback on both axes:
a traversal with neither instant set reads live topology and live text.
Trait Implementations§
Source§impl Clone for TraversalBuilder
impl Clone for TraversalBuilder
Source§fn clone(&self) -> TraversalBuilder
fn clone(&self) -> TraversalBuilder
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl Freeze for TraversalBuilder
impl RefUnwindSafe for TraversalBuilder
impl Send for TraversalBuilder
impl Sync for TraversalBuilder
impl Unpin for TraversalBuilder
impl UnsafeUnpin for TraversalBuilder
impl UnwindSafe for TraversalBuilder
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request