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