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}