extern crate alloc;
use alloc::vec::Vec;
use crate::row_header::{HEAP_XMIN_FROZEN, RowHeader, XMAX_ALIVE};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XactStatus {
InProgress,
Committed,
Aborted,
}
pub trait XactStatusOracle {
fn status(&self, version: u64) -> XactStatus;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AllCommitted;
impl XactStatusOracle for AllCommitted {
#[inline]
fn status(&self, _version: u64) -> XactStatus {
XactStatus::Committed
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InProgressSet {
sorted: Vec<u64>,
}
impl InProgressSet {
#[must_use]
pub fn from_sorted(sorted: Vec<u64>) -> Self {
debug_assert!(
sorted.windows(2).all(|w| w[0] < w[1]),
"InProgressSet::from_sorted requires strictly monotonic input"
);
Self { sorted }
}
#[must_use]
pub const fn empty() -> Self {
Self { sorted: Vec::new() }
}
#[must_use]
pub fn ids(&self) -> &[u64] {
&self.sorted
}
#[must_use]
pub fn contains(&self, xid: u64) -> bool {
self.sorted.binary_search(&xid).is_ok()
}
#[must_use]
pub fn len(&self) -> usize {
self.sorted.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sorted.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot {
pub version: u64,
pub in_progress: InProgressSet,
pub oldest_active: u64,
pub tx_id: u64,
pub locked_out: Option<(
crate::row_header::RelId,
alloc::collections::BTreeSet<usize>,
)>,
}
impl Snapshot {
#[must_use]
pub const fn unbounded() -> Self {
Self {
locked_out: None,
version: u64::MAX,
in_progress: InProgressSet::empty(),
oldest_active: u64::MAX,
tx_id: 0,
}
}
#[must_use]
pub fn new(version: u64, in_progress: InProgressSet, oldest_active: u64, tx_id: u64) -> Self {
Self {
locked_out: None,
version,
in_progress,
oldest_active,
tx_id,
}
}
#[must_use]
pub fn visible(&self, h: &RowHeader) -> bool {
if self.tx_id != 0 {
if h.xmax == self.tx_id {
return false;
}
if h.xmin == self.tx_id {
return true;
}
}
if h.xmin > self.version {
return false;
}
if self.in_progress.contains(h.xmin) {
return false;
}
if h.xmax == XMAX_ALIVE {
return true;
}
if h.xmax > self.version || self.in_progress.contains(h.xmax) {
return true;
}
false
}
#[must_use]
pub fn visible_with_status<O: XactStatusOracle + ?Sized>(
&self,
h: &RowHeader,
xact: &O,
) -> bool {
if self.tx_id != 0 {
if h.xmax == self.tx_id {
return false;
}
if h.xmin == self.tx_id {
return true;
}
}
if h.flags & HEAP_XMIN_FROZEN != 0 && h.xmax == XMAX_ALIVE {
return true;
}
if h.xmin > self.version {
return false;
}
if self.in_progress.contains(h.xmin) {
return false;
}
if xact.status(h.xmin) == XactStatus::Aborted {
return false;
}
if h.xmax == XMAX_ALIVE {
return true;
}
if h.xmax > self.version || self.in_progress.contains(h.xmax) {
return true;
}
if xact.status(h.xmax) == XactStatus::Aborted {
return true;
}
false
}
}
impl Default for Snapshot {
fn default() -> Self {
Self::unbounded()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::row_header::RowHeader;
fn ips(xs: &[u64]) -> InProgressSet {
InProgressSet::from_sorted(xs.to_vec())
}
#[test]
fn unbounded_snapshot_sees_everything() {
let s = Snapshot::unbounded();
let frozen = RowHeader::frozen();
let alive = RowHeader::alive(7);
assert!(s.visible(&frozen));
assert!(s.visible(&alive));
}
#[test]
fn snapshot_hides_future_writes() {
let s = Snapshot::new(100, ips(&[]), 100, 0);
let row = RowHeader::alive(150); assert!(!s.visible(&row));
}
#[test]
fn snapshot_hides_in_progress_writes() {
let s = Snapshot::new(200, ips(&[50, 60, 70]), 50, 0);
let row = RowHeader::alive(60); assert!(!s.visible(&row));
let row2 = RowHeader::alive(55); assert!(s.visible(&row2));
}
#[test]
fn snapshot_hides_committed_deletions() {
let s = Snapshot::new(200, ips(&[]), 100, 0);
let row = RowHeader {
xmin: 50,
xmax: 100, flags: 0,
};
assert!(!s.visible(&row));
}
#[test]
fn snapshot_keeps_pending_deletions_visible() {
let s = Snapshot::new(200, ips(&[150]), 100, 0);
let row = RowHeader {
xmin: 50,
xmax: 150, flags: 0,
};
assert!(s.visible(&row));
}
#[test]
fn reader_sees_its_own_insert_but_not_its_own_delete() {
let s = Snapshot::new(100, ips(&[]), 100, 42);
let own_insert = RowHeader::alive(42);
assert!(s.visible(&own_insert));
let own_insert_then_delete = RowHeader {
xmin: 42,
xmax: 42,
flags: 0,
};
assert!(!s.visible(&own_insert_then_delete));
let other_insert_i_deleted = RowHeader {
xmin: 7,
xmax: 42,
flags: 0,
};
assert!(!s.visible(&other_insert_i_deleted));
}
#[test]
fn snapshot_hides_future_deletion_done_by_in_flight_tx() {
let s = Snapshot::new(200, ips(&[150]), 30, 0);
let row = RowHeader {
xmin: 30,
xmax: 150,
flags: 0,
};
assert!(s.visible(&row));
}
struct AbortedSet(alloc::vec::Vec<u64>);
impl XactStatusOracle for AbortedSet {
fn status(&self, v: u64) -> XactStatus {
if self.0.contains(&v) {
XactStatus::Aborted
} else {
XactStatus::Committed
}
}
}
#[test]
fn all_committed_oracle_matches_plain_visible() {
let s = Snapshot::new(200, ips(&[150]), 50, 42);
let headers = [
RowHeader::frozen(),
RowHeader::alive(60),
RowHeader::alive(250),
RowHeader {
xmin: 50,
xmax: 100,
flags: 0,
},
RowHeader {
xmin: 50,
xmax: 150,
flags: 0,
},
RowHeader {
xmin: 42,
xmax: XMAX_ALIVE,
flags: 0,
},
];
for h in &headers {
assert_eq!(
s.visible(h),
s.visible_with_status(h, &AllCommitted),
"mismatch on {h:?}"
);
}
}
#[test]
fn aborted_xmin_hides_the_row() {
let s = Snapshot::new(200, ips(&[]), 50, 0);
let row = RowHeader::alive(60);
assert!(s.visible(&row), "two-state rule shows the orphan");
assert!(
!s.visible_with_status(&row, &AbortedSet(alloc::vec![60])),
"abort oracle hides the never-committed insert"
);
}
#[test]
fn aborted_xmax_revives_the_row() {
let s = Snapshot::new(200, ips(&[]), 50, 0);
let row = RowHeader {
xmin: 50,
xmax: 90,
flags: 0,
};
assert!(
!s.visible(&row),
"two-state rule treats delete as committed"
);
assert!(
s.visible_with_status(&row, &AbortedSet(alloc::vec![90])),
"abort oracle keeps the row whose delete was rolled back"
);
}
#[test]
fn frozen_row_skips_the_oracle() {
struct Panicking;
impl XactStatusOracle for Panicking {
fn status(&self, _v: u64) -> XactStatus {
panic!("oracle must not be consulted for a frozen+alive row");
}
}
let s = Snapshot::new(200, ips(&[]), 50, 0);
assert!(s.visible_with_status(&RowHeader::frozen(), &Panicking));
}
#[test]
fn in_progress_set_binary_search_correctness() {
let s = ips(&[10, 20, 30, 40, 50]);
assert!(s.contains(10));
assert!(s.contains(30));
assert!(s.contains(50));
assert!(!s.contains(0));
assert!(!s.contains(25));
assert!(!s.contains(60));
}
}