use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Change {
Inserted,
Updated,
Deleted(Option<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetChange {
pub entity: String,
pub pk: String,
pub change: Change,
}
#[derive(Default)]
struct Journal {
entries: Vec<(String, String, Change)>,
marks: Vec<usize>,
overflow: BTreeSet<String>,
}
thread_local! {
static JOURNAL: RefCell<Journal> = RefCell::new(Journal::default());
static FLUSH_CHANGED: RefCell<std::collections::HashSet<(String, String)>> =
RefCell::new(std::collections::HashSet::new());
}
pub fn begin_flush() {
FLUSH_CHANGED.with(|c| c.borrow_mut().clear());
}
pub fn changed_in_flush(entity: &str, pk: i64) -> bool {
FLUSH_CHANGED.with(|c| c.borrow().contains(&(entity.to_string(), pk.to_string())))
}
pub fn record(entity: &str, pk: String, change: Change) {
FLUSH_CHANGED.with(|c| c.borrow_mut().insert((entity.to_string(), pk.clone())));
let cap = crate::config::report_max_tracked();
if cap == 0 {
return;
}
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
if j.entries.len() < cap {
j.entries.push((entity.to_string(), pk, change));
} else {
j.overflow.insert(entity.to_string());
}
});
}
pub fn savepoint_start() {
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
let len = j.entries.len();
j.marks.push(len);
});
}
pub fn savepoint_abort() {
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
if let Some(len) = j.marks.pop() {
j.entries.truncate(len);
}
});
}
pub fn savepoint_commit() {
JOURNAL.with(|j| {
j.borrow_mut().marks.pop();
});
}
pub fn clear() {
JOURNAL.with(|j| *j.borrow_mut() = Journal::default());
}
pub fn summarize(reset: bool) -> (Vec<NetChange>, BTreeSet<String>) {
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
let net = net_changes(&j.entries);
let overflow = j.overflow.clone();
if reset {
j.entries.clear();
j.overflow.clear();
j.marks.fill(0);
}
(net, overflow)
})
}
fn net_changes(entries: &[(String, String, Change)]) -> Vec<NetChange> {
let mut order: Vec<(String, String)> = Vec::new();
let mut state: HashMap<(String, String), Option<Change>> = HashMap::new();
for (entity, pk, change) in entries {
let key = (entity.clone(), pk.clone());
let prev = if let Some(prev) = state.get(&key) {
prev.clone()
} else {
order.push(key.clone());
None
};
let next = match (prev, change) {
(None, c) => Some(c.clone()),
(Some(Change::Inserted), Change::Deleted(_)) => None,
(Some(Change::Inserted), _) => Some(Change::Inserted),
(Some(Change::Deleted(_)), Change::Inserted | Change::Updated) => Some(Change::Updated),
(Some(_), c) => Some(c.clone()),
};
state.insert(key, next);
}
order
.into_iter()
.filter_map(|key| {
let change = state.remove(&key).flatten()?;
Some(NetChange {
entity: key.0,
pk: key.1,
change,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{Change, net_changes};
fn e(entity: &str, pk: &str, c: Change) -> (String, String, Change) {
(entity.to_string(), pk.to_string(), c)
}
fn net(entries: &[(String, String, Change)]) -> Vec<(String, String, Change)> {
net_changes(entries)
.into_iter()
.map(|n| (n.entity, n.pk, n.change))
.collect()
}
#[test]
fn keeps_first_seen_order_and_folds_repeats() {
let got = net(&[
e("post", "2", Change::Updated),
e("user", "1", Change::Updated),
e("post", "2", Change::Updated),
]);
assert_eq!(
got,
[
e("post", "2", Change::Updated),
e("user", "1", Change::Updated)
]
);
}
#[test]
fn insert_then_delete_cancels_and_delete_then_insert_is_update() {
let got = net(&[
e("post", "1", Change::Inserted),
e("post", "1", Change::Updated),
e("post", "1", Change::Deleted(Some("a".into()))),
e("post", "2", Change::Deleted(Some("b".into()))),
e("post", "2", Change::Inserted),
e("post", "3", Change::Inserted),
e("post", "3", Change::Updated),
]);
assert_eq!(
got,
[
e("post", "2", Change::Updated),
e("post", "3", Change::Inserted)
]
);
}
#[test]
fn update_then_delete_is_delete() {
let got = net(&[
e("post", "1", Change::Updated),
e("post", "1", Change::Deleted(Some("a".into()))),
]);
assert_eq!(got, [e("post", "1", Change::Deleted(Some("a".into())))]);
}
}