use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Change {
Inserted,
Updated,
Deleted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Recorded {
pub entity: String,
pub typename: std::rc::Rc<str>,
pub pk: String,
pub id: Option<crate::catalog::RowId>,
pub change: Change,
pub parents: Vec<(
std::rc::Rc<crate::catalog::Collection>,
crate::catalog::collections::ParentRef,
)>,
}
impl Recorded {
#[must_use]
pub fn of(
meta: &crate::catalog::TviewMeta,
pk: String,
id: Option<String>,
change: Change,
references: &[Option<String>],
old: Option<&[Option<String>]>,
) -> Self {
Self {
entity: meta.entity_name.clone(),
typename: std::rc::Rc::clone(&meta.typename),
pk,
id: id.map(|text| meta.row_id(text)),
change,
parents: meta.parents(references, old),
}
}
}
pub type NetChange = Recorded;
#[derive(Default)]
struct Journal {
entries: Vec<Recorded>,
base: usize,
reported: usize,
overflow: BTreeSet<String>,
open: usize,
}
#[derive(Debug)]
pub struct Mark {
position: usize,
reported: usize,
overflow: BTreeSet<String>,
}
impl Journal {
fn compact(&mut self) {
if self.open == 0 {
self.entries.drain(..self.reported - self.base);
self.base = self.reported;
}
}
}
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(recorded: Recorded) {
let counter = match recorded.change {
Change::Deleted => crate::stats::Counter::RowsDeleted,
_ => crate::stats::Counter::RowsWritten,
};
crate::stats::add(&recorded.entity, counter, 1);
FLUSH_CHANGED.with(|c| {
c.borrow_mut()
.insert((recorded.entity.clone(), recorded.pk.clone()))
});
if let Some(publish) = crate::config::publish_changes() {
super::published::add(publish, &recorded);
}
let cap = crate::config::report_max_tracked();
if cap == 0 {
return;
}
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
if j.base + j.entries.len() - j.reported < cap {
j.entries.push(recorded);
} else {
j.overflow.insert(recorded.entity);
}
});
}
pub fn record_rebuilt(entity: &str) -> crate::TViewResult<()> {
if crate::config::report_max_tracked() > 0 {
JOURNAL.with(|j| j.borrow_mut().overflow.insert(entity.to_string()));
}
if crate::config::publish_changes().is_some()
&& let Some(meta) = crate::catalog::TviewMeta::load_by_entity(entity)?
{
super::published::add_type(&meta.typename);
}
Ok(())
}
pub fn mark() -> Mark {
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
j.open += 1;
Mark {
position: j.base + j.entries.len(),
reported: j.reported,
overflow: j.overflow.clone(),
}
})
}
pub fn release(_mark: Mark) {
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
j.open = j.open.saturating_sub(1);
j.compact();
});
}
pub fn rollback(mark: Mark) {
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
j.open = j.open.saturating_sub(1);
let keep = mark.position - j.base;
j.entries.truncate(keep);
j.reported = mark.reported;
j.overflow.extend(mark.overflow);
j.compact();
});
}
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[j.reported - j.base..]);
let overflow = j.overflow.clone();
if reset {
j.reported = j.base + j.entries.len();
j.overflow.clear();
j.compact();
}
(net, overflow)
})
}
#[must_use]
pub const fn fold_change(prev: Option<Change>, next: Change) -> Option<Change> {
match (prev, next) {
(None, c) => Some(c),
(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),
}
}
pub fn net_changes(entries: &[Recorded]) -> Vec<NetChange> {
let mut order: Vec<(String, String)> = Vec::new();
let mut state: HashMap<(String, String), (Option<Change>, &Recorded)> = HashMap::new();
for entry in entries {
let key = (entry.entity.clone(), entry.pk.clone());
let prev = if let Some((prev, _)) = state.get(&key) {
*prev
} else {
order.push(key.clone());
None
};
let next = fold_change(prev, entry.change);
let last = match state.get(&key) {
Some((_, last)) if entry.id.is_none() => last,
_ => entry,
};
state.insert(key, (next, last));
}
order
.into_iter()
.filter_map(|key| {
let (change, last) = state.remove(&key)?;
Some(Recorded {
change: change?,
..last.clone()
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{
Change, JOURNAL, Recorded, clear, mark, net_changes, release, rollback, summarize,
};
fn push(pk: &str) {
JOURNAL.with(|j| {
j.borrow_mut().entries.push(e("user", pk, Change::Updated));
});
}
fn report(reset: bool) -> Vec<String> {
summarize(reset).0.into_iter().map(|n| n.pk).collect()
}
#[test]
fn a_rolled_back_reset_is_undone() {
clear();
push("1");
let m = mark();
assert_eq!(report(true), ["1"]);
push("2");
rollback(m);
assert_eq!(report(true), ["1"]);
assert!(report(false).is_empty());
clear();
}
#[test]
fn a_released_reset_holds_until_an_outer_rollback() {
clear();
push("1");
let outer = mark();
let inner = mark();
assert_eq!(report(true), ["1"]);
release(inner);
assert!(report(false).is_empty());
rollback(outer);
assert_eq!(report(false), ["1"]);
clear();
}
#[test]
fn reported_entries_are_dropped_once_no_subtransaction_is_open() {
clear();
push("1");
let m = mark();
report(true);
release(m);
push("2");
assert_eq!(report(true), ["2"]);
JOURNAL.with(|j| assert!(j.borrow().entries.is_empty()));
clear();
}
fn with_id(entity: &str, pk: &str, id: Option<&str>, change: Change) -> Recorded {
Recorded {
entity: entity.to_string(),
typename: entity.to_uppercase().into(),
pk: pk.to_string(),
id: id.map(|text| crate::catalog::RowId {
text: text.to_string(),
kind: crate::catalog::PublicId::String,
}),
change,
parents: Vec::new(),
}
}
fn e(entity: &str, pk: &str, c: Change) -> Recorded {
with_id(entity, pk, None, c)
}
fn net(entries: &[Recorded]) -> Vec<(String, String, Change)> {
net_changes(entries)
.into_iter()
.map(|n| (n.entity, n.pk, n.change))
.collect()
}
fn ids(entries: &[Recorded]) -> Vec<(String, Option<String>, Change)> {
net_changes(entries)
.into_iter()
.map(|n| (n.pk, n.id.map(|id| id.text), n.change))
.collect()
}
#[test]
fn the_net_change_keeps_the_last_id_and_the_type_name() {
let got = net_changes(&[
with_id("post", "1", Some("a"), Change::Inserted),
with_id("post", "1", Some("b"), Change::Updated),
with_id("post", "2", Some("c"), Change::Updated),
with_id("post", "2", None, Change::Updated),
]);
assert_eq!(
got.iter()
.map(|n| (
n.pk.as_str(),
n.id.as_ref().map(|id| id.text.as_str()),
n.change,
&*n.typename
))
.collect::<Vec<_>>(),
[
("1", Some("b"), Change::Inserted, "POST"),
("2", Some("c"), Change::Updated, "POST")
]
);
}
#[test]
fn a_delete_after_an_update_keeps_the_deletes_id() {
assert_eq!(
ids(&[
with_id("post", "1", Some("old"), Change::Updated),
with_id("post", "1", Some("gone"), Change::Deleted),
]),
[("1".to_string(), Some("gone".to_string()), Change::Deleted)]
);
}
#[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,
[
("post".to_string(), "2".to_string(), Change::Updated),
("user".to_string(), "1".to_string(), 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),
e("post", "2", Change::Deleted),
e("post", "2", Change::Inserted),
e("post", "3", Change::Inserted),
e("post", "3", Change::Updated),
]);
assert_eq!(
got,
[
("post".to_string(), "2".to_string(), Change::Updated),
("post".to_string(), "3".to_string(), Change::Inserted)
]
);
}
#[test]
fn update_then_delete_is_delete() {
let got = net(&[
e("post", "1", Change::Updated),
e("post", "1", Change::Deleted),
]);
assert_eq!(
got,
[("post".to_string(), "1".to_string(), Change::Deleted)]
);
}
}