use crate::types::{RowKey, ScanRow, TombstoneType, Value};
#[derive(Debug, Clone, PartialEq)]
pub struct CompactionRow {
pub key: RowKey,
pub row_timestamp: i64,
pub row_data: CompactionRowData,
}
impl CompactionRow {
pub fn from_legacy_value(key: RowKey, row: ScanRow, row_timestamp: i64) -> Self {
let row_data = match row {
ScanRow::Row(entries) => {
let simple = entries
.into_iter()
.map(|(k, v)| {
let timestamp = match &v {
Value::Tombstone(info) => info.deletion_time,
_ => row_timestamp,
};
SimpleCell {
column: k.to_string(),
value: v,
timestamp,
ttl: None,
local_deletion_time: None,
}
})
.collect();
CompactionRowData::Live {
simple,
complex: Vec::new(),
row_deletion: None,
row_liveness: RowLiveness::default(),
}
}
ScanRow::Marker(Value::Tombstone(info))
if info.tombstone_type == TombstoneType::RowTombstone =>
{
CompactionRowData::Tombstone {
deletion_time: info.deletion_time,
local_deletion_time: 0,
clustering: Vec::new(),
}
}
ScanRow::RawRow(bytes) => CompactionRowData::Live {
simple: vec![SimpleCell {
column: "value".to_string(),
value: Value::Blob(bytes.into()),
timestamp: row_timestamp,
ttl: None,
local_deletion_time: None,
}],
complex: Vec::new(),
row_deletion: None,
row_liveness: RowLiveness::default(),
},
ScanRow::Marker(other) => CompactionRowData::Live {
simple: vec![SimpleCell {
column: "value".to_string(),
value: other,
timestamp: row_timestamp,
ttl: None,
local_deletion_time: None,
}],
complex: Vec::new(),
row_deletion: None,
row_liveness: RowLiveness::default(),
},
};
CompactionRow {
key,
row_timestamp,
row_data,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompactionBound {
Inclusive(Vec<(String, Value)>),
Exclusive(Vec<(String, Value)>),
Bottom,
Top,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RowLiveness {
pub has_marker: bool,
pub expires_at_seconds: Option<i64>,
pub marker_timestamp: Option<i64>,
}
impl RowLiveness {
pub fn marker_live_at(&self, now_secs: i64) -> bool {
self.has_marker && self.expires_at_seconds.is_none_or(|s| s > now_secs)
}
pub fn merge(self, other: RowLiveness) -> RowLiveness {
match (self.has_marker, other.has_marker) {
(false, false) => RowLiveness::default(),
(true, false) => self,
(false, true) => other,
(true, true) => {
let self_wins = match (self.marker_timestamp, other.marker_timestamp) {
(Some(a), Some(b)) if a != b => a > b,
_ => match (self.expires_at_seconds, other.expires_at_seconds) {
(None, _) => true,
(_, None) => false,
(Some(a), Some(b)) => a >= b,
},
};
if self_wins {
self
} else {
other
}
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompactionRowData {
RangeMarker {
start: CompactionBound,
end: CompactionBound,
deletion_time: i64,
local_deletion_time: i32,
},
PartitionDelete {
deletion_time: i64,
local_deletion_time: i32,
},
Tombstone {
deletion_time: i64,
local_deletion_time: i32,
clustering: Vec<(String, Value)>,
},
Live {
simple: Vec<SimpleCell>,
complex: Vec<ComplexColumn>,
row_deletion: Option<(i64, i32)>,
row_liveness: RowLiveness,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct SimpleCell {
pub column: String,
pub value: Value,
pub timestamp: i64,
pub ttl: Option<u32>,
pub local_deletion_time: Option<i32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComplexColumn {
pub column: String,
pub complex_deletion: Option<(i64, i32)>,
pub elements: Vec<ComplexElement>,
pub collapsed_value: Value,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComplexElement {
pub cell_path: Vec<u8>,
pub value: Option<Value>,
pub decoded_key: Option<Value>,
pub timestamp: i64,
pub ttl: Option<u32>,
pub local_deletion_time: Option<i32>,
pub is_deleted: bool,
pub has_empty_value: bool,
}
#[cfg(test)]
mod row_liveness_tests {
use super::RowLiveness;
fn marker(ts: i64, expires_at_seconds: Option<i64>) -> RowLiveness {
RowLiveness {
has_marker: true,
expires_at_seconds,
marker_timestamp: Some(ts),
}
}
#[test]
fn newer_expired_ttl_supersedes_older_live_forever() {
let gen_a = marker(200, None);
let gen_b = marker(300, Some(1_000));
let merged = gen_a.merge(gen_b);
assert_eq!(merged.marker_timestamp, Some(300));
assert_eq!(merged.expires_at_seconds, Some(1_000));
assert!(
!merged.marker_live_at(2_000),
"newer expired-TTL marker must hide the key-only row (timestamp-LWW)"
);
let merged_rev = gen_b.merge(gen_a);
assert_eq!(merged_rev.marker_timestamp, Some(300));
assert!(!merged_rev.marker_live_at(2_000));
}
#[test]
fn newer_live_forever_supersedes_older_expired_ttl() {
let gen_a = marker(200, Some(500));
let gen_b = marker(300, None);
let merged = gen_a.merge(gen_b);
assert_eq!(merged.marker_timestamp, Some(300));
assert_eq!(merged.expires_at_seconds, None);
assert!(
merged.marker_live_at(2_000),
"newer live-forever marker must keep the key-only row visible (timestamp-LWW)"
);
assert!(gen_b.merge(gen_a).marker_live_at(2_000));
}
#[test]
fn equal_timestamps_tie_break_on_later_expiry() {
let live_forever = marker(300, None);
let ttl = marker(300, Some(500));
assert_eq!(live_forever.merge(ttl).expires_at_seconds, None);
assert_eq!(ttl.merge(live_forever).expires_at_seconds, None);
let earlier = marker(300, Some(400));
let later = marker(300, Some(900));
assert_eq!(earlier.merge(later).expires_at_seconds, Some(900));
assert_eq!(later.merge(earlier).expires_at_seconds, Some(900));
}
#[test]
fn absent_marker_carries_the_present_one_through() {
let present = marker(300, Some(500));
assert_eq!(present.merge(RowLiveness::default()), present);
assert_eq!(RowLiveness::default().merge(present), present);
assert_eq!(
RowLiveness::default().merge(RowLiveness::default()),
RowLiveness::default()
);
}
}