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: i64,
pub id: Option<crate::catalog::RowId>,
pub change: Change,
pub parents: Vec<(
std::rc::Rc<crate::catalog::Collection>,
crate::catalog::collections::ParentRef,
)>,
pub captured: bool,
}
impl Recorded {
#[must_use]
pub fn of(
meta: &crate::catalog::TviewMeta,
pk: i64,
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),
captured: crate::config::publish_changes() == Some(super::published::Publish::Rows),
}
}
}
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, i64)>> =
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)))
}
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))
});
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());
}
#[derive(Debug, Default)]
pub struct Summary {
pub changes: Vec<NetChange>,
pub overflow: BTreeSet<String>,
pub parents: Option<
Vec<(
std::rc::Rc<crate::catalog::Collection>,
crate::catalog::collections::ParentRef,
)>,
>,
}
pub fn summarize(reset: bool, capturing: bool) -> Summary {
JOURNAL.with(|j| {
let mut j = j.borrow_mut();
let entries = &j.entries[j.reported - j.base..];
let captured = if entries.is_empty() {
capturing
} else {
entries.iter().all(|e| e.captured)
};
let summary = Summary {
changes: net_changes(entries),
overflow: j.overflow.clone(),
parents: captured.then(|| parents(entries)),
};
if reset {
j.reported = j.base + j.entries.len();
j.overflow.clear();
j.compact();
}
summary
})
}
fn parents(
entries: &[Recorded],
) -> Vec<(
std::rc::Rc<crate::catalog::Collection>,
crate::catalog::collections::ParentRef,
)> {
let mut seen = std::collections::HashSet::new();
entries
.iter()
.flat_map(|e| e.parents.iter())
.filter(|(c, parent)| seen.insert((std::rc::Rc::clone(&c.key), parent.key.clone())))
.cloned()
.collect()
}
#[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, i64)> = Vec::new();
let mut state: HashMap<(String, i64), (Option<Change>, &Recorded)> = HashMap::new();
for entry in entries {
let key = (entry.entity.clone(), entry.pk);
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: i64) {
JOURNAL.with(|j| {
j.borrow_mut().entries.push(e("user", pk, Change::Updated));
});
}
fn report(reset: bool) -> Vec<i64> {
summarize(reset, false)
.changes
.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: i64, id: Option<&str>, change: Change) -> Recorded {
Recorded {
entity: entity.to_string(),
typename: entity.to_uppercase().into(),
pk,
id: id.map(|text| crate::catalog::RowId {
text: text.to_string(),
kind: crate::catalog::PublicId::String,
}),
change,
parents: Vec::new(),
captured: true,
}
}
fn e(entity: &str, pk: i64, c: Change) -> Recorded {
with_id(entity, pk, None, c)
}
fn net(entries: &[Recorded]) -> Vec<(String, i64, Change)> {
net_changes(entries)
.into_iter()
.map(|n| (n.entity, n.pk, n.change))
.collect()
}
fn ids(entries: &[Recorded]) -> Vec<(i64, 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,
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, 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, Change::Updated),
("user".to_string(), 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),
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, Change::Updated),
("post".to_string(), 3, 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, Change::Deleted)]);
}
#[test]
fn a_row_written_twice_names_every_parent_it_left_or_joined() {
let post: std::rc::Rc<crate::catalog::Collection> =
std::rc::Rc::new(crate::catalog::Collection {
key: "Comment.fk_post".into(),
child_type: "Comment".into(),
column: "fk_post".to_string(),
parent_table: "public.tv_post".to_string(),
parent_table_oid: pgrx::pg_sys::Oid::from(1),
parent_column: "pk_post".to_string(),
parent_type: "integer".to_string(),
parent_id: Some(crate::catalog::PublicId::String),
parent_typename: "Post".into(),
parent_keyed_by_pk: true,
});
let moved = |to: &str, from: &str| {
let mut r = e("comment", 1, Change::Updated);
r.parents = [to, from]
.iter()
.map(|key| {
(
std::rc::Rc::clone(&post),
crate::catalog::collections::ParentRef {
key: (*key).to_string(),
id: None,
},
)
})
.collect();
r
};
clear();
JOURNAL.with(|j| {
j.borrow_mut()
.entries
.extend([moved("3", "2"), moved("1", "3")]);
});
let keys: Vec<String> = summarize(true, true)
.parents
.unwrap_or_default()
.into_iter()
.map(|(_, parent)| parent.key)
.collect();
assert_eq!(keys, ["3", "2", "1"]);
clear();
}
}