use std::collections::BTreeSet;
use crate::Key;
use crate::error::DbResult;
use crate::hook::{TypedWriteHook, WriteHook};
use crate::unique::{ClaimIntent, ClaimSink, UniqueTargets};
pub(crate) fn claim_set<K: Key, T, H: TypedWriteHook<K, T>>(
hook: &H,
targets: &UniqueTargets,
max_rows: usize,
max_bytes: usize,
key: &K,
snapshot: Option<&T>,
) -> DbResult<BTreeSet<(&'static str, Vec<u8>)>> {
let mut set = BTreeSet::new();
if let Some(value) = snapshot {
let mut sink = ClaimSink::new(targets, max_rows, max_bytes);
hook.claim_project(key, value, &mut sink)?;
for intent in sink.claims() {
set.insert((intent.namespace, intent.key.clone()));
}
}
Ok(set)
}
pub(crate) fn first_changed<K: Key, T, H: TypedWriteHook<K, T>>(
hook: &H,
targets: &UniqueTargets,
max_rows: usize,
max_bytes: usize,
key: &K,
old: Option<&T>,
new: Option<&T>,
) -> DbResult<Option<&'static str>> {
let before = claim_set(hook, targets, max_rows, max_bytes, key, old)?;
let after = claim_set(hook, targets, max_rows, max_bytes, key, new)?;
Ok(before
.symmetric_difference(&after)
.next()
.map(|(namespace, _)| *namespace))
}
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct ClaimDelta {
pub claims: Vec<ClaimIntent>,
pub releases: Vec<ClaimIntent>,
}
pub(crate) fn claim_delta<K: Key, T, H: TypedWriteHook<K, T>>(
hook: &H,
targets: &UniqueTargets,
max_rows: usize,
max_bytes: usize,
key: &K,
old: Option<&T>,
new: Option<&T>,
) -> DbResult<ClaimDelta> {
let before = claim_set(hook, targets, max_rows, max_bytes, key, old)?;
let after = claim_set(hook, targets, max_rows, max_bytes, key, new)?;
Ok(ClaimDelta {
releases: before
.difference(&after)
.cloned()
.map(into_intent)
.collect(),
claims: after
.difference(&before)
.cloned()
.map(into_intent)
.collect(),
})
}
fn into_intent((namespace, key): (&'static str, Vec<u8>)) -> ClaimIntent {
ClaimIntent { namespace, key }
}
pub(crate) fn claim_set_bytes<K: Key, H: WriteHook<K>>(
hook: &H,
targets: &UniqueTargets,
max_rows: usize,
max_bytes: usize,
key: &K,
snapshot: Option<&[u8]>,
) -> DbResult<BTreeSet<(&'static str, Vec<u8>)>> {
let mut set = BTreeSet::new();
if let Some(value) = snapshot {
let mut sink = ClaimSink::new(targets, max_rows, max_bytes);
hook.claim_project(key, None, Some(value), &mut sink)?;
for intent in sink.claims() {
set.insert((intent.namespace, intent.key.clone()));
}
}
Ok(set)
}
pub(crate) fn first_changed_bytes<K: Key, H: WriteHook<K>>(
hook: &H,
targets: &UniqueTargets,
max_rows: usize,
max_bytes: usize,
key: &K,
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Option<&'static str>> {
let before = claim_set_bytes(hook, targets, max_rows, max_bytes, key, old)?;
let after = claim_set_bytes(hook, targets, max_rows, max_bytes, key, new)?;
Ok(before
.symmetric_difference(&after)
.next()
.map(|(namespace, _)| *namespace))
}
pub(crate) fn claim_delta_bytes<K: Key, H: WriteHook<K>>(
hook: &H,
targets: &UniqueTargets,
max_rows: usize,
max_bytes: usize,
key: &K,
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<ClaimDelta> {
let before = claim_set_bytes(hook, targets, max_rows, max_bytes, key, old)?;
let after = claim_set_bytes(hook, targets, max_rows, max_bytes, key, new)?;
Ok(ClaimDelta {
releases: before
.difference(&after)
.cloned()
.map(into_intent)
.collect(),
claims: after
.difference(&before)
.cloned()
.map(into_intent)
.collect(),
})
}
#[cfg(all(test, feature = "armour"))]
mod tests {
use std::sync::Arc;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
use super::*;
use crate::armour::Db;
use crate::unique::UniqueIndex;
use crate::{FixedConfig, unique_index};
#[derive(
Clone, Copy, Debug, PartialEq, Eq, Hash, FromBytes, IntoBytes, Immutable, KnownLayout,
)]
#[repr(C)]
pub struct UsernameKey([u8; 16]);
crate::impl_key_zerocopy!(
UsernameKey,
armour_core::KeyScheme::Typed(&[armour_core::KeyType::Array(16)])
);
impl UsernameKey {
fn new(s: &str) -> Self {
let mut b = [0u8; 16];
b[..s.len()].copy_from_slice(s.as_bytes());
Self(b)
}
}
unique_index!(
DiffTestUsernames,
key = UsernameKey,
name = "unique_diff_test_usernames"
);
struct User {
username: Option<String>,
avatar: u32,
}
struct ClaimingHook {
usernames: Arc<UniqueIndex<DiffTestUsernames>>,
_db: Arc<Db>,
_dir: tempfile::TempDir,
}
impl TypedWriteHook<[u8; 8], User> for ClaimingHook {
const CLAIMS: bool = true;
fn on_write(&self, _key: &[u8; 8], _old: Option<&User>, _new: Option<&User>) {}
fn claim_project(
&self,
_key: &[u8; 8],
value: &User,
sink: &mut ClaimSink<'_>,
) -> DbResult<()> {
if let Some(username) = &value.username {
sink.claim(&self.usernames, UsernameKey::new(username))?;
}
Ok(())
}
fn claim_targets(&self) -> Option<&UniqueTargets> {
None
}
}
fn key() -> [u8; 8] {
[7u8; 8]
}
fn fixture() -> (ClaimingHook, UniqueTargets) {
let dir = tempfile::tempdir().expect("tempdir");
let db = Arc::new(Db::open_test(dir.path()).expect("open db"));
let usernames = db
.open_unique_index::<DiffTestUsernames>(FixedConfig::test())
.expect("open namespace");
let targets = UniqueTargets::new().with(&usernames);
(
ClaimingHook {
usernames,
_db: db,
_dir: dir,
},
targets,
)
}
fn username_key_bytes(s: &str) -> Vec<u8> {
crate::Key::as_bytes(&UsernameKey::new(s)).to_vec()
}
#[test]
fn unique_diff_reports_no_change_when_the_claim_set_is_equal() {
let (hook, targets) = fixture();
let a = User {
username: Some("rail".into()),
avatar: 1,
};
let b = User {
username: Some("rail".into()),
avatar: 2,
};
assert_ne!(
a.avatar, b.avatar,
"fixture must vary a non-claimed field, or this test cannot tell \
'diff ignores non-claim fields' from 'diff ignores everything'"
);
let set = claim_set(&hook, &targets, 4096, 1 << 20, &key(), Some(&a)).unwrap();
assert_eq!(
set,
BTreeSet::from([(hook.usernames.name(), username_key_bytes("rail"))])
);
assert_eq!(
first_changed(&hook, &targets, 4096, 1 << 20, &key(), Some(&a), Some(&b)).unwrap(),
None
);
}
#[test]
fn unique_diff_first_changed_names_the_namespace_on_a_real_change() {
let (hook, targets) = fixture();
let a = User {
username: Some("old".into()),
avatar: 1,
};
let b = User {
username: Some("new".into()),
avatar: 1,
};
let ns = hook.usernames.name();
assert_eq!(
first_changed(&hook, &targets, 4096, 1 << 20, &key(), Some(&a), Some(&b)).unwrap(),
Some(ns)
);
assert_eq!(
first_changed(&hook, &targets, 4096, 1 << 20, &key(), None, Some(&a)).unwrap(),
Some(ns)
);
assert_eq!(
first_changed(&hook, &targets, 4096, 1 << 20, &key(), Some(&a), None).unwrap(),
Some(ns)
);
}
#[test]
fn unique_diff_splits_a_rename_into_one_release_and_one_claim() {
let (hook, targets) = fixture();
let a = User {
username: Some("old".into()),
avatar: 1,
};
let b = User {
username: Some("new".into()),
avatar: 1,
};
let d = claim_delta(&hook, &targets, 4096, 1 << 20, &key(), Some(&a), Some(&b)).unwrap();
let ns = hook.usernames.name();
assert_eq!(d.releases.len(), 1);
assert_eq!(d.releases[0].namespace, ns);
assert_eq!(d.releases[0].key, username_key_bytes("old"));
assert_eq!(d.claims.len(), 1);
assert_eq!(d.claims[0].namespace, ns);
assert_eq!(d.claims[0].key, username_key_bytes("new"));
}
#[test]
fn unique_diff_treats_insert_and_delete_as_claim_set_changes() {
let (hook, targets) = fixture();
let v = User {
username: Some("rail".into()),
avatar: 1,
};
let ns = hook.usernames.name();
let rail = username_key_bytes("rail");
assert_eq!(
first_changed(&hook, &targets, 4096, 1 << 20, &key(), None, Some(&v)).unwrap(),
Some(ns)
);
let insert = claim_delta(&hook, &targets, 4096, 1 << 20, &key(), None, Some(&v)).unwrap();
assert_eq!(insert.releases.len(), 0);
assert_eq!(insert.claims.len(), 1);
assert_eq!(insert.claims[0].namespace, ns);
assert_eq!(insert.claims[0].key, rail);
assert_eq!(
first_changed(&hook, &targets, 4096, 1 << 20, &key(), Some(&v), None).unwrap(),
Some(ns)
);
let delete = claim_delta(&hook, &targets, 4096, 1 << 20, &key(), Some(&v), None).unwrap();
assert_eq!(delete.releases.len(), 1);
assert_eq!(delete.claims.len(), 0);
assert_eq!(delete.releases[0].namespace, ns);
assert_eq!(delete.releases[0].key, rail);
}
}