use std::ops::Bound;
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::fingerprint::Fingerprint;
use crate::hrtree::HRTree;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct HashSegment<K> {
range: (Bound<K>, Bound<K>),
hash: Fingerprint,
size: usize,
}
pub type DiffRange<K> = (Bound<K>, Bound<K>);
pub fn start_diff<K, V>(tree: &HRTree<K, V>) -> Vec<HashSegment<K>>
where
K: std::hash::Hash + Ord,
V: std::hash::Hash,
{
vec![HashSegment {
range: (Bound::Unbounded, Bound::Unbounded),
hash: tree.hash(&..),
size: tree.len(),
}]
}
pub fn diff_round<K, V>(
tree: &HRTree<K, V>,
in_comparison: Vec<HashSegment<K>>,
out_comparison: &mut Vec<HashSegment<K>>,
differences: &mut Vec<DiffRange<K>>,
) where
K: Clone + std::hash::Hash + Ord,
V: std::hash::Hash,
{
for segment in in_comparison {
let HashSegment { range, hash, size } = segment;
let local_hash = tree.hash(&range);
let (start_bound, end_bound) = range;
let start_index = match start_bound.as_ref() {
Bound::Unbounded => 0,
Bound::Included(key) => tree.insertion_position(key),
Bound::Excluded(_) => {
debug!("dropping segment with unsupported excluded start bound");
continue;
}
};
let end_index = match end_bound.as_ref() {
Bound::Unbounded => tree.len(),
Bound::Excluded(key) => tree.insertion_position(key),
Bound::Included(_) => {
debug!("dropping segment with unsupported included end bound");
continue;
}
};
let local_size = match end_index.checked_sub(start_index) {
Some(local_size) => local_size,
None => {
debug!("dropping segment with inverted range");
continue;
}
};
if hash == local_hash && size == local_size {
continue;
} else if size == 0 {
differences.push((start_bound, end_bound));
continue;
} else if local_size == 0 {
out_comparison.push(HashSegment {
range: (start_bound, end_bound),
hash: Fingerprint::ZERO,
size: 0,
});
continue;
} else if size == 1 && local_size == 1 {
out_comparison.push(HashSegment {
range: (start_bound.clone(), end_bound.clone()),
hash: Fingerprint::ZERO,
size: 0,
});
differences.push((start_bound, end_bound));
} else if local_size == 1 {
out_comparison.push(HashSegment {
range: (start_bound, end_bound),
hash: local_hash,
size: local_size,
});
} else {
let step = 1.max((end_index - start_index) / 16);
let mut cur_bound = start_bound;
let mut cur_index = start_index;
loop {
let next_index = cur_index + step;
if next_index >= end_index {
let range = (cur_bound, end_bound);
out_comparison.push(HashSegment {
hash: tree.hash(&range),
range,
size: end_index - cur_index,
});
break;
} else {
let next_key = tree.key_at(next_index);
let range = (cur_bound, Bound::Excluded(next_key.clone()));
out_comparison.push(HashSegment {
hash: tree.hash(&range),
range,
size: next_index - cur_index,
});
cur_bound = Bound::Included(next_key.clone());
cur_index = next_index;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tree(keys: &[i32]) -> HRTree<i32, i32> {
HRTree::from_iter(keys.iter().map(|&k| (k, 0)))
}
fn round(
store: &HRTree<i32, i32>,
segment: HashSegment<i32>,
) -> (Vec<HashSegment<i32>>, Vec<DiffRange<i32>>) {
let mut out_comparison = Vec::new();
let mut differences = Vec::new();
diff_round(store, vec![segment], &mut out_comparison, &mut differences);
(out_comparison, differences)
}
#[test]
fn excluded_start_bound_is_dropped_not_panicking() {
let store = tree(&[10, 20, 30]);
let segment = HashSegment {
range: (Bound::Excluded(0), Bound::Unbounded),
hash: Fingerprint([1, 0, 0, 0]),
size: 1,
};
let (out_comparison, differences) = round(&store, segment);
assert!(out_comparison.is_empty());
assert!(differences.is_empty());
}
#[test]
fn included_end_bound_is_dropped_not_panicking() {
let store = tree(&[10, 20, 30]);
let segment = HashSegment {
range: (Bound::Unbounded, Bound::Included(20)),
hash: Fingerprint([1, 0, 0, 0]),
size: 1,
};
let (out_comparison, differences) = round(&store, segment);
assert!(out_comparison.is_empty());
assert!(differences.is_empty());
}
#[test]
fn inverted_range_is_dropped_not_panicking() {
let store = tree(&[10, 20, 30]);
let segment = HashSegment {
range: (Bound::Included(100), Bound::Excluded(5)),
hash: Fingerprint([1, 0, 0, 0]),
size: 1,
};
let (out_comparison, differences) = round(&store, segment);
assert!(out_comparison.is_empty());
assert!(differences.is_empty());
}
#[test]
fn wellformed_segment_still_processed() {
let store = tree(&[10, 20, 30]);
let segment = HashSegment {
range: (Bound::Unbounded, Bound::Unbounded),
hash: Fingerprint::ZERO,
size: 0,
};
let (_out_comparison, differences) = round(&store, segment);
assert_eq!(differences, vec![(Bound::Unbounded, Bound::Unbounded)]);
}
#[test]
fn nonempty_zero_hash_vs_empty_is_not_in_sync() {
let store = tree(&[]); let segment = HashSegment {
range: (Bound::Unbounded, Bound::Unbounded),
hash: Fingerprint::ZERO, size: 2, };
let (out_comparison, differences) = round(&store, segment);
assert!(differences.is_empty());
assert_eq!(out_comparison.len(), 1);
assert_eq!(
out_comparison[0],
HashSegment {
range: (Bound::Unbounded, Bound::Unbounded),
hash: Fingerprint::ZERO,
size: 0,
}
);
}
#[test]
fn matching_hash_and_size_is_in_sync() {
let store = tree(&[10, 20, 30]);
let segment = HashSegment {
range: (Bound::Unbounded, Bound::Unbounded),
hash: store.hash(&..),
size: store.len(),
};
let (out_comparison, differences) = round(&store, segment);
assert!(out_comparison.is_empty());
assert!(differences.is_empty());
}
#[test]
fn matching_hash_but_wrong_size_is_refined() {
let store = tree(&[10, 20, 30, 40, 50]);
let segment = HashSegment {
range: (Bound::Unbounded, Bound::Unbounded),
hash: store.hash(&..), size: store.len() + 7, };
let (out_comparison, differences) = round(&store, segment);
assert!(!out_comparison.is_empty());
assert!(differences.is_empty());
}
}