Skip to main content

macrame/
branch.rs

1//! Lineage identity and its lifecycle (§15.2, §15.4).
2//!
3//! The read half of branching shipped first, at 0.14.2 through 0.14.6: the
4//! ledger tables carry a `branch_id`, [`crate::graph::TraversalBuilder::on_branch`]
5//! resolves along the ancestry, and 0.14.6 bounded that resolution by the fork
6//! point ([D-223]). Nothing in that half could *create* a lineage — only raw
7//! SQL could, which is why 0.14.6 could repair the read's semantics without
8//! breaking anyone. This module is the other half: the name, and the one write
9//! that registers it.
10//!
11//! # Why the read shipped first, and why that ordering is not an accident
12//!
13//! A write that creates something unreadable is the worse order. Had `fork()`
14//! landed at 0.14.2, every branch created between then and 0.14.6 would have
15//! been readable only through a query that silently absorbed its parent's later
16//! writes — and the repair would have been a semantic break on stored data
17//! rather than a correction to an unreachable path. That is [D-160] → [D-174]'s
18//! ordering, applied a third time and recorded in [D-223].
19//!
20//! [D-160]: ../../docs/architecture/s13-decision-register.md#d-160
21//! [D-174]: ../../docs/architecture/s13-decision-register.md#d-174
22//! [D-223]: ../../docs/architecture/s13-decision-register.md#d-223
23
24use crate::error::{DbError, Result};
25use crate::schema::ddl;
26
27/// One resolved ancestor of a lineage, nearest first.
28///
29/// Public here rather than in `graph` because it is a fact about the register
30/// this module owns, and because the two functions that take one —
31/// [`crate::temporal::resolve_beliefs`] and
32/// [`crate::temporal::reconstruct_on`] — are read-path answers *about* a
33/// branch. Obtain one from
34/// [`Database::ancestry`](crate::connection::Database::ancestry); the crate
35/// resolves it from `branches` and nothing else can construct it correctly
36/// (0.15.17, [D-259]).
37///
38/// [D-259]: ../docs/architecture/s13-decision-register.md#d-259
39pub use crate::graph::lineage::Ancestor;
40
41/// Longest accepted lineage name.
42///
43/// A sanity bound rather than a schema limit — `branches.branch_id` is `TEXT`
44/// and SQLite would take a megabyte. It is set well above every identifier
45/// shape the motivating use case generates (a ULID is 26 characters, a
46/// hyphenated UUID 36, a path-like `turn/17/alt/3` shorter still) and well
47/// below anything that would make a `branches` listing unreadable.
48pub const MAX_BRANCH_ID: usize = 128;
49
50/// A validated lineage name.
51///
52/// # This is not [`ModelName`](crate::vector::ModelName)'s reason, and the rule
53/// is deliberately different
54///
55/// `ModelName` exists because a model name is spliced into a table identifier
56/// and SQLite cannot bind an identifier as a parameter, so the validation is
57/// what makes the splice safe. **None of that applies here.** A `branch_id`
58/// reaches SQL as a bound value at every one of its call sites; there is no
59/// splice to protect. Copying `ModelName`'s `[a-z][a-z0-9_]*` rule would be
60/// borrowing a justification that does not hold, and it would reject the two
61/// name shapes the use case in §15.5 actually produces — a hyphenated UUID and
62/// a path-like turn id.
63///
64/// What this type is for is narrower and has nothing to do with SQL:
65///
66/// * `branch_id` is a primary key that four ledger tables hold a foreign key
67///   into, and `branches` is append-only under two unconditional triggers
68///   ([`CREATE_BRANCHES_GUARD_UPDATE`](crate::schema::ddl::CREATE_BRANCHES_GUARD_UPDATE)).
69///   A name is therefore written **once** and can never be corrected — not by
70///   the crate, not by raw SQL. Validation has one chance, and this is it.
71/// * A name that differs from the caller's intent by a trailing space is not a
72///   typo, it is a **second lineage that reads as the first**. Every subsequent
73///   `on_branch("release ")` resolves to a different ancestry than
74///   `on_branch("release")`, both succeed, and neither reports anything. That
75///   is the failure shape this wave keeps finding, and it is cheapest to refuse
76///   at [D-034]'s boundary.
77///
78/// So the rule is: non-empty, at most [`MAX_BRANCH_ID`] bytes, no ASCII control
79/// characters, and no leading or trailing whitespace. Refused rather than
80/// trimmed, on §4.1's principle — a silent repair here becomes two lineages
81/// sharing one intent later.
82///
83/// # `main` is constructible and not forkable
84///
85/// [`Database::branches`](crate::Database::branches) returns the trunk and
86/// every read may name it, so `BranchId::new("main")` must succeed. What is
87/// refused is *creating* it a second time, and that refusal belongs to
88/// [`Database::fork`](crate::Database::fork) rather than to this type: the
89/// trunk is a lineage like any other to a reader, and only a writer has cause
90/// to care that it already exists.
91///
92/// [D-034]: ../../docs/architecture/s13-decision-register.md#d-034
93#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub struct BranchId(String);
95
96impl BranchId {
97    /// Validate `raw` as a lineage name, or explain why it is not one.
98    pub fn new(raw: impl AsRef<str>) -> Result<Self> {
99        let raw = raw.as_ref();
100        let invalid = || DbError::InvalidBranchId(raw.to_string());
101
102        if raw.is_empty() || raw.len() > MAX_BRANCH_ID {
103            return Err(invalid());
104        }
105        if raw.chars().any(|c| c.is_control()) {
106            return Err(invalid());
107        }
108        // `trim` and not `trim_ascii`: a non-breaking space is invisible in
109        // every terminal this name will be read in, which is the whole argument
110        // above for refusing the ASCII one.
111        if raw.trim() != raw {
112            return Err(invalid());
113        }
114        Ok(Self(raw.to_string()))
115    }
116
117    /// The trunk, which every database has from its first migration.
118    pub fn main() -> Self {
119        Self(ddl::MAIN_BRANCH.to_string())
120    }
121
122    /// Adopt a name already stored in `branches`, without revalidating it.
123    ///
124    /// Infallible on purpose. `branches` may hold rows this type never saw:
125    /// written by raw SQL, or by a build older than 0.14.7, both of which the
126    /// schema permits and neither of which the append-only guards allow anyone
127    /// to repair. A listing that returned `Err` on one such row would be
128    /// unusable for the one thing it is for — finding out what is in there —
129    /// and would report the *listing* as broken rather than the row.
130    pub(crate) fn from_stored(raw: impl Into<String>) -> Self {
131        Self(raw.into())
132    }
133
134    /// The name, as it is stored.
135    pub fn as_str(&self) -> &str {
136        &self.0
137    }
138
139    /// Whether this names the trunk.
140    pub fn is_main(&self) -> bool {
141        self.0 == ddl::MAIN_BRANCH
142    }
143}
144
145impl std::fmt::Display for BranchId {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.write_str(&self.0)
148    }
149}
150
151impl AsRef<str> for BranchId {
152    fn as_ref(&self) -> &str {
153        &self.0
154    }
155}
156
157/// So a `BranchId` can be handed straight to the 0.14.4 read surface.
158///
159/// [`TraversalBuilder::on_branch`](crate::graph::TraversalBuilder::on_branch)
160/// and [`query_as_of_edges_on`](crate::temporal::query_as_of_edges_on) take
161/// `impl Into<String>`, and they shipped two releases before this type existed.
162/// This impl is what makes `on_branch(id)` compile rather than
163/// `on_branch(id.as_str())` — additive, and cheaper than widening four
164/// signatures that are already public.
165impl From<BranchId> for String {
166    fn from(id: BranchId) -> Self {
167        id.0
168    }
169}
170
171/// One row of `branches`: a lineage, its parent, and where it was cut.
172///
173/// # `#[non_exhaustive]` costs nothing here, and that is a fact about direction
174///
175/// [`EdgeBelief`](crate::temporal::EdgeBelief) needed a constructor to go with
176/// the attribute, because `save_snapshot` is public and *takes* one — without
177/// `EdgeBelief::new` the attribute would not have made the next field additive,
178/// it would have made a public function uncallable (0.14.5). This type only
179/// ever travels outward: [`Database::branches`](crate::Database::branches)
180/// returns it and nothing public accepts it. So the attribute buys the additive
181/// field and takes nothing back, and no constructor is owed. §15.4's
182/// abandonment arm is the field it is being kept open for.
183#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[non_exhaustive]
185pub struct Branch {
186    /// The lineage's own name.
187    pub id: BranchId,
188    /// The lineage it was cut from, or `None` for the trunk.
189    pub parent: Option<BranchId>,
190    /// The transaction-time instant it was cut at, or `None` for the trunk.
191    ///
192    /// This is the visibility cutoff 0.14.6 reads: the branch sees its parent's
193    /// history up to and including this instant, and nothing the parent records
194    /// after it ([D-223]).
195    ///
196    /// [D-223]: ../../docs/architecture/s13-decision-register.md#d-223
197    pub forked_at: Option<String>,
198    /// When the row was written.
199    ///
200    /// A separate column from `forked_at` rather than a duplicate of it, and
201    /// the two are equal for every branch this release can create. They are
202    /// separate so that forking from a *past* instant is an additive change
203    /// later: a historical fork has a `forked_at` behind its `created_at`, and
204    /// the schema has always allowed it — `CHECK (forked_at <= created_at)`.
205    pub created_at: String,
206}
207
208/// One lineage's handle on the ledger (§15.4, 0.14.9, [D-226]).
209///
210/// A `Database` plus a [`BranchId`], so a caller who forked writes and reads
211/// through the fork instead of naming it at every call. Every operation here
212/// exists on [`Database`](crate::Database) already and takes a lineage there —
213/// **this type buys ergonomics and no capability**, which is what makes it the
214/// last piece of §15.4's first bullet rather than a fifth release of it.
215///
216/// ```no_run
217/// # use std::sync::Arc;
218/// # use macrame::graph::EdgeAssertion;
219/// # use macrame::{Database, BranchId};
220/// # async fn f(db: Arc<Database>) -> macrame::Result<()> {
221/// let alt = db.fork(BranchId::new("turn/17/alt/1")?, BranchId::main()).await?;
222/// let view = db.view(alt.id);
223///
224/// view.assert_edge(EdgeAssertion::new("a", "b", "CITES").valid_from(ts())).await?;
225/// let seen = view.traversal("a").execute_ids(view.read_conn(), ts()).await?;
226/// # Ok(())
227/// # }
228/// # fn ts() -> &'static str { "2020-01-01T00:00:00.000000Z" }
229/// ```
230///
231/// # It holds an `Arc<Database>` and cannot close it
232///
233/// [`Database::close`](crate::Database::close) takes `self` by value, and an
234/// `Arc` cannot give that up while any clone survives. So the borrow is
235/// structural rather than a documented request: a caller who forks a view,
236/// reads it and drops it is not one call away from stopping the actor everyone
237/// else is using. That is why the view is a **separate type** over an
238/// `Arc<Database>` and not a `Database` with a field added, and it is the same
239/// argument [D-203] made when `Database: Clone` was declined — a handle that can
240/// be cloned freely must not carry the right to end the thing it handles.
241///
242/// `Clone` is therefore free of that concern and is derived: the view owns no
243/// lifecycle, so cloning it is cloning an `Arc` and a short string.
244///
245/// # What it does with an assertion that names a lineage
246///
247/// It stamps its own on one that names none, and **refuses** one that names a
248/// different lineage with [`DbError::BranchMismatch`].
249/// Stamping over is the shape a caller building through the view produces and
250/// costs nothing; relabelling a write that already named somewhere else would
251/// discard a belief rather than contradict it. See that variant for the failure
252/// it catches.
253///
254/// [D-203]: ../../docs/architecture/s13-decision-register.md#d-203
255/// [D-226]: ../../docs/architecture/s13-decision-register.md#d-226
256#[derive(Clone)]
257pub struct BranchView {
258    db: std::sync::Arc<crate::Database>,
259    branch: BranchId,
260}
261
262/// Hand-written because [`Database`](crate::Database) is not `Debug` — it owns
263/// a connection, a channel and a clock, none of which prints usefully. What a
264/// reader of this type wants is which lineage against which file, so that is
265/// what it prints.
266impl std::fmt::Debug for BranchView {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        f.debug_struct("BranchView")
269            .field("branch", &self.branch)
270            .field("path", &self.db.path())
271            .finish()
272    }
273}
274
275impl BranchView {
276    /// Bind a lineage to a handle.
277    ///
278    /// Infallible and does no I/O: the name is already validated by
279    /// [`BranchId`], and whether it is *registered* is a question every
280    /// operation on this view asks for itself, answering
281    /// [`DbError::UnknownBranch`] by name. A
282    /// constructor that checked would buy one round trip's worth of earlier
283    /// notice and cost the type its `const`-cheapness, and the check would be
284    /// stale by the next call anyway — `branches` is append-only, but the view
285    /// outlives the answer.
286    pub fn new(db: std::sync::Arc<crate::Database>, branch: BranchId) -> Self {
287        Self { db, branch }
288    }
289
290    /// The lineage this view reads and writes.
291    pub fn id(&self) -> &BranchId {
292        &self.branch
293    }
294
295    /// The handle underneath, for the operations that are not lineage-scoped.
296    ///
297    /// `archive`, `checkpoint`, `verify` and the rest are properties of the
298    /// file rather than of a lineage, so they are reached through here rather
299    /// than duplicated onto a view that would answer the same thing for every
300    /// branch.
301    pub fn database(&self) -> &std::sync::Arc<crate::Database> {
302        &self.db
303    }
304
305    /// The read connection, so a [`TraversalBuilder`] from
306    /// [`Self::traversal`] can be executed without reaching for the handle.
307    ///
308    /// [`TraversalBuilder`]: crate::graph::TraversalBuilder
309    pub fn read_conn(&self) -> &libsql::Connection {
310        self.db.read_conn()
311    }
312
313    /// A traversal already pointed at this lineage.
314    ///
315    /// The one read this type needs to wrap, because everything downstream of
316    /// it — `execute_ids`, `execute`,
317    /// [`load_subgraph_with`](crate::Database::load_subgraph_with) — takes the
318    /// lineage *from the builder*. Seeding it here is therefore the whole of
319    /// the read side rather than a first method of several.
320    pub fn traversal(&self, start_node: impl Into<String>) -> crate::graph::TraversalBuilder {
321        crate::graph::TraversalBuilder::new(start_node).on_branch(self.branch.as_str())
322    }
323
324    /// [`Database::load_subgraph`](crate::Database::load_subgraph) on this
325    /// lineage.
326    ///
327    /// The sugar form has no builder to carry the branch, so it is wrapped;
328    /// `load_subgraph_with` is not, because a builder from [`Self::traversal`]
329    /// already carries it.
330    pub async fn load_subgraph(
331        &self,
332        start_node: &str,
333        max_hops: u32,
334        now_ts: &str,
335        byte_budget: usize,
336    ) -> Result<crate::graph::Subgraph> {
337        self.db
338            .load_subgraph_with(
339                &self
340                    .traversal(start_node)
341                    .max_depth(max_hops as usize)
342                    .min_weight(f64::NEG_INFINITY),
343                now_ts,
344                byte_budget,
345            )
346            .await
347    }
348
349    /// Every edge this lineage believes in at `ts`.
350    #[allow(clippy::type_complexity)]
351    pub async fn query_as_of_edges(
352        &self,
353        ts: &str,
354    ) -> Result<Vec<(String, String, String, String, String)>> {
355        crate::temporal::query_as_of_edges_on(self.read_conn(), ts, Some(self.branch.as_str()))
356            .await
357    }
358
359    /// Assert an edge on this lineage.
360    /// What this lineage believes that `other` does not.
361    ///
362    /// [`Database::diff`](crate::Database::diff) with this view's lineage as
363    /// the first argument. The direction matters and is not symmetric: this
364    /// answers *what did I conclude that they do not know*, which is the
365    /// question the view's holder is in a position to ask.
366    pub async fn diff(&self, other: &BranchId) -> Result<Vec<Divergence>> {
367        diff(self.read_conn(), &self.branch, other).await
368    }
369
370    pub async fn assert_edge(&self, edge: crate::graph::EdgeAssertion) -> Result<()> {
371        self.db.assert_edge(self.claim_edge(edge)?).await
372    }
373
374    /// Retire an edge on this lineage — [`Database::retire_edge_on`] without
375    /// the argument.
376    ///
377    /// An inherited edge is retired by writing this lineage's **own** closed
378    /// row at the ancestor's key; the parent's row is never touched.
379    ///
380    /// [`Database::retire_edge_on`]: crate::Database::retire_edge_on
381    pub async fn retire_edge(
382        &self,
383        source: impl Into<String>,
384        target: impl Into<String>,
385        edge_type: impl Into<String>,
386        valid_from: &str,
387        valid_to: &str,
388    ) -> Result<()> {
389        self.db
390            .retire_edge_on(
391                source,
392                target,
393                edge_type,
394                valid_from,
395                valid_to,
396                self.branch.clone(),
397            )
398            .await
399    }
400
401    /// Mint a concept on this lineage.
402    ///
403    /// A branch **inherits** its parent's concepts and may not restate one:
404    /// `concepts` is keyed by identity, so that is
405    /// [`DbError::CrossLineage`].
406    pub async fn upsert_concept(&self, concept: crate::ConceptUpsert) -> Result<()> {
407        self.db.upsert_concept(self.claim_concept(concept)?).await
408    }
409
410    /// [`Database::write_bulk_atomic`](crate::Database::write_bulk_atomic) with
411    /// every edge on this lineage.
412    pub async fn write_bulk_atomic(
413        &self,
414        edges: Vec<crate::graph::EdgeAssertion>,
415    ) -> Result<usize> {
416        self.db.write_bulk_atomic(self.claim_edges(edges)?).await
417    }
418
419    /// [`Database::bulk_import`](crate::Database::bulk_import) with every edge
420    /// on this lineage.
421    pub async fn bulk_import(
422        &self,
423        edges: Vec<crate::graph::EdgeAssertion>,
424    ) -> crate::error::BulkResult<usize> {
425        let edges = self
426            .claim_edges(edges)
427            .map_err(|cause| crate::error::BulkInterrupted { written: 0, cause })?;
428        self.db.bulk_import(edges).await
429    }
430
431    /// [`Database::write_concepts`](crate::Database::write_concepts) with every
432    /// concept on this lineage.
433    pub async fn write_concepts(
434        &self,
435        concepts: Vec<crate::ConceptUpsert>,
436    ) -> crate::error::BulkResult<usize> {
437        let concepts = concepts
438            .into_iter()
439            .map(|c| self.claim_concept(c))
440            .collect::<Result<Vec<_>>>()
441            .map_err(|cause| crate::error::BulkInterrupted { written: 0, cause })?;
442        self.db.write_concepts(concepts).await
443    }
444
445    /// Refuse a foreign lineage, stamp an unnamed one.
446    fn claim_edge(&self, edge: crate::graph::EdgeAssertion) -> Result<crate::graph::EdgeAssertion> {
447        match &edge.branch {
448            Some(named) if named != &self.branch => Err(DbError::BranchMismatch {
449                view: self.branch.as_str().to_string(),
450                named: named.as_str().to_string(),
451            }),
452            Some(_) => Ok(edge),
453            None => Ok(edge.on_branch(self.branch.clone())),
454        }
455    }
456
457    /// [`Self::claim_edge`] for a batch, refusing on the **first** foreign
458    /// lineage rather than reporting all of them.
459    ///
460    /// A batch that names two lineages is a caller error about the batch, not a
461    /// list of independent mistakes, and the first one names the confusion as
462    /// well as the tenth would.
463    fn claim_edges(
464        &self,
465        edges: Vec<crate::graph::EdgeAssertion>,
466    ) -> Result<Vec<crate::graph::EdgeAssertion>> {
467        edges.into_iter().map(|e| self.claim_edge(e)).collect()
468    }
469
470    /// [`Self::claim_edge`] for a concept.
471    fn claim_concept(&self, concept: crate::ConceptUpsert) -> Result<crate::ConceptUpsert> {
472        match &concept.branch {
473            Some(named) if named != &self.branch => Err(DbError::BranchMismatch {
474                view: self.branch.as_str().to_string(),
475                named: named.as_str().to_string(),
476            }),
477            Some(_) => Ok(concept),
478            None => Ok(concept.on_branch(self.branch.clone())),
479        }
480    }
481}
482
483/// Register a lineage, refusing the three things a `CHECK` cannot (§15.2).
484///
485/// Runs inside the write actor, so the three reads and the insert are one turn
486/// against one connection and nothing can register a colliding name between the
487/// check and the write.
488///
489/// # What the schema already refuses, and what is left over
490///
491/// `branches` carries a foreign key on `parent_id`, a primary key on
492/// `branch_id`, and two row-local `CHECK`s. So a missing parent and a duplicate
493/// name would both fail at the engine anyway — they are checked here to be
494/// *named*, because `classify` would otherwise surface a duplicate fork as a
495/// constraint violation naming a column, and the caller's question was about a
496/// branch.
497///
498/// # The third refusal, and the invariant that turned out not to be checkable
499///
500/// [`CREATE_BRANCHES_TABLE`]'s comment left this to `fork()`: *"the fork point
501/// is at or after the parent's creation" is not [row-local], and a `CHECK`
502/// cannot see another row. The cross-row half is `fork()`'s to enforce at
503/// D-034's boundary.* Enforcing it as written **refuses every fork on every
504/// injected-clock database in the crate**, and the reason is not the clock the
505/// test chose. `seed_root_branch` stamps `main.created_at` from
506/// `SystemTime::now()` inside `migrations::run`, which runs *before* the
507/// database's clock is resolved — the floor that clock is raised against is
508/// read from tables the migration has to create first, so the order cannot
509/// simply be swapped. **`branches.created_at` is therefore not on the ledger's
510/// timeline**, and on a [`FakeClock`](crate::util::FakeClock) database it sits
511/// years in the future of every row.
512///
513/// What *is* comparable is `forked_at`: every one of them is issued by this
514/// function from the same clock as every `recorded_at`. So the check is against
515/// the parent's fork point, and the trunk — whose `forked_at` is `NULL` because
516/// it was cut from nothing — constrains nothing, which is right: as far as any
517/// ledger row is concerned the trunk has always existed.
518///
519/// The narrower rule is also the one worth having. It makes fork points
520/// **non-decreasing down any root path**, which is precisely the property
521/// [`ancestry_cte`](crate::graph::lineage) clamps for defensively. The clamp
522/// stays — `branches` accepts raw-SQL rows this function never saw — but for
523/// rows the crate wrote it is now a belt beside braces rather than the only
524/// thing holding the shape.
525///
526/// What it refuses is a fork point earlier than its parent's. That branch would
527/// inherit **nothing whatever from the parent it names** — every row the parent
528/// wrote is after the parent's own fork point, so all of them fall past the
529/// child's cutoff — leaving a lineage whose `parent_id` says one thing and
530/// whose visible history says another. A sibling wearing a child's parent
531/// pointer, and silent about it.
532///
533/// [`CREATE_BRANCHES_TABLE`]: crate::schema::ddl::CREATE_BRANCHES_TABLE
534pub(crate) async fn fork(
535    conn: &libsql::Connection,
536    name: &BranchId,
537    parent: &BranchId,
538    stamp: &str,
539) -> Result<Branch> {
540    // `EXISTS` separately from `forked_at`, because the trunk's `forked_at` is
541    // legitimately NULL and a single nullable column cannot tell "no such
542    // branch" apart from "the root".
543    let mut rows = conn
544        .query(
545            "SELECT (SELECT COUNT(*) FROM branches WHERE branch_id = ?1), \
546                    (SELECT COUNT(*) FROM branches WHERE branch_id = ?2), \
547                    (SELECT forked_at FROM branches WHERE branch_id = ?2)",
548            libsql::params![name.as_str(), parent.as_str()],
549        )
550        .await?;
551    let row = rows.next().await?.ok_or_else(|| {
552        // A three-aggregate SELECT always returns a row; if it did not, the
553        // honest report is that the parent could not be established.
554        DbError::UnknownBranch(parent.as_str().to_string())
555    })?;
556    let taken: i64 = row.get(0)?;
557    let parent_exists: i64 = row.get(1)?;
558    let parent_forked_at: Option<String> = row.get(2)?;
559
560    if taken > 0 {
561        return Err(DbError::BranchExists(name.as_str().to_string()));
562    }
563    if parent_exists == 0 {
564        return Err(DbError::UnknownBranch(parent.as_str().to_string()));
565    }
566    if let Some(parent_forked_at) = parent_forked_at {
567        if stamp < parent_forked_at.as_str() {
568            return Err(DbError::ForkPrecedesParent {
569                branch: name.as_str().to_string(),
570                parent: parent.as_str().to_string(),
571                forked_at: stamp.to_string(),
572                parent_forked_at,
573            });
574        }
575    }
576
577    conn.execute(
578        "INSERT INTO branches (branch_id, parent_id, forked_at, created_at) \
579         VALUES (?1, ?2, ?3, ?3)",
580        libsql::params![name.as_str(), parent.as_str(), stamp],
581    )
582    .await?;
583
584    Ok(Branch {
585        id: name.clone(),
586        parent: Some(parent.clone()),
587        forked_at: Some(stamp.to_string()),
588        created_at: stamp.to_string(),
589    })
590}
591
592/// One belief `a` holds that `b` does not (§15.4, 0.14.11, [D-228]).
593///
594/// Returned by [`Database::diff`](crate::Database::diff), one per edge key on
595/// which the two lineages disagree, ordered by that key. The fields describe
596/// **`a`'s** side; `b`'s side is `diff(b, a)`, which is the same question asked
597/// the other way round.
598///
599/// # `branch_id` is the interesting field
600///
601/// It is the lineage on `a`'s ancestry that actually holds this row, and it is
602/// what separates *what this exploration concluded* from *what it still holds
603/// while the trunk moved on*. When it equals `a`, the divergence is `a`'s own
604/// assertion. When it does not, `a` is retaining an ancestor's belief that `b`
605/// no longer shares — which happens whenever `b` churned the key after `a`
606/// forked, and is exactly the case that makes "the divergence is the set of
607/// rows carrying the branch's own id" false. See [D-228] for the two shapes
608/// that break it.
609///
610/// # No constructor, and no `Eq`
611///
612/// `#[non_exhaustive]` with nothing owed, for [`Branch`]'s reason: this type
613/// only ever travels outward and nothing public accepts one, so the attribute
614/// buys the additive field and takes nothing back.
615///
616/// `weight` is an `f64`, so `Eq`, `Ord` and `Hash` are not derivable. That is
617/// a real difference from [`EdgeBelief`](crate::temporal::EdgeBelief), which
618/// carries no weight and is compared as a canonical form by the snapshot
619/// suite. Ordering here comes from the query instead, on the edge key, so two
620/// diffs of the same pair are still directly comparable.
621///
622/// [D-228]: ../../docs/architecture/s13-decision-register.md#d-228
623#[derive(Debug, Clone, PartialEq, PartialOrd)]
624#[non_exhaustive]
625pub struct Divergence {
626    /// The edge's source concept.
627    pub source_id: String,
628    /// The edge's target concept.
629    pub target_id: String,
630    /// The edge's type.
631    pub edge_type: String,
632    /// When the asserted interval opens — part of the key, not of the belief.
633    pub valid_from: String,
634    /// When `a` believes the interval closes, or the open sentinel.
635    pub valid_to: String,
636    /// The weight `a` believes.
637    pub weight: f64,
638    /// The lineage on `a`'s ancestry holding this row. See the type docs.
639    pub branch_id: String,
640}
641
642/// The beliefs `a` holds that `b` does not (§15.4, 0.14.11, [D-228]).
643///
644/// # What the plan expected, and the two shapes that break it
645///
646/// §15.4 says divergence "is exactly the set of rows carrying the branch's own
647/// id", and that is true only when `b` is `a`'s parent **and has not churned
648/// since the fork**. Two counterexamples, both ordinary:
649///
650/// * `b` retires or reweights an inherited edge after `a` forked. `a` still
651///   sees the pre-fork version — that is the whole of
652///   [D-223](../../docs/architecture/s13-decision-register.md#d-223) — so `a`
653///   believes something `b` does not, and the row carries **`b`'s** id, or the
654///   trunk's, never `a`'s.
655/// * `a` and `b` are siblings and `b` shadow-retires an edge both inherited.
656///   `a` believes it and `b` does not, and the row belongs to their common
657///   ancestor.
658///
659/// A row-provenance answer misses both, and it also *adds* one it should not:
660/// `a` re-asserting an inherited edge at the value it already had writes a row
661/// on `a` and changes no belief. So the answer is a difference of the two
662/// resolved views, which is what the reader already computes for one lineage
663/// at a time, and the cheap characterisation is a special case rather than the
664/// definition.
665///
666/// # Cost, and the narrowing that is recorded rather than built
667///
668/// This is O(ledger): `a`'s visible set is the whole ledger from `a`'s point of
669/// view, so the join has to see all of it. The narrowing is real and known —
670/// keys that both lineages resolve to the *same* lineage are identical by
671/// construction and cannot differ, so only keys held on the symmetric
672/// difference of the two ancestries can — but it is not built here, per F-33:
673/// the shape is understood, the trigger is a branched workload large enough to
674/// notice, and there is no measurement yet that says it is worth the second
675/// query shape.
676///
677/// [D-228]: ../../docs/architecture/s13-decision-register.md#d-228
678pub(crate) async fn diff(
679    conn: &libsql::Connection,
680    a: &BranchId,
681    b: &BranchId,
682) -> Result<Vec<Divergence>> {
683    use crate::graph::lineage::{LineageShape, Lineages};
684
685    // One load, four answers: both shapes and both ancestries (0.15.17,
686    // [D-259](../docs/architecture/s13-decision-register.md#d-259)). This asked
687    // `Lineages::shape` twice until then — two round trips for two of the four.
688    let lineages = Lineages::load(conn).await?;
689    // Both names are checked before any work, and each refusal names its own
690    // lineage rather than the pair — a caller who mistyped one wants to know
691    // which one.
692    let shape = lineages.shape(a.as_str())?;
693    lineages.shape(b.as_str())?;
694    // `Trunk` only: a forked trunk is `TrunkOnForked` and reaches the
695    // statement below, where `diff_sql` lowers both sides `Resolved` — the
696    // third shape is a one-lineage read's saving, and a diff is two.
697    if shape == LineageShape::Trunk {
698        // `Trunk` is `branches` holding one row, and both names were just found
699        // in it, so `a` and `b` are the same lineage and their views are equal
700        // by construction. Exact rather than an optimisation, for the reason
701        // `Lineages::shape` gives about its own sufficient condition — and it is
702        // reached only by `diff(main, main)` on a ledger that never forked.
703        return Ok(Vec::new());
704    }
705
706    // Both sides lower `Resolved`, so both bind an ancestry: `?1` and `?2` name
707    // the lineages, `a`'s block follows, then `b`'s.
708    let (a_anc, b_anc) = (lineages.ancestry(a.as_str()), lineages.ancestry(b.as_str()));
709    let mut params: Vec<libsql::Value> = vec![a.as_str().into(), b.as_str().into()];
710    params.extend(crate::graph::lineage::ancestry_params(&a_anc));
711    params.extend(crate::graph::lineage::ancestry_params(&b_anc));
712    let mut rows = conn
713        .query(&crate::graph::lineage::diff_sql(&a_anc, &b_anc), params)
714        .await?;
715    let mut out = Vec::new();
716    while let Some(row) = rows.next().await? {
717        out.push(Divergence {
718            source_id: row.get(0)?,
719            target_id: row.get(1)?,
720            edge_type: row.get(2)?,
721            valid_from: row.get(3)?,
722            valid_to: row.get(4)?,
723            weight: row.get(5)?,
724            branch_id: row.get(6)?,
725        });
726    }
727    Ok(out)
728}
729
730/// Every lineage the ledger knows about, trunk first, then by creation.
731///
732/// Read through the read connection rather than the actor: `branches` is
733/// append-only, so a listing cannot be torn by a concurrent write in any way a
734/// caller could act on — the worst a racing `fork` can do is not appear yet.
735///
736/// Ordered rather than left to the engine, and ordered by `created_at` rather
737/// than by name, because the useful reading of this list is the shape of the
738/// tree over time. The trunk is pinned first because it is the one row that is
739/// always there and is nobody's child.
740pub(crate) async fn list(conn: &libsql::Connection) -> Result<Vec<Branch>> {
741    let mut rows = conn
742        .query(
743            "SELECT branch_id, parent_id, forked_at, created_at FROM branches \
744             ORDER BY (parent_id IS NOT NULL), created_at, branch_id",
745            (),
746        )
747        .await?;
748    let mut out = Vec::new();
749    while let Some(row) = rows.next().await? {
750        out.push(Branch {
751            id: BranchId::from_stored(row.get::<String>(0)?),
752            parent: row.get::<Option<String>>(1)?.map(BranchId::from_stored),
753            forked_at: row.get(2)?,
754            created_at: row.get(3)?,
755        });
756    }
757    Ok(out)
758}
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763
764    #[test]
765    fn accepts_the_name_shapes_the_use_case_generates() {
766        for ok in [
767            "main",
768            "b9",
769            "01ARZ3NDEKTSV4RRFFQ69G5FAV",           // ULID
770            "3f2504e0-4f89-11d3-9a0c-0305e82c3301", // hyphenated UUID
771            "turn/17/alt/3",                        // path-like turn id
772            "explore Kant's second critique",       // a human sentence
773        ] {
774            assert_eq!(BranchId::new(ok).unwrap().as_str(), ok);
775        }
776    }
777
778    /// The two that matter are the whitespace pair: each is a second lineage
779    /// that reads as the first everywhere it is printed.
780    #[test]
781    fn refuses_what_cannot_be_corrected_later() {
782        for bad in [
783            "",
784            " release", // leading space
785            "release ", // trailing space
786            "release\n",
787            "rel\tease", // control character in the middle
788            "rel\0ease",
789        ] {
790            assert!(
791                BranchId::new(bad).is_err(),
792                "{bad:?} was accepted, and `branches` is append-only"
793            );
794        }
795        assert!(BranchId::new("x".repeat(MAX_BRANCH_ID)).is_ok());
796        assert!(BranchId::new("x".repeat(MAX_BRANCH_ID + 1)).is_err());
797    }
798
799    /// `ModelName`'s rule is the one this type is most likely to be "fixed" to
800    /// match. Pinned so the fix has to argue with a test.
801    #[test]
802    fn the_rule_is_not_model_names_rule() {
803        for accepted_here_rejected_there in ["Release-1", "3f2504e0-4f89", "a.b"] {
804            assert!(BranchId::new(accepted_here_rejected_there).is_ok());
805            assert!(crate::vector::ModelName::new(accepted_here_rejected_there).is_err());
806        }
807    }
808
809    #[test]
810    fn the_trunk_knows_itself() {
811        assert!(BranchId::main().is_main());
812        assert!(BranchId::new("main").unwrap().is_main());
813        assert!(!BranchId::new("mains").unwrap().is_main());
814    }
815}