Skip to main content

macrame/graph/
lineage.rs

1//! Reading one lineage's belief out of a ledger that holds several (§15.2,
2//! §15.3, D-219, D-220, D-223).
3//!
4//! # Three shapes, and the measurements that forced them
5//!
6//! `links_current` is keyed `(source_id, target_id, edge_type, valid_from,
7//! branch_id)` since v12, so a branch that corrects or retires an edge it
8//! inherited writes its **own** row beside the ancestor's rather than over it.
9//! Reading a lineage therefore means picking one row per edge key: the one
10//! belonging to the **nearest** branch on the path from the reader to the root.
11//!
12//! The naive alternative — admit every row whose `branch_id` is anywhere on the
13//! path — is not a resolution and `branch_traversal_probe` §4b measures what it
14//! costs. A branch that retires an inherited edge by shadowing it gets the
15//! *whole subtree back*: 1,111 nodes where the resolved read gives 1,000. The
16//! union form does not merely report a stale weight, it discards retirement.
17//!
18//! Resolution is not free and does not become free when there is nothing to
19//! resolve. Measured on a single-lineage database — every database this crate
20//! has ever written — the resolved traversal is **3.0×** the unresolved one,
21//! because a window function is opaque to the planner and it cannot see that
22//! every partition holds exactly one row. So the read path picks a shape, the
23//! way `temporal::replay::cold_lineage` picks one at the archive boundary
24//! (D-216): [`LineageShape::Trunk`] emits today's SQL, [`LineageShape::Resolved`]
25//! emits the ancestry join.
26//!
27//! The third shape is the trunk's, on a database that has forked
28//! ([`LineageShape::TrunkOnForked`], 0.15.2, D-244). Once `branches` holds
29//! two rows the trunk's read was `Resolved` like everyone else's, and paid
30//! the hybrid's fixed cost — D-223 measured 1.45× at zero churn, and the
31//! trunk has zero churn *structurally*: it has no ancestors, so no cutoff
32//! and no churned set. Its resolved read reduces to its own rows, and the
33//! third shape emits that reduction as one predicate on `branch_id`. It is
34//! the escalation D-223 named — the naive filter, emitted when the answer
35//! to *does any ancestor hold a post-cutoff row* is no — taken where the
36//! answer is no by construction rather than by probing.
37//!
38//! # Why one lineage is a sufficient condition for the fast shape
39//!
40//! `branch_id` is `NOT NULL DEFAULT 'main' REFERENCES branches(branch_id)` on
41//! every ledger table (D-215), and the key is real. So a `branches` table
42//! holding one row is a database in which every ledger row reads `'main'` — not
43//! by convention but because nothing else could have been stored. The ancestry
44//! of `main` is `{main}`, every partition has one member, and the resolved form
45//! and the plain form return the same rows by construction. The check is
46//! therefore exact rather than a heuristic, which is what makes it safe to skip
47//! the work rather than merely cheap.
48//!
49//! # The fork point is a visibility cutoff, and one filter cannot apply it
50//!
51//! 0.14.4 resolved *which lineage holds an edge* and never looked at
52//! `branches.forked_at`, so a branch kept absorbing writes its parent made
53//! after the fork. §15.3 says the opposite in as many words — rows written on
54//! each ancestor **before the fork point on the path down from A** — and
55//! `ddl`'s own comment calls `forked_at` "what §15.3's visibility cutoffs are
56//! computed over". Nothing computed them. 0.14.6 does (D-223).
57//!
58//! **The finding is not that a filter was missing.** `links_current` answers
59//! *current as of now* and structurally cannot answer *current as of t*: the
60//! projection holds one belief per key per lineage, and
61//! `trg_links_current_sync` is `ON CONFLICT … DO UPDATE … recorded_at =
62//! excluded.recorded_at`. So the moment the trunk reweights or retires an edge
63//! after the fork, the pre-fork version is **not in the table at all**, and its
64//! only home is `transaction_log`. Adding `recorded_at <= cutoff` to the
65//! existing read would therefore not show the branch its inherited edge — it
66//! would make that edge *vanish*, which is wrong in a new and quieter way.
67//! Every "just add the filter" instinct fails for that one reason.
68//!
69//! So the read is a **hybrid**: [`links_cut_cte`] takes the `links_current`
70//! rows a lineage may still see directly, and folds the log for exactly the
71//! keys where it may not. The fold arm's cost scales with **post-fork churn on
72//! the ancestors**, not with history size, and the untouched keys — the common
73//! case, because a fork exists to diverge from a trunk that mostly stands still
74//! — stay on the projection.
75//!
76//! **The two arms are disjoint by construction, and that is why [`churned_cte`]
77//! is defined over `links_current` rather than over the log.** One predicate on
78//! one row decides which arm emits a given `(edge key, lineage)`:
79//! `recorded_at <= cutoff` sends it to the projection arm, `>` sends it to the
80//! fold arm. Deriving the churned set from `transaction_log` instead would have
81//! been a cheaper seek — `idx_txlog_time` is a range index and the projection
82//! has none on `recorded_at` — but disjointness would then be an argument about
83//! what the archive does and does not remove, rather than a property of a
84//! single comparison. Two arms emitting one key would put two rows at the same
85//! `dist` into [`visible_cte`]'s partition, where the winner is whichever the
86//! engine happened to order first.
87//!
88//! **Where this degrades, named rather than left to be found.** The fold arm
89//! reads the *hot* log, and `LOG_ARCHIVABLE` archives any entry superseded by a
90//! later one for the same entity. A pre-fork assertion superseded by a
91//! post-fork correction is superseded, so once the retention horizon passes the
92//! fork point that entry can be cold — and then the branch loses an edge its
93//! ancestor churned. That is §3.2's already-carried `AtTime` degradation
94//! reached from the branch side, not a new class of loss, and it is bounded to
95//! churned keys: the projection arm never degrades, because `links_current` is
96//! re-derived from surviving `links` rather than deleted from. It is
97//! deliberately **not** guarded by `check_recorded_reach`, and the reason has
98//! changed shape without changing sides (0.15.4, D-246). It used to be that the
99//! guard's bit was coarse — any archive at all flipped it, so guarding here
100//! would have refused every branched read on every archived database. The guard
101//! is now scoped to the instants the archive really took, which removes that
102//! objection and leaves the smaller one: `links_cut` reads the log for a *fork
103//! point*, not for a belief, and a fork point that has gone cold is a different
104//! question from an instant that has. A cold arm for `links_cut` is the fix if
105//! it is ever needed, and it belongs with §3.2 rather than with the cutoff.
106
107use crate::error::{DbError, Result};
108use crate::graph::plan::{lower, Resolution};
109use crate::schema::ddl;
110
111/// Which form of the read to emit. See the module docs.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub(crate) enum LineageShape {
114    /// One lineage exists, so there is nothing to resolve and the plain SQL is
115    /// exact. Not an optimisation with a correctness caveat — see the module
116    /// docs for why the condition is sufficient.
117    Trunk,
118    /// More than one lineage exists. The read resolves along the ancestry.
119    Resolved,
120    /// More than one lineage exists and the reader is a **root** — the
121    /// trunk, on every database this crate can write (0.15.2, D-244).
122    ///
123    /// A root has no ancestors, so its ancestry is one row with no cutoff,
124    /// its churned set is empty by construction, and the resolved read
125    /// reduces exactly to *its own rows*: `links_current WHERE branch_id =
126    /// ?` under current belief, and the fold over its own log entries at a
127    /// recorded instant. This shape emits that reduction directly. It is
128    /// the third shape [D-223] named as the escalation — "the naive filter
129    /// emitted when no ancestor holds a post-cutoff row" — taken at the
130    /// one lineage for which the condition is structural rather than
131    /// measured, and it is what stops the trunk paying for the branches
132    /// (review C-7). It **binds** the branch, unlike `Trunk`, because its
133    /// SQL names it.
134    ///
135    /// [D-223]: ../../docs/architecture/s13-decision-register.md#d-223
136    TrunkOnForked,
137}
138
139impl LineageShape {
140    /// Whether the emitted SQL names the reading branch at all.
141    ///
142    /// `Trunk` is the one shape that does not: there is one lineage and
143    /// nothing to name. Every placeholder layout that puts the branch at a
144    /// slot asks this rather than comparing against `Resolved`, so that a
145    /// shape added later binds correctly by default.
146    pub(crate) fn binds_branch(self) -> bool {
147        !matches!(self, LineageShape::Trunk)
148    }
149}
150
151/// The shape, and the ancestry the shape needs — one round trip for both.
152///
153/// This is what `lineage_shape` became (0.15.17, [D-259]). Where that asked
154/// SQLite for three aggregates, this loads the rows and answers from them;
155/// measured, that is **10.0 µs against 11.0**
156/// (`examples/ancestry_resolve_probe.rs` §5), so the ancestry arrives for less
157/// than the shape alone used to cost and nothing has to be cached to afford it.
158///
159/// The ancestry is empty under the two trunk shapes. Neither emits a `lineage`
160/// relation, so resolving one would be a walk whose result no SQL names — and
161/// an empty slice is what makes [`crate::graph::plan::lower`] able to ignore
162/// the field rather than branch on whether it was populated.
163///
164/// A caller wanting the question and not the answer goes through [`Lineages`]
165/// directly: `branch::diff` loads the register once and asks [`Lineages::shape`]
166/// per name, which refuses an unregistered lineage before either side is
167/// lowered — and is one round trip for both, where a free function per name was
168/// two.
169///
170/// [D-259]: ../../docs/architecture/s13-decision-register.md#d-259
171pub(crate) async fn resolve_for(
172    conn: &libsql::Connection,
173    branch: Option<&str>,
174) -> Result<(LineageShape, Vec<Ancestor>)> {
175    let named = branch.unwrap_or(ddl::MAIN_BRANCH);
176    let lineages = Lineages::load(conn).await?;
177    let shape = lineages.shape(named)?;
178    let ancestry = match shape {
179        LineageShape::Resolved => lineages.ancestry(named),
180        _ => Vec::new(),
181    };
182    Ok((shape, ancestry))
183}
184
185/// One lineage's row in `branches`, as the resolution needs it.
186///
187/// Three columns and not the table: `created_at` is the audit column and no
188/// read resolves over it, so loading it would be bytes moved for nothing.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub(crate) struct BranchRow {
191    pub(crate) id: String,
192    pub(crate) parent: Option<String>,
193    pub(crate) forked_at: Option<String>,
194}
195
196/// Every lineage the database holds — the table [`resolve`] walks.
197///
198/// A `Vec` and a linear scan, for the reason `distinct_branches` gives: the
199/// bound is the number of lineages, which is small and human-authored, and a
200/// map would allocate to index a list that is usually of length one.
201///
202/// # Why this is loaded per read rather than cached (0.15.17, [D-259])
203///
204/// [A-2] proposed a cached `Vec<Branch>` with a generation counter, on the
205/// premise that resolving ancestry in Rust needs the *rows* where the shape
206/// needed only three aggregates, and that the extra read has to be paid for.
207/// Measured (`examples/ancestry_resolve_probe.rs`, §5), it does not: loading 17
208/// rows costs **10.0 µs** against the three-aggregate `SELECT`'s **11.0 µs**,
209/// so the rows arrive for *less* than the answer they replace. The cache is an
210/// optimisation nothing has asked for, and a cache the read side does not need
211/// is a coherency question the read side does not have to answer.
212///
213/// The actor keeps its copy ([`crate::connection`]'s `ActorState`) because the
214/// write path already had one and invalidates it on the two commands that write
215/// `branches`. That is a cache with an owner; this is not.
216///
217/// [A-2]: ../../docs/Macrame%20Codebase%20Review%20v0.15.0.md
218/// [D-259]: ../../docs/architecture/s13-decision-register.md#d-259
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub(crate) struct Lineages {
221    pub(crate) rows: Vec<BranchRow>,
222}
223
224impl Lineages {
225    /// Load `branches`, in no particular order.
226    ///
227    /// Unordered because every consumer here searches by name or walks parent
228    /// links, and neither cares; `Database::branches` is the listing that
229    /// promises trunk-first and it sorts for itself.
230    pub(crate) async fn load(conn: &libsql::Connection) -> Result<Self> {
231        let mut rows = conn
232            .query("SELECT branch_id, parent_id, forked_at FROM branches", ())
233            .await?;
234        let mut out = Vec::with_capacity(1);
235        while let Some(row) = rows.next().await? {
236            out.push(BranchRow {
237                id: row.get::<String>(0)?,
238                parent: row.get::<Option<String>>(1)?,
239                forked_at: row.get::<Option<String>>(2)?,
240            });
241        }
242        Ok(Self { rows: out })
243    }
244
245    fn find(&self, name: &str) -> Option<&BranchRow> {
246        self.rows.iter().find(|b| b.id == name)
247    }
248
249    /// The shape for one name, or [`DbError::UnknownBranch`].
250    ///
251    /// The same three facts `lineage_shape`'s `SELECT` asked for until 0.15.17 — the total,
252    /// the name's existence, and whether it is a root — read off the rows
253    /// instead of counted by SQLite.
254    /// # Refusing an unregistered lineage rather than answering for the trunk
255    ///
256    /// A read naming a branch that is not in `branches` has asked a question
257    /// about something that does not exist. Answering it with the trunk's view
258    /// would be the [D-069] failure — a right-looking answer to a question that
259    /// was not asked — and it is the answer a caller is *least* able to detect,
260    /// because on a database that has never forked the trunk's view is what
261    /// they expected to see anyway.
262    ///
263    /// The refusal is [`DbError::UnknownBranch`] from 0.14.7 and was
264    /// [`DbError::NotFound`] before it, whose `Display` reads *"node {0} not
265    /// found"* — the wrong noun, pointing a caller at their concept ids. There
266    /// was no better variant until `fork()` needed one, and shipping the right
267    /// variant while leaving this on the old one would have been two spellings
268    /// of one fact.
269    ///
270    /// [D-069]: ../../docs/architecture/s13-decision-register.md
271    pub(crate) fn shape(&self, name: &str) -> Result<LineageShape> {
272        let row = self
273            .find(name)
274            .ok_or_else(|| DbError::UnknownBranch(name.to_string()))?;
275        Ok(if self.rows.len() <= 1 {
276            LineageShape::Trunk
277        } else if row.parent.is_none() {
278            LineageShape::TrunkOnForked
279        } else {
280            LineageShape::Resolved
281        })
282    }
283
284    /// The shape a batch takes, given every lineage it names.
285    ///
286    /// Every name is checked, because that is the first thing the caller wants:
287    /// a batch naming a lineage that does not exist is refused **by name**,
288    /// rather than by whatever the guard finds when it looks in the wrong place.
289    ///
290    /// # Why the last answer used to be every answer, and why it is not one now
291    ///
292    /// Before 0.15.2 the shape was a function of the row *count* alone, so
293    /// asking per name and keeping the last was correct and read like a bug —
294    /// review C-24. [`LineageShape::TrunkOnForked`] (D-244) made the shape a
295    /// function of the **name** as well: a root and a fork on one database now
296    /// have different shapes, and the loop went on keeping whichever came last.
297    /// Where the names disagree the resolved form is the one exact for all of
298    /// them, roots included, so the ambiguity is resolved rather than
299    /// tie-broken by iteration order.
300    pub(crate) fn shape_of(&self, names: &[&str]) -> Result<LineageShape> {
301        let mut agreed: Option<LineageShape> = None;
302        for name in names {
303            let shape = self.shape(name)?;
304            agreed = Some(match agreed {
305                None => shape,
306                Some(prev) if prev == shape => prev,
307                Some(_) => LineageShape::Resolved,
308            });
309        }
310        // No names is the trunk: `distinct_branches` never returns empty, and
311        // `write_concepts_atomic` refuses an empty chunk before it gets here.
312        Ok(agreed.unwrap_or(LineageShape::Trunk))
313    }
314
315    /// [`resolve`], against these rows.
316    pub(crate) fn ancestry(&self, start: &str) -> Vec<Ancestor> {
317        resolve(&self.rows, start)
318    }
319}
320
321/// One resolved ancestor: exactly the three columns the recursive
322/// `ancestry_cte` produced until 0.15.17.
323///
324/// Public through [`crate::branch`] as the answer `reconstruct_on` resolves
325/// over, which is review C-10: until 0.15.17 the resolution rule existed only
326/// as SQL, so a caller holding a `Vec<EdgeBelief>` had no function to finish
327/// the question the fold started.
328///
329/// # Usually read, occasionally built (0.15.17, [D-255])
330///
331/// `#[non_exhaustive]`, and [`new`](Ancestor::new) is the way in — a fourth
332/// column is plausible (the fork's own `created_at` has been wanted twice) and
333/// a struct literal outside this crate would make it a major version.
334///
335/// Almost every caller *reads* one: [`Database::ancestry`] resolves it out of
336/// `branches`, refusing an unregistered lineage first, and
337/// [`resolve_beliefs`](crate::temporal::resolve_beliefs) consumes what it
338/// returned. The constructor exists for the caller who is exercising that pure
339/// function rather than a database, which is a real case — it is what this
340/// crate's own test for it does — and is worth naming, because an ancestry
341/// assembled by hand is a distance rule the caller has stated and
342/// `resolve_beliefs` will apply as faithfully as it applies a resolved one.
343///
344/// [D-255]: ../../docs/architecture/s13-decision-register.md#d-255
345/// [`Database::ancestry`]: crate::Database::ancestry
346#[derive(Debug, Clone, PartialEq, Eq)]
347#[non_exhaustive]
348pub struct Ancestor {
349    /// The lineage.
350    pub branch_id: String,
351    /// Steps from the reader: 0 is the reader itself.
352    pub dist: i64,
353    /// The instant past which this ancestor's rows are not visible, or `None`
354    /// for the reader, which has no cutoff.
355    pub cutoff: Option<String>,
356}
357
358impl Ancestor {
359    /// One ancestor at `dist` steps up the parent chain, uncut.
360    ///
361    /// `cutoff` is `None`, which is the reader's own row and the only shape
362    /// with no fork point above it; [`cutoff`](Ancestor::cutoff) sets it for
363    /// the rest. Two arguments and a setter rather than three arguments,
364    /// because the third is an `Option` that is `None` on exactly one row of
365    /// every ancestry and a positional `None` at every call site reads as a
366    /// decision nobody made.
367    ///
368    /// Building one states a distance rule rather than reporting a resolved
369    /// one. [`crate::Database::ancestry`] is what to use against a real ledger.
370    pub fn new(branch_id: impl Into<String>, dist: i64) -> Self {
371        Self {
372            branch_id: branch_id.into(),
373            dist,
374            cutoff: None,
375        }
376    }
377
378    /// Cut this ancestor at `ts`.
379    pub fn cutoff(mut self, ts: impl Into<String>) -> Self {
380        self.cutoff = Some(ts.into());
381        self
382    }
383}
384
385/// Walk `branches` from `start` to its root, carrying the running minimum.
386///
387/// The Rust half of [D-259]: the same relation [`ancestry_cte`] computed with
388/// `WITH RECURSIVE`, computed here instead. Term for term — the reader at
389/// `dist` 0 with no cutoff, then one row per ancestor, each carrying the
390/// **minimum** `forked_at` seen on the path down to it.
391///
392/// See [`ancestry_cte`] for why the cutoff is the *child's* `forked_at` and why
393/// the minimum is running rather than assigned. The property that matters here
394/// is that this is a second implementation of a rule the crate already had, so
395/// it is pinned against the original differentially rather than against a
396/// restatement of the rule (`the_rust_walk_agrees_with_the_cte`, below).
397///
398/// # Termination without trusting the data
399///
400/// `branches.parent_id` is a foreign key into an append-only table whose parent
401/// must exist before its child, so the graph is a forest and the walk is finite.
402/// The loop is bounded by the row count anyway. That bound is not the
403/// termination argument — it is what stops a corrupted file from hanging a read,
404/// which is a different failure from the one the schema rules out, and the walk
405/// returns the prefix it had rather than raising: a caller reading a broken
406/// `branches` gets a narrower answer, not a panic.
407///
408/// [D-259]: ../../docs/architecture/s13-decision-register.md#d-259
409pub(crate) fn resolve(rows: &[BranchRow], start: &str) -> Vec<Ancestor> {
410    let mut out = Vec::new();
411    let mut cur = start.to_string();
412    let mut cutoff: Option<String> = None;
413    for dist in 0..=(rows.len() as i64) {
414        out.push(Ancestor {
415            branch_id: cur.clone(),
416            dist,
417            cutoff: cutoff.clone(),
418        });
419        let Some(node) = rows.iter().find(|b| b.id == cur) else {
420            break;
421        };
422        // The CHECK pairs these two, so a row with one and not the other is a
423        // file that has been edited outside this crate. Treated as the root.
424        let (Some(parent), Some(forked)) = (node.parent.as_ref(), node.forked_at.as_ref()) else {
425            break;
426        };
427        cutoff = Some(match cutoff {
428            Some(c) if c.as_str() <= forked.as_str() => c,
429            _ => forked.clone(),
430        });
431        cur = parent.clone();
432    }
433    out
434}
435
436/// The ancestry as a bound `VALUES` table, replacing the recursive CTE.
437///
438/// `first_slot` is where the block starts; it occupies `3 × rows` placeholders
439/// from there, and every reader puts it **after** its own fixed slots so that no
440/// existing layout moves.
441///
442/// # Bound and not interpolated
443///
444/// A branch id is caller-supplied text. The crate has exactly one arbitrary-SQL
445/// surface ([D-258]) and this is not a second one, so every value binds — the
446/// cutoff too, `NULL` included, which libSQL accepts inside `VALUES`
447/// (`ancestry_resolve_probe.rs` §1 checks it rather than assuming it). The
448/// consequence is that the statement *text* varies with ancestry **depth** and
449/// with nothing else, so a prepared-statement cache keyed on text sees one entry
450/// per distinct fork depth rather than one per lineage.
451///
452/// `dist` binds too, though it is the row's own index and could be a literal.
453/// Measured (`ancestry_resolve_probe.rs` §7) that saves a third of the
454/// placeholders and **1–5%**, inside the noise, so it is spelled the way the
455/// other two columns are rather than differently for a gain that did not
456/// survive being measured.
457///
458/// [D-258]: ../../docs/architecture/s13-decision-register.md#d-258
459pub(crate) fn ancestry_values(rows: usize, first_slot: usize, tag: &str) -> String {
460    let tuples: Vec<String> = (0..rows)
461        .map(|i| {
462            let b = first_slot + i * 3;
463            format!("(?{}, ?{}, ?{})", b, b + 1, b + 2)
464        })
465        .collect();
466    format!(
467        "lineage{tag}(branch_id, dist, cutoff) AS (VALUES {})",
468        tuples.join(", ")
469    )
470}
471
472/// The ancestry's placeholder values, in the order [`ancestry_values`] names
473/// them.
474pub(crate) fn ancestry_params(ancestry: &[Ancestor]) -> Vec<libsql::Value> {
475    let mut v = Vec::with_capacity(ancestry.len() * 3);
476    for a in ancestry {
477        v.push(libsql::Value::Text(a.branch_id.clone()));
478        v.push(libsql::Value::Integer(a.dist));
479        v.push(match &a.cutoff {
480            Some(c) => libsql::Value::Text(c.clone()),
481            None => libsql::Value::Null,
482        });
483    }
484    v
485}
486
487/// The ancestry of the reading branch, nearest first, each with its cutoff.
488///
489/// `dist` is what makes this a resolution rather than a union: it is the
490/// distance from the reader to each ancestor, and the row a lineage sees is the
491/// one belonging to the smallest `dist` holding that edge key.
492///
493/// The recursion terminates on `parent_id IS NULL`, which the `branches` CHECK
494/// pairs with `forked_at IS NULL` so that exactly one row — the root — can end
495/// it. A cycle is not representable: `parent_id` is a foreign key into a table
496/// whose rows are append-only and whose parent must already exist when the child
497/// is written, so the graph is a forest by construction and the walk is finite
498/// without a depth bound.
499/// `slot` is where the reading branch binds, and it is passed rather than
500/// spelled: the placeholder layout belongs to
501/// [`TraversalBuilder`](crate::graph::TraversalBuilder), which computes it once
502/// so that no two call sites can agree on it separately (D-030, D-035).
503///
504/// # `cutoff`, and why it is the child's `forked_at` rather than the row's
505///
506/// §15.3: a read on B sees rows written on each ancestor A *before the fork
507/// point on the path down from A*. The fork point on the path down from A is
508/// where **A's child on that path** diverged — so stepping from a branch to its
509/// parent, the parent's cutoff is the stepping branch's own `forked_at`. The
510/// reader itself has no cutoff at all, which is `NULL` in the anchor row and
511/// the only `NULL` the column ever holds.
512///
513/// It is a running minimum rather than a plain assignment. `b` forks from `a`
514/// at *t₂* and `a` from `main` at *t₁*, so `b` sees `main` as of *t₁* and not
515/// as of *t₂* — inheritance composes, and each step can only narrow the window.
516/// With `forked_at` monotone down a chain the `min` never fires; it is here
517/// because the schema does not enforce that ordering and a read should not be
518/// the thing that discovers it isn't.
519///
520/// # The equivalence that makes the fold arm safe
521///
522/// Under these cutoffs, **nearest-ancestor-wins and latest-`recorded_at`-wins
523/// coincide**: each ancestor's visible window ends where its descendant's
524/// begins, so the deepest lineage member holding a pre-cutoff row for a key is
525/// also the one holding the newest such row. That is why
526/// [`links_cut_cte`]'s fold arm may bound per lineage with a plain
527/// `ROW_NUMBER() … PARTITION BY entity_id, branch_id` and hand the cross-lineage
528/// question to [`visible_cte`], with no tiebreak between ancestors anywhere.
529///
530/// Stated as a consequence of the write path, not as a theorem about the
531/// schema: it holds because a branch's own writes follow its fork, which
532/// `fork()` and branch-scoped writes guarantee and no CHECK does. The
533/// resolution still orders by `dist`, which is the definition; the equivalence
534/// is what says the fold cannot disagree with it.
535///
536/// # `tag`, and the one query that needs two of these (0.14.11, [D-228])
537///
538/// Every read before `diff` resolved **one** lineage, so the four CTEs could
539/// take their names as constants. A diff resolves two in one statement — it
540/// has to, because two statements compare two snapshots and can report a
541/// difference that never existed — and SQLite has one namespace per `WITH`
542/// list. So each name here takes a suffix, and every caller that resolves one
543/// lineage passes `""` and emits exactly the text it emitted before.
544///
545/// A suffix rather than a second copy of the hybrid, for the reason
546/// [D-227](../../docs/architecture/s13-decision-register.md#d-227) gave when it
547/// declined to hand-write the cutoff into `query_as_of_edges_on`: the two arms
548/// of [`links_cut_cte`] must partition, and that is a property of one
549/// comparison written once, not an argument to restate in a second place.
550///
551/// [D-228]: ../../docs/architecture/s13-decision-register.md#d-228
552/// The three placeholders that fix one edge key, when the reader has one.
553///
554/// The write path always does: [`crate::connection`]'s overlap guard and its
555/// retirement both hold a `(source, target, edge_type)` before any SQL exists,
556/// and asking the resolution about the whole projection to then discard all but
557/// one key would make a branched bulk write O(rows) per row.
558///
559/// Narrowing is **not** a filter appended to the resolved relation. It is
560/// pushed into the base scans of [`churned_cte`] and [`links_cut_cte`], where
561/// `idx_lc_open_interval` leads with exactly these three columns and each arm
562/// becomes a seek (0.14.8, [D-225]; lowered 0.15.8, W13.3, [D-250]).
563///
564/// It also decides one column. A keyed read is a *write* path read, and the
565/// write path carries `properties` through the resolution because
566/// [`retire_from_resolved`] restates it on the shadow row; an
567/// unkeyed traversal does not, and carrying a JSON blob through a window over
568/// the whole projection is not a cost a reader should pay for a column it never
569/// selects.
570///
571/// [D-225]: ../../docs/architecture/s13-decision-register.md#d-225
572/// [D-250]: ../../docs/architecture/s13-decision-register.md#d-250
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub(crate) struct KeySlots {
575    pub source: usize,
576    pub target: usize,
577    pub edge_type: usize,
578}
579
580impl KeySlots {
581    /// The three equalities, on `alias`, as one line of a `WHERE`.
582    pub(crate) fn equalities(&self, alias: &str) -> String {
583        format!(
584            "{alias}.source_id = ?{} AND {alias}.target_id = ?{} AND {alias}.edge_type = ?{}",
585            self.source, self.target, self.edge_type
586        )
587    }
588}
589
590/// # Retained as the oracle, not as production SQL (0.15.17, [D-259])
591///
592/// Nothing emits this any more — [`ancestry_values`] does, with the same three
593/// columns computed by [`resolve`] instead of by SQLite. It is compiled for
594/// tests only, and it stays because deleting it would leave
595/// [`resolve`] pinned against a *restatement* of the rule rather than against
596/// the implementation it replaced. `the_rust_walk_agrees_with_the_cte` is the
597/// differential test; this is the half of it that cannot be wrong by the same
598/// mistake.
599///
600/// [D-259]: ../../docs/architecture/s13-decision-register.md#d-259
601#[cfg(test)]
602pub(crate) fn ancestry_cte(slot: usize, tag: &str) -> String {
603    format!(
604        r#"lineage{tag}(branch_id, dist, cutoff) AS (
605    SELECT ?{slot}, 0, NULL
606    UNION ALL
607    SELECT b.parent_id, g.dist + 1,
608           CASE WHEN g.cutoff IS NULL OR b.forked_at < g.cutoff
609                THEN b.forked_at ELSE g.cutoff END
610    FROM branches b JOIN lineage{tag} g ON b.branch_id = g.branch_id
611    WHERE b.parent_id IS NOT NULL
612)"#
613    )
614}
615
616/// The `(edge key, lineage)` pairs whose projected row is *younger* than the
617/// cutoff, and therefore cannot be shown to the reader.
618///
619/// One row here means: this ancestor holds a belief about this edge, and it
620/// wrote that belief after the reader's line diverged. `links_current` has
621/// overwritten whatever it believed before — the sync trigger's `DO UPDATE`
622/// carries `recorded_at` forward — so the pre-fork version has to come from the
623/// log. [`links_cut_cte`] is what goes and gets it.
624///
625/// **`entity_id` is composed here in the same order the log triggers compose
626/// it**, `source|target|type|valid_from`, because that is the key the fold
627/// joins on. It is a second spelling of a format the schema owns, and the thing
628/// that keeps it honest is that a mismatch cannot be quiet: the join would
629/// match nothing, every churned key would drop out of both arms, and
630/// `branch_read_tests`' retire and reweight cases would go red together.
631///
632/// The `cutoff IS NOT NULL` clause is what makes this empty for a read on the
633/// root — `main` has no ancestors and no cutoff, so a forked database still
634/// pays nothing here to read its own trunk.
635pub(crate) fn churned_cte(tag: &str, key: Option<KeySlots>) -> String {
636    // Keyed, the three equalities go first and the base scan becomes a seek on
637    // `idx_lc_open_interval`. Composed from the *columns* either way rather
638    // than from the key's own placeholders: the rows are already narrowed to
639    // that key, so the two spellings hold the same string, and one of them is
640    // the spelling the unkeyed arm has to use anyway.
641    let narrow = match key {
642        Some(k) => format!("{} AND ", k.equalities("lc")),
643        None => String::new(),
644    };
645    format!(
646        r#"churned{tag}(entity_id, branch_id, cutoff) AS (
647    SELECT lc.source_id || '|' || lc.target_id || '|' || lc.edge_type || '|' || lc.valid_from,
648           lc.branch_id, g.cutoff
649    FROM links_current lc
650    JOIN lineage{tag} g ON g.branch_id = lc.branch_id
651    WHERE {narrow}g.cutoff IS NOT NULL AND lc.recorded_at > g.cutoff
652)"#
653    )
654}
655
656/// `links_current` as each lineage on the ancestry was entitled to see it.
657///
658/// The hybrid §15.3 did not have a name for, entered there as option (4). Two
659/// arms over one predicate:
660///
661/// * rows whose lineage may still show them directly — the reader's own
662///   (`cutoff IS NULL`) and every ancestor row recorded at or before its cutoff;
663/// * for the rest, the last log entry that lineage wrote at or before its
664///   cutoff, which is what `links_current` held for that key at the fork.
665///
666/// A key the ancestor first asserted *after* the cutoff contributes nothing
667/// from either arm, and that is correct rather than a gap: the branch's line
668/// diverged before that edge existed, so there is no pre-fork row to resurrect.
669/// The fold returning empty is the answer.
670///
671/// **`UNION ALL` is sound because the arms partition, not because duplicates
672/// are harmless.** They split `links_current ⋈ lineage` on `recorded_at <=
673/// cutoff`; see [`churned_cte`] for why the churned set is derived from the
674/// projection and not from the log, which is the same point from the other
675/// side.
676///
677/// The column list matches `links_current`'s and `links_at_tx`'s exactly, which
678/// is what lets [`visible_cte`] reduce any of the three without knowing which.
679///
680/// # The log arm's join order, which is four words and was worth 1,387x
681/// (0.15.29, [D-272])
682///
683/// The `CROSS JOIN` is not a different join. It is the same inner join with
684/// the loops nailed down, which is what `CROSS` means to SQLite and the only
685/// thing it means: *do not reorder this*.
686///
687/// Left to choose, the planner drove from `transaction_log` on the one
688/// equality it could see — `table_name = 'links'` — and inlined `churned` as
689/// the inner loop, where the key narrowing and the `recorded_at > cutoff`
690/// bound both fell out of the access path:
691///
692/// ```text
693/// SEARCH transaction_log USING INDEX idx_txlog_fold_partition (table_name=?)
694/// SEARCH lc USING COVERING INDEX idx_lc_lineage_cut (branch_id=?)
695/// ```
696///
697/// One column bound on the inner scan, so the whole of that lineage's
698/// `links_current` per row of the links log, per execution of this relation.
699/// [`crate::connection`]'s write path executes it **once per asserted row**,
700/// which is how a 200-edge batch on a fork of a 2,000-edge trunk came to cost
701/// **68 s against the trunk's 26 ms** — and 20x that for a 4x trunk, because
702/// the shape is a product of two things that both grow.
703///
704/// With `churned` driving, the log is reached on three bound columns instead
705/// of one:
706///
707/// ```text
708/// SEARCH lc USING COVERING INDEX idx_lc_lineage_cut (branch_id=? AND recorded_at>?)
709/// SEARCH transaction_log USING INDEX idx_txlog_fold_partition
710///        (table_name=? AND entity_id=? AND branch_id=?)
711/// ```
712///
713/// **`idx_txlog_fold_partition` is not the villain and did not move.** Its
714/// leading `table_name` is what the planner grabbed, but the index serves this
715/// arm better than the `idx_txlog_entity (entity_id=?)` seek the arm had
716/// before [D-254] existed — three equality columns rather than one. It was
717/// being used with a third of itself bound.
718///
719/// **Why reordering cannot change the answer.** `links_current`'s primary key
720/// is `(source_id, target_id, edge_type, valid_from, branch_id)` and
721/// [`churned_cte`] composes `entity_id` from the first four, so `churned` is
722/// unique on the `(entity_id, branch_id)` pair this join matches. The join is
723/// one-to-many in exactly one direction whichever side drives, so the window
724/// below sees the same input rows and `ROW_NUMBER` picks the same one.
725///
726/// **This arm is not only the write path's.** `key` is `None` for every
727/// branched *current-belief read*, where the churned set is the whole of it
728/// rather than one edge, and the same plan was there. Measured through the
729/// public API on a fixture with deliberate post-fork churn
730/// (`examples/resolved_read_probe.rs`, best of 40, 400 concepts):
731///
732/// ```text
733///                                   depth 1        depth 8
734/// edges(plan), current belief    5.6 -> 1.3 ms  13.6 -> 1.3 ms
735/// traverse, depth 6              5.4 -> 1.2 ms  13.7 -> 1.4 ms
736/// edges(plan), recorded instant  1.87 -> 1.90   2.43 -> 2.44   (no arm here)
737/// edges(plan) on the trunk       0.33 -> 0.33   0.33 -> 0.33   [control]
738/// ```
739///
740/// The branched read **stopped growing with fork depth**, which is the durable
741/// half of that table: depth multiplies the ancestry, the ancestry multiplied
742/// the churned set, and the churned set was being re-derived per log row.
743///
744/// The gate is `the_log_arm_is_driven_by_the_churned_set` below, and it pins
745/// the bound column rather than a millisecond — [D-055] is why.
746///
747/// [D-254]: ../../docs/architecture/s13-decision-register.md#d-254
748/// [D-272]: ../../docs/architecture/s13-decision-register.md#d-272
749/// [D-055]: ../../docs/architecture/s13-decision-register.md#d-055
750pub(crate) fn links_cut_cte(tag: &str, key: Option<KeySlots>) -> String {
751    // The narrowing goes on the **projection** arm only. The log arm joins
752    // `churned{tag}`, which is already one key when this one is, so repeating
753    // the equalities there would narrow nothing and read as though it did.
754    //
755    // The parentheses around the cutoff disjunction are the whole reason this
756    // is built rather than concatenated: `A AND B OR C` is `(A AND B) OR C` in
757    // SQL, so appending the key to this arm without them would return every
758    // ancestor's pre-cutoff row for every edge in the ledger.
759    let (narrow, props, log_props) = match key {
760        Some(k) => (
761            format!(
762                "{} AND (g.cutoff IS NULL OR lc.recorded_at <= g.cutoff)",
763                k.equalities("lc")
764            ),
765            ", properties",
766            "\n           json_extract(payload, '$.properties'),",
767        ),
768        None => (
769            "g.cutoff IS NULL OR lc.recorded_at <= g.cutoff".to_string(),
770            "",
771            "",
772        ),
773    };
774    let carried = if key.is_some() {
775        "lc.weight, lc.properties, lc.branch_id"
776    } else {
777        "lc.weight, lc.branch_id"
778    };
779    format!(
780        r#"links_cut{tag}(source_id, target_id, edge_type, valid_from, valid_to, weight{props}, branch_id) AS (
781    SELECT lc.source_id, lc.target_id, lc.edge_type, lc.valid_from, lc.valid_to,
782           {carried}
783    FROM links_current lc
784    JOIN lineage{tag} g ON g.branch_id = lc.branch_id
785    WHERE {narrow}
786    UNION ALL
787    SELECT json_extract(payload, '$.source_id'),
788           json_extract(payload, '$.target_id'),
789           json_extract(payload, '$.edge_type'),
790           json_extract(payload, '$.valid_from'),
791           json_extract(payload, '$.valid_to'),
792           json_extract(payload, '$.weight'),{log_props}
793           branch_id
794    FROM (
795        SELECT transaction_log.payload, transaction_log.branch_id,
796               ROW_NUMBER() OVER (
797                   PARTITION BY transaction_log.entity_id, transaction_log.branch_id
798                   ORDER BY transaction_log.seq_id DESC
799               ) AS rn
800        FROM churned{tag} k
801        -- CROSS, so `churned` drives. See this function's rustdoc: the same
802        -- join with the loops the other way round is O(log rows x lineage
803        -- rows) per execution, and the write path executes it per asserted
804        -- row (0.15.29, D-272).
805        CROSS JOIN transaction_log ON transaction_log.entity_id = k.entity_id
806                      AND transaction_log.branch_id = k.branch_id
807        WHERE transaction_log.table_name = 'links'
808          AND transaction_log.recorded_at <= k.cutoff
809    ) WHERE rn = 1
810)"#
811    )
812}
813
814/// The slots the write path binds, which are fixed by its two statements
815/// rather than by a layout type (0.14.8, D-225).
816///
817/// `?1` source, `?2` target, `?3` edge type, `?4` the `valid_from` each tail
818/// selects on, `?5` the writing lineage, and for the retirement `?6` the new
819/// `valid_to` and `?7` the stamp. Both callers bind all of them positionally
820/// at one site each.
821const WRITE_KEY: KeySlots = KeySlots {
822    source: 1,
823    target: 2,
824    edge_type: 3,
825};
826const WRITE_BRANCH_SLOT: usize = 5;
827
828/// Where the guard's ancestry block starts: after the five it already binds.
829///
830/// The two write statements have different layouts, so the slot is passed to
831/// [`write_resolution`] rather than named once here — the guard binds five
832/// (`key`, `valid_from`, `branch`) and the retirement binds seven.
833const GUARD_ANCESTRY_SLOT: usize = 6;
834
835/// Where the retirement's ancestry block starts: after `valid_to` and `stamp`.
836const RETIRE_ANCESTRY_SLOT: usize = 8;
837
838/// What the write path has decided before any SQL exists.
839///
840/// Current belief always: the guard asks what this lineage believes *now*, and
841/// an assertion is made against now. There is no recorded slot to name.
842fn write_resolution<'a>(
843    shape: LineageShape,
844    ancestry: &'a [Ancestor],
845    ancestry_slot: usize,
846) -> Resolution<'a> {
847    Resolution {
848        shape,
849        branch_slot: WRITE_BRANCH_SLOT,
850        recorded_slot: None,
851        tag: "",
852        key: Some(WRITE_KEY),
853        ancestry,
854        ancestry_slot,
855    }
856}
857
858/// Overlap candidates as the writing lineage can see them (0.14.8, §15.4,
859/// [D-225]; lowered 0.15.8, W13.3, [D-250]).
860///
861/// # Why the write path needs a resolution at all
862///
863/// [`crate::connection`]'s overlap guard reads `links_current` for the edge key
864/// being asserted and refuses an assertion whose valid-time interval overlaps
865/// one already recorded (defect AA, D-060). Until 0.14.8 every row in the table
866/// was `main`'s, so reading the key with no lineage predicate was exact. The
867/// moment a second lineage can write, the same statement is wrong in **both
868/// directions at once**: a branch would be refused for overlapping its parent's
869/// belief that it is entitled to supersede, and the trunk would be refused for
870/// overlapping a branch's belief it cannot even see. An unfiltered read is not
871/// a conservative approximation of a filtered one here; it is a different
872/// question.
873///
874/// Adding `AND branch_id = ?` would fix the trunk direction and leave the
875/// branch one wrong the other way — a branch would then be checked against
876/// *only its own* rows and could assert `[10,20)` over an inherited `[5,15)`,
877/// putting two overlapping intervals into its own view. That is defect AA
878/// reintroduced across lineages, and it is the shape
879/// `trg_links_single_open`'s v12 comment left open as "§15.4's write-path
880/// question": the trigger sees one row and cannot answer it. This is the
881/// answer. **What a lineage may not overlap is what that lineage can see**,
882/// which is the read's definition and now the write's.
883///
884/// # It was a second spelling of that definition until 0.15.8
885///
886/// The narrowing was real and is unchanged — [`churned_cte`] and
887/// [`links_cut_cte`] are written for a traversal and scan `links_current`
888/// whole, so calling them per assertion would make a branched bulk write
889/// O(rows) per row, and pushing the key into the base scans turns each arm
890/// into a seek on `idx_lc_open_interval`. What was not real was the *copy*:
891/// a `key_visibility_cte` holding its own `lineage`, its own churned set, its
892/// own two-arm hybrid and its own nearest-lineage window, four relations that
893/// had to keep agreeing with four in [`crate::graph::plan`] and were kept
894/// honest only by `branch_write_tests` asserting the two answers match on one
895/// fixture. [D-227](../../docs/architecture/s13-decision-register.md#d-227) is
896/// four releases of what happens when a reader spells its own. The key is a
897/// [`KeySlots`] on [`Resolution`] now, and this function is the lowering plus
898/// one line.
899///
900/// # What that cost, and bought, on `examples/edge_write_probe`
901///
902/// Best of 500 `assert_edge` calls, three runs each, release build:
903///
904/// ```text
905///           0.15.7    0.15.8
906/// trunk    0.0958    0.0966 ms   unchanged
907/// forked   0.1044    0.0975 ms   -6.6%
908/// branch   0.1059    0.1091 ms   +3.0%
909/// ```
910///
911/// The forked trunk gains because it stopped taking the resolved form: it was
912/// exact there only because a root's ancestry is itself, and D-248's C-24
913/// repair is what lets [`crate::graph::LineageShape`] tell a root apart from a
914/// branch at all. It now lowers to a two-predicate lookup on `links_current`.
915///
916/// The branch loses because the shared [`visible_cte`] joins `lineage` to order
917/// by `dist`, where the deleted `key_visibility_cte` carried `dist` through its
918/// own relations and needed no join. Buying that 3% back means giving the keyed
919/// spelling its own `dist` column in three functions — a second shape for the
920/// hybrid, decided by the caller — which is the divergence this release exists
921/// to remove, over a join against a materialised ancestry of two rows.
922///
923/// **The `churned` base scan is unchanged and was never the cost.** It planned
924/// as `SEARCH lc USING COVERING INDEX idx_lc_lineage_cut (branch_id=? AND
925/// recorded_at>?)` in 0.15.7 and it plans that way now: SQLite inlines the
926/// key-narrowed CTE into each use, so the equalities and the `recorded_at`
927/// range meet in one scan either way. Splitting them apart with
928/// `AS MATERIALIZED` does restore the key seek, and costs more than it saves —
929/// the log arm then loses `SEARCH transaction_log USING INDEX idx_txlog_entity
930/// (entity_id=?)` and scans the whole log, because a materialised `churned` is
931/// no longer a small driving set the planner can see through. It was measured
932/// and not taken.
933///
934/// **The middle sentence of that paragraph stopped being true at 0.15.12 and
935/// was true again at 0.15.29** ([D-272]). `idx_txlog_fold_partition` ([D-254])
936/// gave the planner a reason to drive from `transaction_log` instead, and the
937/// churned base scan then planned as `idx_lc_lineage_cut (branch_id=?)` —
938/// **one** column, not the two the paragraph reports — because it had become
939/// an inner loop. The paragraph is left as written because what it says about
940/// 0.15.7 and 0.15.8 is still what happened; what it could not know is that
941/// this plan was a *choice* the planner was free to revisit, and it did. It is
942/// nailed down now: see [`links_cut_cte`]'s `CROSS JOIN`.
943///
944/// The refusal of `AS MATERIALIZED` survives that and is now refused twice
945/// over. Re-measured against the current schema
946/// (`examples/branch_write_guard_probe.rs`, per asserted row): materialised
947/// alone is **0.158 ms** at a 2,000-edge trunk and **0.565 ms** at 8,000,
948/// against the forced join order's **0.012** and **0.016** — it fixes the
949/// symptom partially and the growth not at all, because a materialised
950/// `churned` is still the inner loop.
951///
952/// # The predicate set, which is three equalities and nothing else
953///
954/// `valid_from <> ?4` excludes the row being re-asserted: re-assertion at the
955/// same `valid_from` is Doctrine III's ordinary case and is settled by the
956/// primary key and the single-open trigger, not by this guard.
957///
958/// **The "and nothing else" was measured, not assumed**, and it is the half of
959/// this statement a lowering must not quietly improve. The first version added
960/// `AND valid_from < :new_valid_to`, a provably safe narrowing — overlap
961/// requires `max(start) < min(end)`, so an interval starting at or after the
962/// new one's end cannot overlap it. It cost **9.8 ms on a 90-edge chunk into a
963/// 2,000-edge hub**, because it walked the planner straight into D-059's trap:
964///
965/// ```text
966/// with the range:     SEARCH links_current USING COVERING INDEX
967///                     idx_lc_traversal_cover (source_id=? AND valid_from<?)
968/// without it:         SEARCH links_current USING COVERING INDEX
969///                     idx_lc_open_interval (source_id=? AND target_id=? AND edge_type=?)
970/// ```
971///
972/// `idx_lc_traversal_cover` leads on `(source_id, valid_from, …)` and contains
973/// every column that query mentions, so with a `valid_from` range available it
974/// wins as a covering index while binding **one** equality column — and the
975/// guard scans the source's entire out-degree. Same shape as the defect D-059
976/// diagnosed in `trg_links_single_open`, reintroduced by an optimisation one
977/// wave after it was fixed. Three equalities make it a point lookup that
978/// `idx_lc_open_interval` serves exactly, and the rows it returns are a version
979/// count rather than an out-degree. **A narrowing predicate is not free if it
980/// changes the plan** — which is also why [`KeySlots`] pushes its equalities
981/// into the base scans rather than appending them to the resolved relation,
982/// and why `index_plan_tests` pins this statement's plan on every shape.
983///
984/// [D-225]: ../../docs/architecture/s13-decision-register.md#d-225
985/// [D-250]: ../../docs/architecture/s13-decision-register.md#d-250
986pub(crate) fn overlap_candidates_resolved(shape: LineageShape, ancestry: &[Ancestor]) -> String {
987    let l = lower(&write_resolution(shape, ancestry, GUARD_ANCESTRY_SLOT));
988    format!(
989        "{}SELECT l.valid_from, l.valid_to FROM {} l WHERE l.valid_from <> ?4{}",
990        l.with_clause(),
991        l.source,
992        l.filter
993    )
994}
995
996/// The row a branch is retiring, which may belong to an ancestor.
997///
998/// Retirement on a branch is **shadow retirement**: the branch writes its own
999/// row at the ancestor's key carrying a closed interval, the read prefers it by
1000/// `dist`, and the ancestor's row is untouched. [`visible_cte`]'s rustdoc has
1001/// described this write since 0.14.4; this is it. Closing the ancestor's own
1002/// row is the parent corruption
1003/// [Doctrine III](../../docs/architecture/s0-s3-foundations.md#doctrine-iii)
1004/// forbids, and it is not merely avoided by policy — `links` is append-only and
1005/// there is no statement in the crate that could do it.
1006///
1007/// `?6` is the new `valid_to`, `?7` the stamp. `weight` and `properties` are
1008/// carried from the visible row rather than restated, which is what makes this
1009/// a retirement rather than a new assertion that happens to be closed — and it
1010/// is the reason a keyed resolution carries `properties` at all (see
1011/// [`KeySlots`]).
1012pub(crate) fn retire_from_resolved(shape: LineageShape, ancestry: &[Ancestor]) -> String {
1013    let l = lower(&write_resolution(shape, ancestry, RETIRE_ANCESTRY_SLOT));
1014    format!(
1015        "{}INSERT INTO links \
1016             (source_id, target_id, edge_type, valid_from, valid_to, weight, \
1017              properties, recorded_at, branch_id) \
1018         SELECT ?1, ?2, ?3, ?4, ?6, l.weight, l.properties, ?7, ?5 \
1019         FROM {} l WHERE l.valid_from = ?4{}",
1020        l.with_clause(),
1021        l.source,
1022        l.filter
1023    )
1024}
1025
1026/// What one lineage believes and another does not, in **one** statement
1027/// (0.14.11, §15.4, [D-228]).
1028///
1029/// # Why one statement rather than two reads and a difference in Rust
1030///
1031/// Not for the round trip. A diff is a *comparison*, and two statements
1032/// against [`Database::read_conn`](crate::Database::read_conn) are two
1033/// snapshots — a write landing between them can make the answer report a
1034/// difference that never existed at any instant. The obvious repair, a read
1035/// transaction, is not available: `read_conn()` is public and shared, so
1036/// beginning one inside a library call would change what every other holder of
1037/// that connection sees. One statement gets the single snapshot for free.
1038///
1039/// # Why the CTEs are tagged rather than copied
1040///
1041/// This resolves two lineages, so it needs two of each of the four CTEs, and
1042/// SQLite has one namespace per `WITH` list. Hence the `tag` parameter on
1043/// [`ancestry_cte`] and its three companions rather than a second spelling of
1044/// the hybrid — which [D-227](../../docs/architecture/s13-decision-register.md#d-227)
1045/// declined for `query_as_of_edges_on` and would be the same mistake here.
1046/// Every single-lineage caller passes `""` and emits the text it always did.
1047///
1048/// # What it compares, and what it does not
1049///
1050/// A `LEFT JOIN` on the **edge key** — `(source, target, type, valid_from)` —
1051/// and a row survives when `b` holds no belief about that key, or holds one
1052/// whose interval or weight differs. So a retirement is reported: `a`'s row is
1053/// the closed one, `b`'s is open, and they differ. That is the case a
1054/// valid-time filter would have hidden, which is why there is no `ts` here at
1055/// all — a diff filtered to an instant cannot see the one divergence that is
1056/// *about* an instant having passed.
1057///
1058/// **`properties` is not compared**, and that is a limit rather than an
1059/// oversight: no read surface in the crate returns edge properties —
1060/// `EdgeAssertion::properties` writes them and nothing reads them back — so a
1061/// diff reporting a change there would be the only reader resolving a column,
1062/// and it would name a difference the caller has no way to look at. It is the
1063/// first thing to widen if edge properties ever become readable.
1064///
1065/// Float equality on `weight` is deliberate. The question is whether `b` holds
1066/// the *same belief*, and a belief is a stored value; an epsilon here would
1067/// invent a tolerance the ledger does not have and would make `diff(a, b)`
1068/// disagree with what a traversal on either lineage shows.
1069///
1070/// Slots: `?1` the lineage being asked about, `?2` the one it is compared to.
1071///
1072/// [D-228]: ../../docs/architecture/s13-decision-register.md#d-228
1073pub(crate) fn diff_sql(a_ancestry: &[Ancestor], b_ancestry: &[Ancestor]) -> String {
1074    // Two lowerings in one `WITH` list (0.15.1, W13.1): the same prelude
1075    // the traversal and `query_as_of_edges_on` splice, told apart by tag.
1076    //
1077    // Two ancestry blocks as well, and they are the reason this function stopped
1078    // being a constant in 0.15.17: `?1` and `?2` still name the two lineages,
1079    // `a`'s ancestry follows at `?3`, and `b`'s follows that. Both lengths are
1080    // read from the database, so the caller resolves before it lowers.
1081    let a = lower(&Resolution {
1082        shape: LineageShape::Resolved,
1083        branch_slot: 1,
1084        recorded_slot: None,
1085        tag: "_a",
1086        key: None,
1087        ancestry: a_ancestry,
1088        ancestry_slot: 3,
1089    });
1090    let b = lower(&Resolution {
1091        shape: LineageShape::Resolved,
1092        branch_slot: 2,
1093        recorded_slot: None,
1094        tag: "_b",
1095        key: None,
1096        ancestry: b_ancestry,
1097        ancestry_slot: 3 + a_ancestry.len() * 3,
1098    });
1099    format!(
1100        "WITH RECURSIVE {},\n{}\n\
1101         SELECT a.source_id, a.target_id, a.edge_type, a.valid_from, \
1102                a.valid_to, a.weight, a.branch_id \
1103         FROM {} a \
1104         LEFT JOIN {} b ON b.source_id  = a.source_id \
1105                              AND b.target_id  = a.target_id \
1106                              AND b.edge_type  = a.edge_type \
1107                              AND b.valid_from = a.valid_from \
1108         WHERE b.source_id IS NULL \
1109            OR b.valid_to <> a.valid_to \
1110            OR b.weight   <> a.weight \
1111         ORDER BY a.source_id, a.target_id, a.edge_type, a.valid_from",
1112        a.with_list(),
1113        b.with_list(),
1114        a.source,
1115        b.source,
1116    )
1117}
1118
1119/// One row per edge key, from the nearest lineage that holds it.
1120///
1121/// `source` is the relation to resolve — [`links_cut_cte`] under current
1122/// belief, or the `links_at_tx` fold when the traversal names a transaction-time
1123/// instant. Both expose the same columns under the same names, which is what
1124/// lets this be written once, and both have already applied the ancestry's
1125/// cutoffs before this sees them.
1126///
1127/// **The partition is the edge key and not the edge.** Two lineages asserting
1128/// the same `(source, target, type)` at *different* `valid_from` are two
1129/// assertions in valid time and stay two rows; two lineages asserting at the
1130/// same `valid_from` are one edge believed twice and resolve to one. That is
1131/// also what makes shadow-retirement work: a branch writes its own row at the
1132/// ancestor's key with a closed interval, the resolution prefers it, and the
1133/// edge is gone from that lineage's view while the ancestor's row is untouched.
1134/// Closing the ancestor's own row is the parent corruption
1135/// [Doctrine III](../../docs/architecture/s0-s3-foundations.md#doctrine-iii)
1136/// forbids, and shadowing is the only retirement across lineages that does not
1137/// commit it.
1138///
1139/// **`ORDER BY g.dist` is a total order over the surviving rows**, because each
1140/// source contributes at most one row per `(edge key, lineage)` and each
1141/// lineage appears in `lineage` once. See [`ancestry_cte`] for why ordering by
1142/// `recorded_at` instead would pick the same row.
1143pub(crate) fn visible_cte(source: &str, tag: &str, key: Option<KeySlots>) -> String {
1144    // The partition stays the whole edge key even when three quarters of it is
1145    // a constant: it is the same rows either way, and a partition that changed
1146    // shape with the caller would be a second definition of what one edge is.
1147    let props = if key.is_some() { ", properties" } else { "" };
1148    let carried = if key.is_some() {
1149        "l.properties, l.branch_id,"
1150    } else {
1151        "l.branch_id,"
1152    };
1153    format!(
1154        r#"visible{tag}(source_id, target_id, edge_type, valid_from, valid_to, weight{props}, branch_id) AS (
1155    SELECT source_id, target_id, edge_type, valid_from, valid_to, weight{props}, branch_id FROM (
1156        SELECT l.source_id, l.target_id, l.edge_type, l.valid_from, l.valid_to, l.weight,
1157               {carried}
1158               ROW_NUMBER() OVER (
1159                   PARTITION BY l.source_id, l.target_id, l.edge_type, l.valid_from
1160                   ORDER BY g.dist
1161               ) AS rn
1162        FROM {source} l
1163        JOIN lineage{tag} g ON g.branch_id = l.branch_id
1164    ) WHERE rn = 1
1165)"#
1166    )
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171    use super::*;
1172
1173    /// The reader plus one ancestor: the smallest ancestry that resolves
1174    /// anything. An empty one lowers to `VALUES ()`, which SQLite refuses.
1175    fn anc() -> Vec<Ancestor> {
1176        vec![
1177            Ancestor {
1178                branch_id: "exp".to_string(),
1179                dist: 0,
1180                cutoff: None,
1181            },
1182            Ancestor {
1183                branch_id: "main".to_string(),
1184                dist: 1,
1185                cutoff: Some("2026-01-06T00:00:00.000000Z".to_string()),
1186            },
1187        ]
1188    }
1189
1190    const TS: &str = "2026-01-06T00:00:00.000000Z";
1191
1192    async fn fresh() -> libsql::Connection {
1193        let db = libsql::Builder::new_local(":memory:")
1194            .build()
1195            .await
1196            .unwrap();
1197        let conn = db.connect().unwrap();
1198        crate::schema::run_migrations(&conn).await.unwrap();
1199        conn
1200    }
1201
1202    async fn fork(conn: &libsql::Connection, child: &str, parent: &str) {
1203        conn.execute(
1204            "INSERT INTO branches (branch_id, parent_id, forked_at, created_at) \
1205             VALUES (?1, ?2, ?3, ?3)",
1206            libsql::params![child, parent, TS],
1207        )
1208        .await
1209        .unwrap();
1210    }
1211
1212    /// **The guard seeks the edge key on every shape it can be given.**
1213    ///
1214    /// D-060's overlap guard fell into D-059's trap one wave after D-059 fixed
1215    /// it: a provably safe `AND valid_from < :new_valid_to` handed the planner
1216    /// a range, `idx_lc_traversal_cover` won as a covering index while binding
1217    /// **one** column, and the guard scanned the source's whole out-degree —
1218    /// **+9.8 ms** on a 90-edge chunk into a 2,000-edge hub, invisible to every
1219    /// correctness test because the rows returned were right.
1220    ///
1221    /// Until 0.15.8 that was pinned in `migration_tests` against a *hand-copied*
1222    /// reproduction of `OVERLAP_CANDIDATES`, which is the weakest form of this
1223    /// test: it pins a string next to the code rather than the code. The
1224    /// statements are generated now ([`overlap_candidates_resolved`],
1225    /// [`retire_from_resolved`]), so the plan is taken from the bytes the guard
1226    /// will prepare — on all three shapes, including the two the old pin could
1227    /// not reach because it predated them.
1228    ///
1229    /// **Three columns bound is the assertion**, not the index name: the
1230    /// resolved shape reaches `links_current` through four CTEs and the trunk
1231    /// shapes reach it directly, so which index serves the seek differs, and
1232    /// what must not differ is that the seek is on the whole key. One column
1233    /// bound is O(out-degree) wherever it appears.
1234    #[tokio::test]
1235    async fn the_guard_seeks_the_edge_key_on_every_shape() {
1236        let conn = fresh().await;
1237        fork(&conn, "exp", "main").await;
1238
1239        for shape in [
1240            LineageShape::Trunk,
1241            LineageShape::TrunkOnForked,
1242            LineageShape::Resolved,
1243        ] {
1244            for (what, sql) in [
1245                ("overlap", overlap_candidates_resolved(shape, &anc())),
1246                ("retire", retire_from_resolved(shape, &anc())),
1247            ] {
1248                let mut rows = conn
1249                    .query(&format!("EXPLAIN QUERY PLAN {sql}"), ())
1250                    .await
1251                    .unwrap();
1252                let mut plan = Vec::new();
1253                while let Some(r) = rows.next().await.unwrap() {
1254                    plan.push(r.get::<String>(3).unwrap());
1255                }
1256                let step = plan.join(" | ");
1257
1258                assert!(
1259                    step.contains("source_id=? AND target_id=? AND edge_type=?"),
1260                    "{shape:?}/{what} binds fewer columns than the key has, so \
1261                     it scans the source's out-degree — D-059 in D-060's \
1262                     guard: {step}"
1263                );
1264                // The overlap read has no `valid_from` equality to fall back
1265                // on, so it is the one that needs the index built for it.
1266                assert!(
1267                    what != "overlap" || step.contains("idx_lc_open_interval"),
1268                    "{shape:?} overlap is off the index added for it: {step}"
1269                );
1270            }
1271        }
1272    }
1273
1274    /// **The log arm is driven by the churned set, not by the whole links
1275    /// log** (0.15.29, [D-272]).
1276    ///
1277    /// Sibling of the test above and the same kind of claim: not *which* index
1278    /// serves the seek, but how much of it is bound. The arm joins
1279    /// `transaction_log` to `churned` on `(entity_id, branch_id)` under
1280    /// `table_name = 'links'`. Driven from `churned`, that is three equality
1281    /// columns and a seek per churned row. Driven from the log — which is what
1282    /// the planner chose for seventeen releases — it is `(table_name=?)`, the
1283    /// whole links log, with `churned` re-derived inside it as a one-column
1284    /// scan of the lineage's `links_current`. The write path runs this once per
1285    /// asserted row, so the second shape is a 200-edge batch costing 68 s
1286    /// against the trunk's 26 ms.
1287    ///
1288    /// `entity_id=?` in the plan is therefore the assertion. It cannot be
1289    /// present unless `churned` is on the outside, and it is what the
1290    /// `CROSS JOIN` in [`links_cut_cte`] exists to guarantee — so reverting
1291    /// those four words makes this red, which is how the gate was checked.
1292    ///
1293    /// **The trunk shapes are asserted to have no arm at all**, because a
1294    /// repair to a relation two of the three shapes never emit would otherwise
1295    /// look like it had been verified on all of them.
1296    ///
1297    /// The fixture is empty and that is deliberate: with no rows and no
1298    /// statistics the planner has nothing but the query text to go on, which is
1299    /// the weakest position the pin can be held from. A pin that needs a
1300    /// populated table is a pin that can be satisfied by a distribution.
1301    ///
1302    /// [D-272]: ../../docs/architecture/s13-decision-register.md#d-272
1303    #[tokio::test]
1304    async fn the_log_arm_is_driven_by_the_churned_set() {
1305        let conn = fresh().await;
1306        fork(&conn, "exp", "main").await;
1307
1308        for shape in [
1309            LineageShape::Trunk,
1310            LineageShape::TrunkOnForked,
1311            LineageShape::Resolved,
1312        ] {
1313            let sql = overlap_candidates_resolved(shape, &anc());
1314            let mut rows = conn
1315                .query(&format!("EXPLAIN QUERY PLAN {sql}"), ())
1316                .await
1317                .unwrap();
1318            let mut plan = Vec::new();
1319            while let Some(r) = rows.next().await.unwrap() {
1320                plan.push(r.get::<String>(3).unwrap());
1321            }
1322            let step = plan.join(" | ");
1323
1324            let arm: Vec<&String> = plan
1325                .iter()
1326                .filter(|s| s.contains("transaction_log"))
1327                .collect();
1328
1329            if shape == LineageShape::Resolved {
1330                assert_eq!(
1331                    arm.len(),
1332                    1,
1333                    "the resolved guard reads the log once and only in the \
1334                     fold arm: {step}"
1335                );
1336                assert!(
1337                    arm[0].contains("entity_id=?"),
1338                    "the log arm is not seeking the churned key, so the whole \
1339                     links log is being read once per asserted row: {step}"
1340                );
1341            } else {
1342                assert!(
1343                    arm.is_empty(),
1344                    "{shape:?} emits no `links_cut`, so it must not touch the \
1345                     log at all: {step}"
1346                );
1347            }
1348        }
1349    }
1350
1351    /// One row in `branches` is the trunk shape whatever name is asked for,
1352    /// and an unknown name is refused before any shape is chosen.
1353    #[tokio::test]
1354    async fn one_lineage_is_the_trunk_shape() {
1355        let conn = fresh().await;
1356        assert_eq!(
1357            resolve_for(&conn, None).await.unwrap().0,
1358            LineageShape::Trunk
1359        );
1360        assert_eq!(
1361            resolve_for(&conn, Some("main")).await.unwrap().0,
1362            LineageShape::Trunk
1363        );
1364        assert!(matches!(
1365            resolve_for(&conn, Some("ghost")).await,
1366            Err(DbError::UnknownBranch(name)) if name == "ghost"
1367        ));
1368    }
1369
1370    /// Once the ledger has forked, the root reads as itself and every other
1371    /// lineage resolves (0.15.2, D-244).
1372    #[tokio::test]
1373    async fn a_forked_ledger_gives_the_root_its_own_shape() {
1374        let conn = fresh().await;
1375        fork(&conn, "b1", "main").await;
1376        fork(&conn, "b2", "b1").await;
1377        assert_eq!(
1378            resolve_for(&conn, None).await.unwrap().0,
1379            LineageShape::TrunkOnForked,
1380            "an unbranched read on a forked ledger is the trunk's own read"
1381        );
1382        assert_eq!(
1383            resolve_for(&conn, Some("main")).await.unwrap().0,
1384            LineageShape::TrunkOnForked
1385        );
1386        assert_eq!(
1387            resolve_for(&conn, Some("b1")).await.unwrap().0,
1388            LineageShape::Resolved
1389        );
1390        assert_eq!(
1391            resolve_for(&conn, Some("b2")).await.unwrap().0,
1392            LineageShape::Resolved
1393        );
1394        assert!(matches!(
1395            resolve_for(&conn, Some("ghost")).await,
1396            Err(DbError::UnknownBranch(_))
1397        ));
1398    }
1399
1400    /// Read the ancestry back from whichever relation produced it, `dist` first.
1401    async fn ancestry_from(conn: &libsql::Connection, sql: &str) -> Vec<Ancestor> {
1402        let mut rows = conn.query(sql, ()).await.unwrap();
1403        let mut out = Vec::new();
1404        while let Some(r) = rows.next().await.unwrap() {
1405            out.push(Ancestor {
1406                branch_id: r.get::<String>(0).unwrap(),
1407                dist: r.get::<i64>(1).unwrap(),
1408                cutoff: r.get::<Option<String>>(2).unwrap(),
1409            });
1410        }
1411        out.sort_by_key(|a| a.dist);
1412        out
1413    }
1414
1415    /// The CTE's answer for `start`, through the oracle.
1416    ///
1417    /// The branch is spliced rather than bound because this is test-only code
1418    /// reading a name this test wrote; the production form binds, which is what
1419    /// [`ancestry_values`] is about.
1420    async fn from_the_cte(conn: &libsql::Connection, start: &str) -> Vec<Ancestor> {
1421        let sql = format!(
1422            "WITH RECURSIVE {} SELECT branch_id, dist, cutoff FROM lineage",
1423            ancestry_cte(1, "").replace("?1", &format!("'{start}'"))
1424        );
1425        ancestry_from(conn, &sql).await
1426    }
1427
1428    /// The Rust walk's answer for `start`, round-tripped through the SQL it
1429    /// generates — so this checks [`resolve`] *and* [`ancestry_values`].
1430    async fn from_the_values(conn: &libsql::Connection, start: &str) -> Vec<Ancestor> {
1431        let lineages = Lineages::load(conn).await.unwrap();
1432        let ancestry = lineages.ancestry(start);
1433        let sql = format!(
1434            "WITH {} SELECT branch_id, dist, cutoff FROM lineage",
1435            ancestry_values(ancestry.len(), 1, "")
1436        );
1437        let mut rows = conn.query(&sql, ancestry_params(&ancestry)).await.unwrap();
1438        let mut out = Vec::new();
1439        while let Some(r) = rows.next().await.unwrap() {
1440            out.push(Ancestor {
1441                branch_id: r.get::<String>(0).unwrap(),
1442                dist: r.get::<i64>(1).unwrap(),
1443                cutoff: r.get::<Option<String>>(2).unwrap(),
1444            });
1445        }
1446        out.sort_by_key(|a| a.dist);
1447        out
1448    }
1449
1450    async fn fork_at(conn: &libsql::Connection, name: &str, parent: &str, at: &str) {
1451        conn.execute(
1452            "INSERT INTO branches (branch_id, parent_id, forked_at, created_at) \
1453             VALUES (?1, ?2, ?3, ?3)",
1454            libsql::params![name, parent, at],
1455        )
1456        .await
1457        .unwrap();
1458    }
1459
1460    /// **The differential test this release exists to pass** ([D-259]).
1461    ///
1462    /// [`resolve`] replaced a recursive CTE, and the way that goes wrong is not
1463    /// a crash: it is a cutoff that is one step too wide, on a lineage nobody
1464    /// reads for a month. So the Rust walk is checked against the SQL it
1465    /// replaced rather than against a restatement of the rule — every lineage
1466    /// on every shape below, both answers, byte for byte.
1467    ///
1468    /// Four shapes, and the third is the one that matters. A **chain** with
1469    /// increasing fork points never fires the running minimum. A chain with
1470    /// *decreasing* ones fires it at every step. Those two are the pair that
1471    /// tells a `min` apart from a plain assignment, which is the mistake a
1472    /// hand-written walk actually makes.
1473    ///
1474    /// [D-259]: ../../docs/architecture/s13-decision-register.md#d-259
1475    #[tokio::test]
1476    async fn the_rust_walk_agrees_with_the_cte() {
1477        for (label, forks) in [
1478            // A chain whose fork points increase down it: the minimum never bites.
1479            (
1480                "increasing chain",
1481                vec![
1482                    ("b1", "main", "2026-01-01T00:00:00.000000Z"),
1483                    ("b2", "b1", "2026-02-01T00:00:00.000000Z"),
1484                    ("b3", "b2", "2026-03-01T00:00:00.000000Z"),
1485                    ("b4", "b3", "2026-04-01T00:00:00.000000Z"),
1486                ],
1487            ),
1488            // The same chain with the fork points running the other way. Every
1489            // step must clamp to the earliest seen, and a walk that assigns
1490            // instead of taking a minimum widens each ancestor's window.
1491            (
1492                "decreasing chain",
1493                vec![
1494                    ("b1", "main", "2026-04-01T00:00:00.000000Z"),
1495                    ("b2", "b1", "2026-03-01T00:00:00.000000Z"),
1496                    ("b3", "b2", "2026-02-01T00:00:00.000000Z"),
1497                    ("b4", "b3", "2026-01-01T00:00:00.000000Z"),
1498                ],
1499            ),
1500            // Siblings: two lineages off one parent, so the walk must not carry
1501            // a cutoff sideways.
1502            (
1503                "siblings",
1504                vec![
1505                    ("l", "main", "2026-01-01T00:00:00.000000Z"),
1506                    ("r", "main", "2026-06-01T00:00:00.000000Z"),
1507                    ("ll", "l", "2026-02-01T00:00:00.000000Z"),
1508                ],
1509            ),
1510            // A root with nothing under it: the ancestry is one row, no cutoff.
1511            (
1512                "lone fork",
1513                vec![("only", "main", "2026-01-01T00:00:00.000000Z")],
1514            ),
1515        ] {
1516            let conn = fresh().await;
1517            for (name, parent, at) in &forks {
1518                fork_at(&conn, name, parent, at).await;
1519            }
1520            let mut names = vec!["main".to_string()];
1521            names.extend(forks.iter().map(|(n, _, _)| n.to_string()));
1522
1523            for name in &names {
1524                let cte = from_the_cte(&conn, name).await;
1525                let values = from_the_values(&conn, name).await;
1526                assert_eq!(cte, values, "{label}: the two forms disagree for `{name}`");
1527                // Not vacuous: the reader is always there, and a lineage that
1528                // is not the trunk always has at least one ancestor.
1529                assert_eq!(
1530                    cte.first().map(|a| a.branch_id.as_str()),
1531                    Some(name.as_str())
1532                );
1533                assert!(cte.first().is_some_and(|a| a.cutoff.is_none()));
1534            }
1535        }
1536    }
1537
1538    /// The clamp, stated directly, so a failure says which rule broke.
1539    ///
1540    /// [`the_rust_walk_agrees_with_the_cte`] would catch a dropped minimum, but
1541    /// it would report it as "the two forms disagree" — true, and one step away
1542    /// from what went wrong. This names it.
1543    #[tokio::test]
1544    async fn the_cutoff_is_the_earliest_fork_on_the_path_and_not_the_nearest() {
1545        let conn = fresh().await;
1546        fork_at(&conn, "early", "main", "2026-01-01T00:00:00.000000Z").await;
1547        fork_at(&conn, "late", "early", "2026-09-01T00:00:00.000000Z").await;
1548
1549        let lineages = Lineages::load(&conn).await.unwrap();
1550        let ancestry = lineages.ancestry("late");
1551
1552        // `late` sees `early` up to the point *it* diverged, and `main` up to
1553        // the point `early` diverged — the earlier of the two, not the nearer.
1554        assert_eq!(ancestry[0].cutoff, None, "the reader has no cutoff");
1555        assert_eq!(
1556            ancestry[1].cutoff.as_deref(),
1557            Some("2026-09-01T00:00:00.000000Z"),
1558            "`early` is seen up to where `late` left it"
1559        );
1560        assert_eq!(
1561            ancestry[2].cutoff.as_deref(),
1562            Some("2026-01-01T00:00:00.000000Z"),
1563            "`main` is clamped to where `early` left it, not to where `late` did"
1564        );
1565    }
1566
1567    /// A `branches` table that cannot be walked returns a prefix, not a hang.
1568    ///
1569    /// The schema makes this unreachable — `parent_id` is a foreign key into an
1570    /// append-only table — so this stages it by writing a row the schema would
1571    /// refuse, with foreign keys off. The bound in [`resolve`] is what stops a
1572    /// corrupted file from hanging a read, and a bound nothing tests is a
1573    /// comment.
1574    #[tokio::test]
1575    async fn a_parent_that_is_not_there_ends_the_walk() {
1576        let conn = fresh().await;
1577        conn.execute("PRAGMA foreign_keys = OFF", ()).await.unwrap();
1578        fork_at(&conn, "orphan", "vanished", "2026-01-01T00:00:00.000000Z").await;
1579
1580        let lineages = Lineages::load(&conn).await.unwrap();
1581        let ancestry = lineages.ancestry("orphan");
1582        // The orphan, then the parent it names — which no row describes, so the
1583        // walk stops there rather than looking for its parent.
1584        assert_eq!(ancestry.len(), 2);
1585        assert_eq!(ancestry[1].branch_id, "vanished");
1586    }
1587
1588    /// The `VALUES` text names three placeholders per ancestor, from the slot
1589    /// it was given, and nothing before it.
1590    #[test]
1591    fn the_bound_ancestry_starts_where_the_reader_put_it() {
1592        let sql = ancestry_values(2, 6, "");
1593        assert!(sql.contains("(?6, ?7, ?8)"), "{sql}");
1594        assert!(sql.contains("(?9, ?10, ?11)"), "{sql}");
1595        assert!(
1596            !sql.contains("?5"),
1597            "nothing below the slot it was given: {sql}"
1598        );
1599        // Tagged, for the diff's two lowerings in one `WITH` list.
1600        assert!(ancestry_values(1, 1, "_a").starts_with("lineage_a("));
1601    }
1602
1603    /// Only `Trunk` leaves the branch unbound; the layouts ask this rather
1604    /// than comparing against `Resolved`.
1605    #[test]
1606    fn every_shape_but_the_trunk_binds_the_branch() {
1607        assert!(!LineageShape::Trunk.binds_branch());
1608        assert!(LineageShape::Resolved.binds_branch());
1609        assert!(LineageShape::TrunkOnForked.binds_branch());
1610    }
1611}
1612