use fsqlite_error::{FrankenError, Result};
use fsqlite_types::{CommitSeq, PageNumber, TxnId};
use tracing::{debug, trace, warn};
use crate::cell_visibility::{CellDeltaKind, CellKey};
pub const CELL_DELTA_FRAME_MARKER: u32 = 0;
pub const CELL_DELTA_HEADER_SIZE: usize = 45;
pub const CELL_DELTA_CHECKSUM_SIZE: usize = 4;
pub const CELL_DELTA_MIN_FRAME_SIZE: usize = CELL_DELTA_HEADER_SIZE + CELL_DELTA_CHECKSUM_SIZE;
pub const CELL_DELTA_MAX_DATA_LEN: u32 = 65_536;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CellDeltaOp {
Insert = 1,
Update = 2,
Delete = 3,
}
impl CellDeltaOp {
#[must_use]
pub fn from_byte(b: u8) -> Option<Self> {
match b {
1 => Some(Self::Insert),
2 => Some(Self::Update),
3 => Some(Self::Delete),
_ => None,
}
}
#[must_use]
pub fn from_kind(kind: &CellDeltaKind) -> Self {
match kind {
CellDeltaKind::Insert => Self::Insert,
CellDeltaKind::Update => Self::Update,
CellDeltaKind::Delete => Self::Delete,
}
}
#[must_use]
pub fn to_kind(self) -> CellDeltaKind {
match self {
Self::Insert => CellDeltaKind::Insert,
Self::Update => CellDeltaKind::Update,
Self::Delete => CellDeltaKind::Delete,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CellDeltaWalFrame {
pub page_number: PageNumber,
pub key_digest: [u8; 16],
pub op: CellDeltaOp,
pub commit_seq: CommitSeq,
pub txn_id: TxnId,
pub cell_data: Vec<u8>,
}
impl CellDeltaWalFrame {
#[must_use]
pub fn new(
page_number: PageNumber,
cell_key: &CellKey,
op: CellDeltaOp,
commit_seq: CommitSeq,
txn_id: TxnId,
cell_data: Vec<u8>,
) -> Self {
Self {
page_number,
key_digest: cell_key.key_digest,
op,
commit_seq,
txn_id,
cell_data,
}
}
#[must_use]
pub fn serialized_size(&self) -> usize {
CELL_DELTA_HEADER_SIZE + self.cell_data.len() + CELL_DELTA_CHECKSUM_SIZE
}
pub fn serialize(&self) -> Result<Vec<u8>> {
if self.cell_data.len() > CELL_DELTA_MAX_DATA_LEN as usize {
return Err(FrankenError::WalCorrupt {
detail: format!(
"cell-delta payload too large: {} bytes exceeds max {CELL_DELTA_MAX_DATA_LEN}",
self.cell_data.len()
),
});
}
if matches!(self.op, CellDeltaOp::Delete) && !self.cell_data.is_empty() {
return Err(FrankenError::WalCorrupt {
detail: "delete cell-delta frame cannot carry cell data".to_owned(),
});
}
let total_size = self.serialized_size();
let mut buf = Vec::with_capacity(total_size);
buf.extend_from_slice(&CELL_DELTA_FRAME_MARKER.to_be_bytes());
buf.extend_from_slice(&self.page_number.get().to_be_bytes());
buf.extend_from_slice(&self.key_digest);
buf.push(self.op as u8);
buf.extend_from_slice(&self.commit_seq.get().to_be_bytes());
buf.extend_from_slice(&self.txn_id.get().to_be_bytes());
let data_len =
u32::try_from(self.cell_data.len()).map_err(|_| FrankenError::WalCorrupt {
detail: format!(
"cell-delta payload length {} does not fit in u32",
self.cell_data.len()
),
})?;
buf.extend_from_slice(&data_len.to_be_bytes());
buf.extend_from_slice(&self.cell_data);
let checksum = crc32c_checksum(&buf);
buf.extend_from_slice(&checksum.to_be_bytes());
trace!(
pgno = self.page_number.get(),
op = ?self.op,
commit_seq = self.commit_seq.get(),
data_len = self.cell_data.len(),
frame_size = buf.len(),
"cell_delta_wal_frame_serialized"
);
Ok(buf)
}
#[must_use]
pub fn deserialize(buf: &[u8]) -> Option<Self> {
if buf.len() < CELL_DELTA_MIN_FRAME_SIZE {
warn!(
buf_len = buf.len(),
min_size = CELL_DELTA_MIN_FRAME_SIZE,
"cell_delta_wal_frame_too_short"
);
return None;
}
let marker = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
if marker != CELL_DELTA_FRAME_MARKER {
return None; }
let page_number = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
let page_number = PageNumber::new(page_number)?;
let mut key_digest = [0u8; 16];
key_digest.copy_from_slice(&buf[8..24]);
let op = CellDeltaOp::from_byte(buf[24])?;
let commit_seq = u64::from_be_bytes([
buf[25], buf[26], buf[27], buf[28], buf[29], buf[30], buf[31], buf[32],
]);
let commit_seq = CommitSeq::new(commit_seq);
let txn_id = u64::from_be_bytes([
buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39], buf[40],
]);
let txn_id = TxnId::new(txn_id)?;
let data_len = u32::from_be_bytes([buf[41], buf[42], buf[43], buf[44]]);
if data_len > CELL_DELTA_MAX_DATA_LEN {
warn!(
data_len,
max = CELL_DELTA_MAX_DATA_LEN,
"cell_delta_wal_frame_data_too_large"
);
return None;
}
if matches!(op, CellDeltaOp::Delete) && data_len != 0 {
warn!(data_len, "cell_delta_wal_frame_delete_payload");
return None;
}
let expected_total_size =
CELL_DELTA_HEADER_SIZE + data_len as usize + CELL_DELTA_CHECKSUM_SIZE;
if buf.len() < expected_total_size {
warn!(
buf_len = buf.len(),
expected_size = expected_total_size,
"cell_delta_wal_frame_truncated"
);
return None;
}
if buf.len() > expected_total_size {
warn!(
buf_len = buf.len(),
expected_size = expected_total_size,
trailing_bytes = buf.len() - expected_total_size,
"cell_delta_wal_frame_trailing_bytes"
);
return None;
}
let data_start = CELL_DELTA_HEADER_SIZE;
let data_end = data_start + data_len as usize;
let cell_data = buf[data_start..data_end].to_vec();
let checksum_start = data_end;
let stored_checksum = u32::from_be_bytes([
buf[checksum_start],
buf[checksum_start + 1],
buf[checksum_start + 2],
buf[checksum_start + 3],
]);
let computed_checksum = crc32c_checksum(&buf[..checksum_start]);
if stored_checksum != computed_checksum {
warn!(
stored = stored_checksum,
computed = computed_checksum,
"cell_delta_wal_frame_checksum_mismatch"
);
return None;
}
trace!(
pgno = page_number.get(),
op = ?op,
commit_seq = commit_seq.get(),
data_len,
"cell_delta_wal_frame_deserialized"
);
Some(Self {
page_number,
key_digest,
op,
commit_seq,
txn_id,
cell_data,
})
}
#[inline]
#[must_use]
pub fn is_cell_delta_frame(buf: &[u8]) -> bool {
if buf.len() < CELL_DELTA_MIN_FRAME_SIZE {
return false;
}
let marker = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
if marker != CELL_DELTA_FRAME_MARKER {
return false;
}
if PageNumber::new(u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]])).is_none() {
return false;
}
let Some(op) = CellDeltaOp::from_byte(buf[24]) else {
return false;
};
let txn_id = u64::from_be_bytes([
buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39], buf[40],
]);
if TxnId::new(txn_id).is_none() {
return false;
}
let data_len = u32::from_be_bytes([buf[41], buf[42], buf[43], buf[44]]);
if data_len > CELL_DELTA_MAX_DATA_LEN
|| (matches!(op, CellDeltaOp::Delete) && data_len != 0)
{
return false;
}
let Some(expected_total_size) = CELL_DELTA_HEADER_SIZE
.checked_add(data_len as usize)
.and_then(|len| len.checked_add(CELL_DELTA_CHECKSUM_SIZE))
else {
return false;
};
if buf.len() != expected_total_size {
return false;
}
let checksum_start = CELL_DELTA_HEADER_SIZE + data_len as usize;
let stored_checksum = u32::from_be_bytes([
buf[checksum_start],
buf[checksum_start + 1],
buf[checksum_start + 2],
buf[checksum_start + 3],
]);
stored_checksum == crc32c_checksum(&buf[..checksum_start])
}
}
#[inline]
fn crc32c_checksum(data: &[u8]) -> u32 {
crc32c::crc32c(data)
}
pub fn serialize_cell_delta_batch(frames: &[CellDeltaWalFrame]) -> Result<Vec<u8>> {
let mut buf = Vec::new();
for frame in frames {
buf.extend_from_slice(&frame.serialize()?);
}
debug!(
frame_count = frames.len(),
total_bytes = buf.len(),
"cell_delta_batch_serialized"
);
Ok(buf)
}
#[must_use]
pub fn deserialize_cell_delta_batch(buf: &[u8]) -> Vec<CellDeltaWalFrame> {
let mut frames = Vec::new();
let mut offset = 0;
while offset < buf.len() {
let remaining = &buf[offset..];
if remaining.len() < CELL_DELTA_HEADER_SIZE {
break;
}
let marker = u32::from_be_bytes([remaining[0], remaining[1], remaining[2], remaining[3]]);
if marker != CELL_DELTA_FRAME_MARKER {
break;
}
if PageNumber::new(u32::from_be_bytes([
remaining[4],
remaining[5],
remaining[6],
remaining[7],
]))
.is_none()
{
break;
}
let Some(op) = CellDeltaOp::from_byte(remaining[24]) else {
break;
};
let txn_id = u64::from_be_bytes([
remaining[33],
remaining[34],
remaining[35],
remaining[36],
remaining[37],
remaining[38],
remaining[39],
remaining[40],
]);
if TxnId::new(txn_id).is_none() {
break;
}
let data_len =
u32::from_be_bytes([remaining[41], remaining[42], remaining[43], remaining[44]]);
if data_len > CELL_DELTA_MAX_DATA_LEN
|| (matches!(op, CellDeltaOp::Delete) && data_len != 0)
{
break;
}
let Some(frame_size) = CELL_DELTA_HEADER_SIZE
.checked_add(data_len as usize)
.and_then(|len| len.checked_add(CELL_DELTA_CHECKSUM_SIZE))
else {
break;
};
if remaining.len() < frame_size {
break;
}
if let Some(frame) = CellDeltaWalFrame::deserialize(&remaining[..frame_size]) {
frames.push(frame);
offset += frame_size;
} else {
break;
}
}
debug!(
frame_count = frames.len(),
bytes_consumed = offset,
"cell_delta_batch_deserialized"
);
frames
}
#[derive(Debug, Clone, Default)]
pub struct CellDeltaRecoverySummary {
pub cell_delta_frames: u64,
pub full_page_frames: u64,
pub cell_delta_bytes: u64,
pub pages_with_cell_deltas: u64,
pub deltas_inserted: u64,
}
impl CellDeltaRecoverySummary {
pub fn log_summary(&self) {
tracing::info!(
cell_delta_frames = self.cell_delta_frames,
full_page_frames = self.full_page_frames,
cell_delta_bytes = self.cell_delta_bytes,
pages_with_cell_deltas = self.pages_with_cell_deltas,
deltas_inserted = self.deltas_inserted,
"wal_recovery_summary"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use crate::cell_visibility::{CellResolve, CellVisibilityLog};
use fsqlite_types::{BtreeRef, SemanticKeyKind, TableId, TxnEpoch, TxnToken};
use proptest::{
collection::vec as prop_vec,
prelude::*,
test_runner::{Config as ProptestConfig, TestCaseError},
};
const MVCC_DURABILITY_PROPTEST_CASES: u32 = 64;
fn make_cell_key() -> CellKey {
CellKey {
btree: BtreeRef::Table(TableId::new(1)),
kind: SemanticKeyKind::TableRow,
key_digest: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
}
}
fn serialize(frame: &CellDeltaWalFrame) -> Vec<u8> {
frame.serialize().expect("test cell-delta frame is valid")
}
fn durable_cell_key_from_digest(key_digest: [u8; 16]) -> CellKey {
CellKey {
btree: BtreeRef::Table(TableId::new(1)),
kind: SemanticKeyKind::TableRow,
key_digest,
}
}
fn replay_committed_frame(
log: &CellVisibilityLog,
frame: &CellDeltaWalFrame,
) -> std::result::Result<(), String> {
let key = durable_cell_key_from_digest(frame.key_digest);
let epoch = u32::try_from(frame.txn_id.get())
.map_err(|err| format!("txn epoch conversion failed: {err}"))?;
let txn = TxnToken::new(frame.txn_id, TxnEpoch::new(epoch));
let delta_idx = match frame.op {
CellDeltaOp::Insert => {
log.record_insert(key, frame.page_number, frame.cell_data.clone(), txn)
}
CellDeltaOp::Update => {
log.record_update(key, frame.page_number, frame.cell_data.clone(), txn)
}
CellDeltaOp::Delete => log.record_delete(key, frame.page_number, txn),
}
.ok_or_else(|| {
format!(
"replay exceeded visibility-log budget for page {}",
frame.page_number.get()
)
})?;
log.commit_delta(delta_idx, frame.commit_seq);
Ok(())
}
fn resolve_committed_cells(
log: &CellVisibilityLog,
cells: &BTreeMap<(u32, [u8; 16]), (PageNumber, CellKey)>,
snapshot: CommitSeq,
) -> BTreeMap<(u32, [u8; 16]), CellResolve> {
cells
.iter()
.map(|(identity, (page, key))| (*identity, log.resolve_state(*page, key, snapshot)))
.collect()
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(MVCC_DURABILITY_PROPTEST_CASES))]
#[test]
fn metamorphic_durability_replays_committed_cell_deltas_after_crash(
raw_writes in prop_vec(
(1_u16..=64, 0_u8..=2, prop_vec(any::<u8>(), 0..=32)),
1..=32,
),
) {
let live_log = CellVisibilityLog::new(1 << 20);
let mut frames = Vec::with_capacity(raw_writes.len());
let mut cells = BTreeMap::new();
for (idx, (rowid, op_selector, raw_payload)) in raw_writes.iter().enumerate() {
let page = PageNumber::new(1 + u32::from(*rowid % 16))
.ok_or_else(|| TestCaseError::fail("generated page number was invalid"))?;
let key = CellKey::table_row(
BtreeRef::Table(TableId::new(1)),
i64::from(*rowid),
);
let op = match op_selector {
0 => CellDeltaOp::Insert,
1 => CellDeltaOp::Update,
_ => CellDeltaOp::Delete,
};
let cell_data = if matches!(op, CellDeltaOp::Delete) {
Vec::new()
} else {
raw_payload.clone()
};
let commit_seq = CommitSeq::new(idx as u64 + 1);
let txn_id = TxnId::new(idx as u64 + 1)
.ok_or_else(|| TestCaseError::fail("generated txn id was invalid"))?;
let txn_epoch = u32::try_from(idx + 1)
.map_err(|err| TestCaseError::fail(format!("txn epoch conversion failed: {err}")))?;
let txn = TxnToken::new(txn_id, TxnEpoch::new(txn_epoch));
let delta_idx = match op {
CellDeltaOp::Insert => {
live_log.record_insert(key, page, cell_data.clone(), txn)
}
CellDeltaOp::Update => {
live_log.record_update(key, page, cell_data.clone(), txn)
}
CellDeltaOp::Delete => live_log.record_delete(key, page, txn),
}
.ok_or_else(|| {
TestCaseError::fail(format!(
"live visibility log budget exceeded for rowid {rowid}"
))
})?;
live_log.commit_delta(delta_idx, commit_seq);
frames.push(CellDeltaWalFrame::new(
page,
&key,
op,
commit_seq,
txn_id,
cell_data,
));
cells.insert((page.get(), key.key_digest), (page, key));
}
let snapshot = CommitSeq::new(raw_writes.len() as u64 + 1);
let expected = resolve_committed_cells(&live_log, &cells, snapshot);
let wal_bytes = serialize_cell_delta_batch(&frames)
.map_err(|err| TestCaseError::fail(format!("serialize WAL batch failed: {err}")))?;
let recovered_frames = deserialize_cell_delta_batch(&wal_bytes);
prop_assert_eq!(&recovered_frames, &frames);
let recovered_log = CellVisibilityLog::new(1 << 20);
for frame in &recovered_frames {
replay_committed_frame(&recovered_log, frame)
.map_err(TestCaseError::fail)?;
}
let recovered = resolve_committed_cells(&recovered_log, &cells, snapshot);
prop_assert_eq!(recovered, expected);
}
}
#[test]
fn test_cell_delta_frame_round_trip() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(12345),
txn_id: TxnId::new(67890).unwrap(),
cell_data: vec![0xDE, 0xAD, 0xBE, 0xEF],
};
let serialized = serialize(&frame);
let deserialized = CellDeltaWalFrame::deserialize(&serialized);
assert_eq!(deserialized, Some(frame));
}
#[test]
fn test_cell_delta_frame_matches_wal_crate_wire_format() {
let key_digest = [1, 3, 3, 7, 8, 13, 21, 34, 55, 89, 144, 233, 5, 8, 13, 21];
let mvcc_frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest,
op: CellDeltaOp::Update,
commit_seq: CommitSeq::new(12345),
txn_id: TxnId::new(67890).unwrap(),
cell_data: vec![0xDE, 0xAD, 0xBE, 0xEF],
};
let wal_frame = fsqlite_wal::CellDeltaWalFrame::new(
mvcc_frame.page_number,
key_digest,
fsqlite_wal::CellOp::Update,
mvcc_frame.commit_seq,
mvcc_frame.txn_id,
mvcc_frame.cell_data.clone(),
);
assert_eq!(
serialize(&mvcc_frame),
wal_frame.serialize().expect("WAL frame should serialize")
);
}
#[test]
fn test_cell_delta_frame_checksum() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
op: CellDeltaOp::Update,
commit_seq: CommitSeq::new(100),
txn_id: TxnId::new(200).unwrap(),
cell_data: vec![1, 2, 3, 4, 5],
};
let mut serialized = serialize(&frame);
let corrupt_idx = serialized.len() / 2;
serialized[corrupt_idx] ^= 0xFF;
assert!(CellDeltaWalFrame::deserialize(&serialized).is_none());
}
#[test]
fn test_cell_delta_frame_variable_length() {
let frame_empty = CellDeltaWalFrame {
page_number: PageNumber::new(1).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Delete,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![],
};
let ser = serialize(&frame_empty);
assert_eq!(CellDeltaWalFrame::deserialize(&ser), Some(frame_empty));
let frame_100 = CellDeltaWalFrame {
page_number: PageNumber::new(2).unwrap(),
key_digest: [1; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(2),
txn_id: TxnId::new(2).unwrap(),
cell_data: vec![0xAB; 100],
};
let ser = serialize(&frame_100);
assert_eq!(CellDeltaWalFrame::deserialize(&ser), Some(frame_100));
let frame_4000 = CellDeltaWalFrame {
page_number: PageNumber::new(3).unwrap(),
key_digest: [2; 16],
op: CellDeltaOp::Update,
commit_seq: CommitSeq::new(3),
txn_id: TxnId::new(3).unwrap(),
cell_data: vec![0xCD; 4000],
};
let ser = serialize(&frame_4000);
assert_eq!(CellDeltaWalFrame::deserialize(&ser), Some(frame_4000));
}
#[test]
fn test_cell_delta_frame_marker_word() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![1, 2, 3],
};
let serialized = serialize(&frame);
let marker =
u32::from_be_bytes([serialized[0], serialized[1], serialized[2], serialized[3]]);
let page_number =
u32::from_be_bytes([serialized[4], serialized[5], serialized[6], serialized[7]]);
assert_eq!(marker, CELL_DELTA_FRAME_MARKER);
assert_eq!(page_number, 42);
assert!(CellDeltaWalFrame::is_cell_delta_frame(&serialized));
let fake_page_frame = [0x00, 0x00, 0x00, 0x01]; assert!(!CellDeltaWalFrame::is_cell_delta_frame(&fake_page_frame));
let fake_marker_page_frame = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, ];
assert!(!CellDeltaWalFrame::is_cell_delta_frame(
&fake_marker_page_frame
));
}
#[test]
fn test_cell_delta_op_conversion() {
assert_eq!(CellDeltaOp::from_byte(1), Some(CellDeltaOp::Insert));
assert_eq!(CellDeltaOp::from_byte(2), Some(CellDeltaOp::Update));
assert_eq!(CellDeltaOp::from_byte(3), Some(CellDeltaOp::Delete));
assert_eq!(CellDeltaOp::from_byte(0), None);
assert_eq!(CellDeltaOp::from_byte(4), None);
assert_eq!(CellDeltaOp::from_byte(255), None);
}
#[test]
fn test_batch_serialization() {
let frames = vec![
CellDeltaWalFrame {
page_number: PageNumber::new(1).unwrap(),
key_digest: [1; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(10),
txn_id: TxnId::new(100).unwrap(),
cell_data: vec![1, 2, 3],
},
CellDeltaWalFrame {
page_number: PageNumber::new(2).unwrap(),
key_digest: [2; 16],
op: CellDeltaOp::Update,
commit_seq: CommitSeq::new(20),
txn_id: TxnId::new(200).unwrap(),
cell_data: vec![4, 5, 6, 7],
},
CellDeltaWalFrame {
page_number: PageNumber::new(3).unwrap(),
key_digest: [3; 16],
op: CellDeltaOp::Delete,
commit_seq: CommitSeq::new(30),
txn_id: TxnId::new(300).unwrap(),
cell_data: vec![],
},
];
let serialized =
serialize_cell_delta_batch(&frames).expect("test cell-delta batch is valid");
let deserialized = deserialize_cell_delta_batch(&serialized);
assert_eq!(deserialized, frames);
}
#[test]
fn test_serialized_size() {
let frame_empty = CellDeltaWalFrame {
page_number: PageNumber::new(1).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Delete,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![],
};
assert_eq!(frame_empty.serialized_size(), 49);
assert_eq!(serialize(&frame_empty).len(), 49);
let frame_100 = CellDeltaWalFrame {
page_number: PageNumber::new(1).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![0; 100],
};
assert_eq!(frame_100.serialized_size(), 149);
assert_eq!(serialize(&frame_100).len(), 149);
}
#[test]
fn test_truncated_frame_rejected() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![1, 2, 3, 4, 5],
};
let serialized = serialize(&frame);
for truncate_at in [0, 10, 20, 40, serialized.len() - 1] {
let truncated = &serialized[..truncate_at];
assert!(
CellDeltaWalFrame::deserialize(truncated).is_none(),
"Should reject frame truncated at {truncate_at}"
);
}
}
#[test]
fn test_trailing_bytes_rejected() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![1, 2, 3],
};
let mut serialized = serialize(&frame);
serialized.extend_from_slice(b"junk");
assert!(
CellDeltaWalFrame::deserialize(&serialized).is_none(),
"frame decoder must reject bytes not covered by the frame checksum"
);
}
#[test]
fn test_delete_payload_rejected() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Delete,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![1, 2, 3],
};
assert!(frame.serialize().is_err());
}
#[test]
fn test_serialize_oversized_payload_rejected() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![0; CELL_DELTA_MAX_DATA_LEN as usize + 1],
};
assert!(frame.serialize().is_err());
}
#[test]
fn test_serialize_batch_rejects_oversized_payload() {
let frame = CellDeltaWalFrame {
page_number: PageNumber::new(42).unwrap(),
key_digest: [0; 16],
op: CellDeltaOp::Insert,
commit_seq: CommitSeq::new(1),
txn_id: TxnId::new(1).unwrap(),
cell_data: vec![0; CELL_DELTA_MAX_DATA_LEN as usize + 1],
};
assert!(serialize_cell_delta_batch(&[frame]).is_err());
}
#[test]
fn test_invalid_page_number_rejected() {
let mut buf = CELL_DELTA_FRAME_MARKER.to_be_bytes().to_vec();
buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&[0u8; 16]); buf.push(1); buf.extend_from_slice(&1u64.to_be_bytes()); buf.extend_from_slice(&1u64.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); let checksum = crc32c_checksum(&buf);
buf.extend_from_slice(&checksum.to_be_bytes());
assert!(CellDeltaWalFrame::deserialize(&buf).is_none());
}
#[test]
fn test_invalid_txn_id_rejected() {
let mut buf = CELL_DELTA_FRAME_MARKER.to_be_bytes().to_vec();
buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&[0u8; 16]); buf.push(1); buf.extend_from_slice(&1u64.to_be_bytes()); buf.extend_from_slice(&0u64.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); let checksum = crc32c_checksum(&buf);
buf.extend_from_slice(&checksum.to_be_bytes());
assert!(CellDeltaWalFrame::deserialize(&buf).is_none());
}
#[test]
fn test_invalid_op_rejected() {
let mut buf = CELL_DELTA_FRAME_MARKER.to_be_bytes().to_vec();
buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&[0u8; 16]); buf.push(99); buf.extend_from_slice(&1u64.to_be_bytes()); buf.extend_from_slice(&1u64.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); let checksum = crc32c_checksum(&buf);
buf.extend_from_slice(&checksum.to_be_bytes());
assert!(CellDeltaWalFrame::deserialize(&buf).is_none());
}
#[test]
fn test_from_cell_key() {
let cell_key = make_cell_key();
let frame = CellDeltaWalFrame::new(
PageNumber::new(42).unwrap(),
&cell_key,
CellDeltaOp::Insert,
CommitSeq::new(100),
TxnId::new(200).unwrap(),
vec![1, 2, 3],
);
assert_eq!(frame.key_digest, cell_key.key_digest);
}
}