use crate::graph::lineage::{
ancestry_values, churned_cte, links_cut_cte, visible_cte, Ancestor, KeySlots, LineageShape,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Resolution<'a> {
pub shape: LineageShape,
pub branch_slot: usize,
pub recorded_slot: Option<usize>,
pub tag: &'a str,
pub key: Option<KeySlots>,
pub ancestry: &'a [Ancestor],
pub ancestry_slot: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Lowered {
pub ctes: Vec<String>,
pub source: String,
pub filter: String,
}
impl Lowered {
pub(crate) fn prelude(&self) -> String {
self.ctes.iter().map(|cte| format!("{cte},\n")).collect()
}
pub(crate) fn with_list(&self) -> String {
self.ctes.join(",\n")
}
pub(crate) fn with_clause(&self) -> String {
if self.ctes.is_empty() {
String::new()
} else {
format!("WITH RECURSIVE {}\n", self.with_list())
}
}
}
pub(crate) fn lower(r: &Resolution<'_>) -> Lowered {
let tag = r.tag;
let folded = r.recorded_slot.is_some();
let key_filter = |k: KeySlots| format!(" AND {}", k.equalities("l"));
let mut ctes: Vec<String> = Vec::new();
if r.shape == LineageShape::Resolved {
ctes.push(ancestry_values(r.ancestry.len(), r.ancestry_slot, tag));
}
if let Some(slot) = r.recorded_slot {
ctes.push(links_at_tx_cte(r.shape, slot, r.branch_slot, tag));
}
let mut filter = String::new();
let source = match r.shape {
LineageShape::Trunk => {
if folded {
format!("links_at_tx{tag}")
} else {
if let Some(k) = r.key {
filter = key_filter(k);
}
"links_current".to_string()
}
}
LineageShape::TrunkOnForked => {
if folded {
format!("links_at_tx{tag}")
} else {
filter = format!(" AND +l.branch_id = ?{}", r.branch_slot);
if let Some(k) = r.key {
filter.push_str(&key_filter(k));
}
"links_current".to_string()
}
}
LineageShape::Resolved => {
let resolved = if folded {
format!("links_at_tx{tag}")
} else {
ctes.push(churned_cte(tag, r.key));
ctes.push(links_cut_cte(tag, r.key));
format!("links_cut{tag}")
};
ctes.push(visible_cte(&resolved, tag, r.key));
format!("visible{tag}")
}
};
Lowered {
ctes,
source,
filter,
}
}
pub(crate) fn links_at_tx_cte(
shape: LineageShape,
slot: usize,
branch_slot: usize,
tag: &str,
) -> String {
let (lineage_join, cutoff) = match shape {
LineageShape::Resolved => (
format!("\n JOIN lineage{tag} g ON g.branch_id = transaction_log.branch_id"),
"\n AND (g.cutoff IS NULL OR transaction_log.recorded_at <= g.cutoff)"
.to_string(),
),
LineageShape::TrunkOnForked => (
String::new(),
format!("\n AND +transaction_log.branch_id = ?{branch_slot}"),
),
LineageShape::Trunk => (String::new(), String::new()),
};
format!(
r#"links_at_tx{tag}(source_id, target_id, edge_type, valid_from, valid_to, weight, branch_id) AS MATERIALIZED (
SELECT json_extract(payload, '$.source_id'),
json_extract(payload, '$.target_id'),
json_extract(payload, '$.edge_type'),
json_extract(payload, '$.valid_from'),
json_extract(payload, '$.valid_to'),
json_extract(payload, '$.weight'),
branch_id
FROM (
SELECT transaction_log.payload, transaction_log.branch_id,
ROW_NUMBER() OVER (
PARTITION BY transaction_log.entity_id, transaction_log.branch_id
ORDER BY transaction_log.seq_id DESC
) AS rn
FROM transaction_log{lineage_join}
WHERE transaction_log.table_name = 'links'
AND transaction_log.recorded_at <= ?{slot}{cutoff}
) WHERE rn = 1
)"#
)
}
#[cfg(test)]
mod tests {
use super::*;
fn anc() -> Vec<Ancestor> {
vec![
Ancestor {
branch_id: "b1".to_string(),
dist: 0,
cutoff: None,
},
Ancestor {
branch_id: "main".to_string(),
dist: 1,
cutoff: Some("2026-01-01T00:00:00.000000Z".to_string()),
},
]
}
use crate::graph::builder::TraversalBuilder;
const TUE: &str = "2026-01-06T00:00:00.000000Z";
fn names(l: &Lowered) -> Vec<String> {
l.ctes
.iter()
.map(|cte| cte.split('(').next().unwrap().to_string())
.collect()
}
#[test]
fn a_trunk_read_lowers_to_nothing() {
let l = lower(&Resolution {
shape: LineageShape::Trunk,
branch_slot: 5,
recorded_slot: None,
tag: "",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert!(l.ctes.is_empty());
assert_eq!(l.source, "links_current");
assert_eq!(l.prelude(), "");
assert_eq!(l.with_list(), "");
}
#[test]
fn a_trunk_read_at_a_recorded_instant_is_the_fold_alone() {
let l = lower(&Resolution {
shape: LineageShape::Trunk,
branch_slot: 5,
recorded_slot: Some(5),
tag: "",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert_eq!(names(&l), ["links_at_tx"]);
assert_eq!(l.source, "links_at_tx");
assert!(l.ctes[0].contains("recorded_at <= ?5"));
assert!(!l.ctes[0].contains("lineage"), "no ancestry to bound by");
}
#[test]
fn a_resolved_read_lowers_to_the_hybrid_in_order() {
let l = lower(&Resolution {
shape: LineageShape::Resolved,
branch_slot: 5,
recorded_slot: None,
tag: "",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert_eq!(names(&l), ["lineage", "churned", "links_cut", "visible"]);
assert_eq!(l.source, "visible");
assert!(l.ctes[0].starts_with("lineage(branch_id, dist, cutoff) AS (VALUES "));
assert!(
l.ctes[0].contains("(?9, ?10, ?11), (?12, ?13, ?14)"),
"{}",
l.ctes[0]
);
assert!(
!l.ctes[0].contains("SELECT"),
"nothing is walked: {}",
l.ctes[0]
);
assert!(l.ctes[3].contains("FROM links_cut l"));
}
#[test]
fn a_resolved_read_at_a_recorded_instant_folds_with_the_cutoff_inside() {
let l = lower(&Resolution {
shape: LineageShape::Resolved,
branch_slot: 5,
recorded_slot: Some(6),
tag: "",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert_eq!(names(&l), ["lineage", "links_at_tx", "visible"]);
assert_eq!(l.source, "visible");
let fold = &l.ctes[1];
assert!(fold.contains("recorded_at <= ?6"));
assert!(fold.contains("JOIN lineage g ON g.branch_id = transaction_log.branch_id"));
assert!(fold.contains("g.cutoff IS NULL OR transaction_log.recorded_at <= g.cutoff"));
assert!(l.ctes[2].contains("FROM links_at_tx l"));
}
#[test]
fn a_tag_reaches_every_name() {
let l = lower(&Resolution {
shape: LineageShape::Resolved,
branch_slot: 1,
recorded_slot: None,
tag: "_a",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert_eq!(
names(&l),
["lineage_a", "churned_a", "links_cut_a", "visible_a"]
);
assert_eq!(l.source, "visible_a");
assert!(l.ctes[3].contains("FROM links_cut_a l"));
assert!(l.ctes[3].contains("JOIN lineage_a g"));
assert!(
!l.with_list().contains("lineage g"),
"an untagged name leaked: {}",
l.with_list()
);
let folded = lower(&Resolution {
shape: LineageShape::Resolved,
branch_slot: 1,
recorded_slot: Some(3),
tag: "_b",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert_eq!(names(&folded), ["lineage_b", "links_at_tx_b", "visible_b"]);
assert!(folded.ctes[1].contains("JOIN lineage_b g"));
assert!(folded.ctes[2].contains("FROM links_at_tx_b l"));
}
#[test]
fn the_three_readers_share_one_prelude() {
use crate::graph::lineage::diff_sql;
let walk = TraversalBuilder::new("a").walk_cte(LineageShape::Resolved, &anc());
let ours = lower(&TraversalBuilder::new("a").resolution(LineageShape::Resolved, &anc()));
assert!(
walk.contains(&ours.prelude()),
"the walk assembles its own prelude"
);
let folded = TraversalBuilder::new("a").as_of_recorded(TUE);
let walk = folded.walk_cte(LineageShape::Resolved, &anc());
assert!(walk.contains(&lower(&folded.resolution(LineageShape::Resolved, &anc())).prelude()));
let walk = folded.walk_cte(LineageShape::Trunk, &anc());
assert!(walk.contains(&lower(&folded.resolution(LineageShape::Trunk, &anc())).prelude()));
let diff = diff_sql(&anc(), &anc());
for (slot, tag, ancestry_slot) in [(1, "_a", 3), (2, "_b", 3 + anc().len() * 3)] {
let side = lower(&Resolution {
shape: LineageShape::Resolved,
branch_slot: slot,
recorded_slot: None,
tag,
key: None,
ancestry: &anc(),
ancestry_slot,
});
assert!(
diff.contains(&side.with_list()),
"diff spells lineage {tag} itself"
);
}
let a = lower(&Resolution {
shape: LineageShape::Resolved,
branch_slot: 1,
recorded_slot: None,
tag: "_a",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
let b = lower(&Resolution {
shape: LineageShape::Resolved,
branch_slot: 2,
recorded_slot: None,
tag: "_b",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
let retagged = ["lineage", "churned", "links_cut", "visible"]
.iter()
.fold(a.with_list(), |s, name| {
s.replace(&format!("{name}_a"), &format!("{name}_b"))
})
.replace("SELECT ?1, 0, NULL", "SELECT ?2, 0, NULL");
assert_eq!(retagged, b.with_list());
}
#[test]
fn the_trunk_on_a_forked_ledger_lowers_to_a_filter() {
let l = lower(&Resolution {
shape: LineageShape::TrunkOnForked,
branch_slot: 5,
recorded_slot: None,
tag: "",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert!(l.ctes.is_empty(), "no ancestry to resolve: {:?}", names(&l));
assert_eq!(l.source, "links_current");
assert_eq!(l.filter, " AND +l.branch_id = ?5");
assert_eq!(l.with_clause(), "");
let folded = lower(&Resolution {
shape: LineageShape::TrunkOnForked,
branch_slot: 5,
recorded_slot: Some(6),
tag: "",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert_eq!(names(&folded), ["links_at_tx"]);
assert_eq!(folded.source, "links_at_tx");
assert_eq!(folded.filter, "");
let fold = &folded.ctes[0];
assert!(fold.contains("recorded_at <= ?6"));
assert!(fold.contains("AND +transaction_log.branch_id = ?5"));
assert!(!fold.contains("lineage"), "a root has no ancestry to join");
assert!(folded
.with_clause()
.starts_with("WITH RECURSIVE links_at_tx("));
}
#[test]
fn only_the_forked_trunk_needs_a_reader_side_filter() {
for (shape, recorded) in [
(LineageShape::Trunk, None),
(LineageShape::Trunk, Some(5)),
(LineageShape::Resolved, None),
(LineageShape::Resolved, Some(6)),
] {
let l = lower(&Resolution {
shape,
branch_slot: 5,
recorded_slot: recorded,
tag: "",
key: None,
ancestry: &anc(),
ancestry_slot: 9,
});
assert_eq!(l.filter, "", "{shape:?} at {recorded:?}");
}
}
}