Skip to main content

TraversalBuilder

Struct TraversalBuilder 

Source
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: 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: Option<String>

The valid-time instant to traverse at, if it is not the present.

Added in 0.6.0 so “as of Tuesday” is expressible. Before this, 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.

§content: bool

Whether 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

Source

pub fn new(start_node: impl Into<String>) -> Self

Source

pub fn max_depth(self, depth: usize) -> Self

Source

pub fn edge_types(self, types: Vec<String>) -> Self

Source

pub fn min_weight(self, weight: f64) -> Self

Source

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.

Source

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.

Source

pub fn as_of(self, ts: impl Into<String>) -> Self

Traverse the graph as it was at ts rather than at the present (§5.2).

§Setting this makes the attribute mode a required decision (T3.2, D-085)

A historical traversal has two independent temporal questions, and until 0.6.0 only one of them was asked. The topology comes from ts. The node attributes — titles, content — come from wherever AttributeMode says, and the default said Current, which is live text. So as_of(Tuesday) returned Tuesday’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 as_of set and no Self::attribute_mode call, Self::execute returns DbError::AttributeModeUnstated rather than guessing. Both answers stay available and neither is silent:

// Tuesday's graph with Tuesday's titles — usually what was meant.
let then = TraversalBuilder::new("a")
    .as_of("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("2026-01-06T00:00:00.000000Z")
    .attribute_mode(AttributeMode::Current)
    .execute(conn, now)
    .await?;

A traversal with no as_of 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.

§ts is read on two different clocks, and that is a defect (0.12.17, W5.6, D-160)

Doctrine VIII says as_of(ts) means valid time under current belief and returns exactly that, and that queries mixing axes say so in their signatures. This one does not. Precisely what the single ts is compared against today:

half of the answerthe column ts is compared tothe axis that is
topology (which edges exist)links.valid_from / links.valid_tovalid time, under current belief — Doctrine VIII’s contract, met
attributes, AttributeMode::AtTimetransaction_log.recorded_attransaction time — belief as of ts, which is reconstruct’s axis
attributes, AttributeMode::Currentnothing — concepts WHERE retired = 0valid time now, belief now

So as_of(t).attribute_mode(AtTime) — the combination the example above calls “usually what was meant” — answers “the edges that were valid at t, labelled with what we believed at t. Both halves are defensible; the pairing is two questions under one timestamp, which is the conflation §3.1 names.

What that costs a caller, concretely. Suppose a concept’s title is corrected today, fixing a typo made in 2020. as_of("2020-06-01") with AtTime returns the uncorrected title, because the correction was recorded after ts. That is the right answer to “what did we believe in 2020” and the wrong one to “what was true in 2020”, and as_of’s name promises the second.

A second, smaller mismatch in the same direction: AtTime hydration filters on recorded_at and on the payload’s retired flag, and never consults the concept’s own valid interval. A concept whose validity had ended before ts still hydrates, so the two halves of the answer do not even agree about what “existed at ts” means.

This is stated and not fixed, deliberately. Changing it changes answers callers already depend on, so it is a break and belongs to a release that is allowed to make one — W7.1, in 0.14.0. Writing the semantics down one release early is what makes that change reviewable against a stated position instead of argued in the commit that makes it.

Until then: if you want belief as of t, that operation exists and is named for it — crate::temporal::reconstruct. If you want what was true at t, as best we now know, no combination here gives it, because no attribute mode reads concept attributes on the valid-time axis.

Source

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.

Source

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 when set, else at now_ts. 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.

Source

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 Self::as_of is set and Self::attribute_mode is not — see as_of for why that combination is a question rather than a default (T3.2, D-085).

now_ts is the caller’s present. A traversal with as_of set reads topology at that instant instead; now_ts is still what an AttributeMode::Current hydrate means by “current”.

Trait Implementations§

Source§

impl Clone for TraversalBuilder

Source§

fn clone(&self) -> TraversalBuilder

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for TraversalBuilder

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more