use super::{EMPTY_ROOT, Node, NodeId, PersistentBTree};
use std::cmp::Ordering;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TreeDiff {
Added { key: Vec<u8>, value: Vec<u8> },
Removed { key: Vec<u8>, value: Vec<u8> },
Modified {
key: Vec<u8>,
old_value: Vec<u8>,
new_value: Vec<u8>,
},
}
impl PersistentBTree {
pub fn diff(&self, old_root: NodeId, new_root: NodeId) -> Vec<TreeDiff> {
let mut result = Vec::new();
diff_walk(self, old_root, new_root, |e| result.push(e));
result
}
}
enum Item {
Node(NodeId, u32),
Entry(Vec<u8>, Vec<u8>),
}
struct Cursor(Vec<Item>);
impl Cursor {
fn new(tree: &PersistentBTree, root: NodeId) -> Self {
if root == EMPTY_ROOT {
Self(Vec::new())
} else {
Self(vec![Item::Node(root, tree.height(root))])
}
}
fn expand(&mut self, tree: &PersistentBTree) {
let Some(Item::Node(id, h)) = self.0.pop() else {
unreachable!("expand called on a non-node front");
};
match tree.node(id) {
Node::Leaf { keys, values } => {
self.0.extend(
keys.into_iter()
.zip(values)
.rev()
.map(|(k, v)| Item::Entry(k, v)),
);
}
Node::Internal { children, .. } => {
let ch = h.saturating_sub(1);
self.0
.extend(children.into_iter().rev().map(|c| Item::Node(c, ch)));
}
}
}
fn pop_entry(&mut self) -> (Vec<u8>, Vec<u8>) {
match self.0.pop() {
Some(Item::Entry(k, v)) => (k, v),
_ => unreachable!("pop_entry called on a non-entry front"),
}
}
}
enum Step {
Done,
SkipShared,
ExpandOld,
ExpandNew,
ExpandBoth,
Removed,
Added,
Compare(Ordering),
}
pub(crate) fn diff_walk(
tree: &PersistentBTree,
old_root: NodeId,
new_root: NodeId,
mut emit: impl FnMut(TreeDiff),
) {
if old_root == new_root {
return;
}
let mut old = Cursor::new(tree, old_root);
let mut new = Cursor::new(tree, new_root);
loop {
let step = match (old.0.last(), new.0.last()) {
(None, None) => Step::Done,
(Some(Item::Node(a, _)), Some(Item::Node(b, _))) if a == b => {
Step::SkipShared
}
(Some(Item::Node(_, ha)), Some(Item::Node(_, hb))) => match ha.cmp(hb) {
Ordering::Greater => Step::ExpandOld,
Ordering::Less => Step::ExpandNew,
Ordering::Equal => Step::ExpandBoth,
},
(Some(Item::Node(..)), _) => Step::ExpandOld,
(_, Some(Item::Node(..))) => Step::ExpandNew,
(Some(Item::Entry(..)), None) => Step::Removed,
(None, Some(Item::Entry(..))) => Step::Added,
(Some(Item::Entry(ka, _)), Some(Item::Entry(kb, _))) => {
Step::Compare(ka.cmp(kb))
}
};
match step {
Step::Done => break,
Step::SkipShared => {
old.0.pop();
new.0.pop();
}
Step::ExpandOld => old.expand(tree),
Step::ExpandNew => new.expand(tree),
Step::ExpandBoth => {
old.expand(tree);
new.expand(tree);
}
Step::Removed | Step::Compare(Ordering::Less) => {
let (key, value) = old.pop_entry();
emit(TreeDiff::Removed { key, value });
}
Step::Added | Step::Compare(Ordering::Greater) => {
let (key, value) = new.pop_entry();
emit(TreeDiff::Added { key, value });
}
Step::Compare(Ordering::Equal) => {
let (key, old_value) = old.pop_entry();
let (_, new_value) = new.pop_entry();
if old_value != new_value {
emit(TreeDiff::Modified {
key,
old_value,
new_value,
});
}
}
}
}
}