use fsqlite_error::{FrankenError, Result};
pub const WAL_INDEX_HASH_MULTIPLIER: u32 = 383;
pub const WAL_INDEX_PAGE_ARRAY_ENTRIES: usize = 4096;
pub const WAL_INDEX_HASH_SLOTS: usize = 8192;
pub const WAL_INDEX_HASH_MASK: u32 = 8191;
pub const WAL_SHM_SEGMENT_BYTES: usize = 32 * 1024;
pub const WAL_SHM_HASH_BYTES: usize = WAL_INDEX_HASH_SLOTS * 2;
pub const WAL_SHM_PAGE_ARRAY_BYTES: usize = WAL_INDEX_PAGE_ARRAY_ENTRIES * 4;
pub const WAL_SHM_FIRST_HEADER_BYTES: usize = 136;
pub const WAL_SHM_FIRST_HEADER_U32_SLOTS: usize = WAL_SHM_FIRST_HEADER_BYTES.div_ceil(4);
pub const WAL_SHM_FIRST_USABLE_PAGE_ENTRIES: usize =
WAL_INDEX_PAGE_ARRAY_ENTRIES - WAL_SHM_FIRST_HEADER_U32_SLOTS;
pub const WAL_SHM_SUBSEQUENT_USABLE_PAGE_ENTRIES: usize = WAL_INDEX_PAGE_ARRAY_ENTRIES;
pub const WAL_INDEX_VERSION: u32 = 3_007_000;
pub const WAL_INDEX_HDR_BYTES: usize = 48;
pub const WAL_CKPT_INFO_BYTES: usize = 40;
pub const WAL_READ_MARK_COUNT: usize = 5;
pub const WAL_LOCK_SLOT_COUNT: usize = 8;
pub const WAL_WRITE_LOCK: usize = 0;
pub const WAL_CKPT_LOCK: usize = 1;
pub const WAL_RECOVER_LOCK: usize = 2;
pub const WAL_READ_LOCK_BASE: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WalIndexHdr {
pub i_version: u32,
pub unused: u32,
pub i_change: u32,
pub is_init: u8,
pub big_end_cksum: u8,
pub sz_page: u16,
pub mx_frame: u32,
pub n_page: u32,
pub a_frame_cksum: [u32; 2],
pub a_salt: [u32; 2],
pub a_cksum: [u32; 2],
}
impl WalIndexHdr {
pub fn from_bytes(buf: &[u8]) -> Result<Self> {
if buf.len() < WAL_INDEX_HDR_BYTES {
return Err(FrankenError::WalCorrupt {
detail: format!(
"WalIndexHdr too small: expected >= {WAL_INDEX_HDR_BYTES}, got {}",
buf.len()
),
});
}
Ok(Self {
i_version: decode_native_u32(read4(buf, 0)),
unused: decode_native_u32(read4(buf, 4)),
i_change: decode_native_u32(read4(buf, 8)),
is_init: buf[12],
big_end_cksum: buf[13],
sz_page: u16::from_ne_bytes([buf[14], buf[15]]),
mx_frame: decode_native_u32(read4(buf, 16)),
n_page: decode_native_u32(read4(buf, 20)),
a_frame_cksum: [
decode_native_u32(read4(buf, 24)),
decode_native_u32(read4(buf, 28)),
],
a_salt: [
decode_native_u32(read4(buf, 32)),
decode_native_u32(read4(buf, 36)),
],
a_cksum: [
decode_native_u32(read4(buf, 40)),
decode_native_u32(read4(buf, 44)),
],
})
}
#[must_use]
pub fn to_bytes(&self) -> [u8; WAL_INDEX_HDR_BYTES] {
let mut buf = [0u8; WAL_INDEX_HDR_BYTES];
write4(&mut buf, 0, self.i_version);
write4(&mut buf, 4, self.unused);
write4(&mut buf, 8, self.i_change);
buf[12] = self.is_init;
buf[13] = self.big_end_cksum;
buf[14..16].copy_from_slice(&self.sz_page.to_ne_bytes());
write4(&mut buf, 16, self.mx_frame);
write4(&mut buf, 20, self.n_page);
write4(&mut buf, 24, self.a_frame_cksum[0]);
write4(&mut buf, 28, self.a_frame_cksum[1]);
write4(&mut buf, 32, self.a_salt[0]);
write4(&mut buf, 36, self.a_salt[1]);
write4(&mut buf, 40, self.a_cksum[0]);
write4(&mut buf, 44, self.a_cksum[1]);
buf
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WalCkptInfo {
pub n_backfill: u32,
pub a_read_mark: [u32; WAL_READ_MARK_COUNT],
pub a_lock: [u8; WAL_LOCK_SLOT_COUNT],
pub n_backfill_attempted: u32,
pub not_used0: u32,
}
impl WalCkptInfo {
pub fn from_bytes(buf: &[u8]) -> Result<Self> {
if buf.len() < WAL_CKPT_INFO_BYTES {
return Err(FrankenError::WalCorrupt {
detail: format!(
"WalCkptInfo too small: expected >= {WAL_CKPT_INFO_BYTES}, got {}",
buf.len()
),
});
}
let mut a_read_mark = [0u32; WAL_READ_MARK_COUNT];
for (i, mark) in a_read_mark.iter_mut().enumerate() {
*mark = decode_native_u32(read4(buf, 4 + i * 4));
}
let mut a_lock = [0u8; WAL_LOCK_SLOT_COUNT];
a_lock.copy_from_slice(&buf[24..32]);
Ok(Self {
n_backfill: decode_native_u32(read4(buf, 0)),
a_read_mark,
a_lock,
n_backfill_attempted: decode_native_u32(read4(buf, 32)),
not_used0: decode_native_u32(read4(buf, 36)),
})
}
#[must_use]
pub fn to_bytes(&self) -> [u8; WAL_CKPT_INFO_BYTES] {
let mut buf = [0u8; WAL_CKPT_INFO_BYTES];
write4(&mut buf, 0, self.n_backfill);
for (i, &mark) in self.a_read_mark.iter().enumerate() {
write4(&mut buf, 4 + i * 4, mark);
}
buf[24..32].copy_from_slice(&self.a_lock);
write4(&mut buf, 32, self.n_backfill_attempted);
write4(&mut buf, 36, self.not_used0);
buf
}
}
#[must_use]
pub fn wal_index_hdr_copies_match(buf: &[u8]) -> bool {
if buf.len() < 2 * WAL_INDEX_HDR_BYTES {
return false;
}
buf[..WAL_INDEX_HDR_BYTES] == buf[WAL_INDEX_HDR_BYTES..2 * WAL_INDEX_HDR_BYTES]
}
pub fn parse_shm_header(buf: &[u8]) -> Result<Option<(WalIndexHdr, WalCkptInfo)>> {
if buf.len() < WAL_SHM_FIRST_HEADER_BYTES {
return Err(FrankenError::WalCorrupt {
detail: format!(
"SHM header too small: expected >= {WAL_SHM_FIRST_HEADER_BYTES}, got {}",
buf.len()
),
});
}
if !wal_index_hdr_copies_match(buf) {
return Ok(None);
}
let hdr = WalIndexHdr::from_bytes(buf)?;
let ckpt = WalCkptInfo::from_bytes(&buf[2 * WAL_INDEX_HDR_BYTES..])?;
Ok(Some((hdr, ckpt)))
}
pub fn write_shm_header(buf: &mut [u8], hdr: &WalIndexHdr, ckpt: &WalCkptInfo) -> Result<()> {
if buf.len() < WAL_SHM_FIRST_HEADER_BYTES {
return Err(FrankenError::WalCorrupt {
detail: format!(
"SHM header buffer too small: expected >= {WAL_SHM_FIRST_HEADER_BYTES}, got {}",
buf.len()
),
});
}
let hdr_bytes = hdr.to_bytes();
buf[..WAL_INDEX_HDR_BYTES].copy_from_slice(&hdr_bytes);
buf[WAL_INDEX_HDR_BYTES..2 * WAL_INDEX_HDR_BYTES].copy_from_slice(&hdr_bytes);
let ckpt_bytes = ckpt.to_bytes();
buf[2 * WAL_INDEX_HDR_BYTES..WAL_SHM_FIRST_HEADER_BYTES].copy_from_slice(&ckpt_bytes);
Ok(())
}
fn read4(buf: &[u8], offset: usize) -> [u8; 4] {
let mut out = [0u8; 4];
out.copy_from_slice(&buf[offset..offset + 4]);
out
}
fn write4(buf: &mut [u8], offset: usize, value: u32) {
buf[offset..offset + 4].copy_from_slice(&encode_native_u32(value));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalIndexSegmentKind {
First,
Subsequent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WalHashLookup {
pub slot: u32,
pub one_based_index: u16,
pub page_number: u32,
}
#[derive(Debug, Clone)]
pub struct WalIndexHashSegment {
kind: WalIndexSegmentKind,
page_numbers: Vec<u32>,
hash_slots: [u16; WAL_INDEX_HASH_SLOTS],
}
impl WalIndexHashSegment {
#[must_use]
pub fn new(kind: WalIndexSegmentKind) -> Self {
Self {
kind,
page_numbers: Vec::with_capacity(usable_page_entries(kind)),
hash_slots: [0; WAL_INDEX_HASH_SLOTS],
}
}
#[must_use]
pub const fn kind(&self) -> WalIndexSegmentKind {
self.kind
}
#[must_use]
pub const fn capacity(&self) -> usize {
usable_page_entries(self.kind)
}
#[must_use]
pub fn len(&self) -> usize {
self.page_numbers.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.page_numbers.is_empty()
}
#[must_use]
pub fn hash_slots(&self) -> &[u16; WAL_INDEX_HASH_SLOTS] {
&self.hash_slots
}
pub fn insert(&mut self, page_number: u32) -> Result<u16> {
if self.page_numbers.len() >= self.capacity() {
return Err(FrankenError::DatabaseFull);
}
self.page_numbers.push(page_number);
let one_based_index = u16::try_from(self.page_numbers.len())
.map_err(|_| FrankenError::internal("WAL page-number index overflowed u16 capacity"))?;
let start_slot = wal_index_hash_slot(page_number);
let mut slot = start_slot;
loop {
let slot_usize = usize::try_from(slot).expect("hash slot must fit usize");
let existing = self.hash_slots[slot_usize];
if existing == 0 {
self.hash_slots[slot_usize] = one_based_index;
return Ok(one_based_index);
}
slot = (slot + 1) & WAL_INDEX_HASH_MASK;
if slot == start_slot {
return Err(FrankenError::DatabaseFull);
}
}
}
#[must_use]
pub fn lookup(&self, page_number: u32) -> Option<WalHashLookup> {
let start_slot = wal_index_hash_slot(page_number);
let mut slot = start_slot;
let mut best: Option<WalHashLookup> = None;
loop {
let slot_usize = usize::try_from(slot).expect("hash slot must fit usize");
let one_based = self.hash_slots[slot_usize];
if one_based == 0 {
break;
}
let idx = usize::from(one_based - 1);
if self.page_numbers[idx] == page_number {
if let Some(ref b) = best {
if one_based > b.one_based_index {
best = Some(WalHashLookup {
slot,
one_based_index: one_based,
page_number,
});
}
} else {
best = Some(WalHashLookup {
slot,
one_based_index: one_based,
page_number,
});
}
}
slot = (slot + 1) & WAL_INDEX_HASH_MASK;
if slot == start_slot {
break;
}
}
best
}
}
#[must_use]
pub const fn wal_index_hash_slot(page_number: u32) -> u32 {
page_number.wrapping_mul(WAL_INDEX_HASH_MULTIPLIER) & WAL_INDEX_HASH_MASK
}
#[must_use]
pub const fn simple_modulo_slot(page_number: u32) -> u32 {
page_number & WAL_INDEX_HASH_MASK
}
#[must_use]
pub const fn usable_page_entries(kind: WalIndexSegmentKind) -> usize {
match kind {
WalIndexSegmentKind::First => WAL_SHM_FIRST_USABLE_PAGE_ENTRIES,
WalIndexSegmentKind::Subsequent => WAL_SHM_SUBSEQUENT_USABLE_PAGE_ENTRIES,
}
}
#[must_use]
pub const fn encode_native_u32(value: u32) -> [u8; 4] {
value.to_ne_bytes()
}
#[must_use]
pub const fn decode_native_u32(bytes: [u8; 4]) -> u32 {
u32::from_ne_bytes(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wal_hash_function_basic() {
assert_eq!(wal_index_hash_slot(1), 383);
assert_eq!(wal_index_hash_slot(2), 766);
assert_eq!(wal_index_hash_slot(10), 3830);
for pgno in 1_u32..=100 {
let expected = pgno.wrapping_mul(383) & 8191;
assert_eq!(wal_index_hash_slot(pgno), expected);
}
}
#[test]
fn test_wal_hash_sequential_distribution() {
let mut buckets = vec![0_u16; WAL_INDEX_HASH_SLOTS];
for pgno in 1_u32..=u32::try_from(WAL_INDEX_PAGE_ARRAY_ENTRIES).expect("fits") {
let slot = usize::try_from(wal_index_hash_slot(pgno)).expect("slot fits");
buckets[slot] += 1;
}
let max_bucket = buckets.into_iter().max().unwrap_or(0);
assert!(max_bucket <= 1, "expected perfect spread, got {max_bucket}");
}
#[test]
fn test_wal_hash_vs_simple_modulo() {
let mut differences = 0_u32;
for pgno in 1_u32..=100 {
if wal_index_hash_slot(pgno) != simple_modulo_slot(pgno) {
differences += 1;
}
}
assert!(
differences >= 90,
"expected >=90 differing slots, got {differences}"
);
}
#[test]
fn test_wal_hash_zero_page() {
assert_eq!(wal_index_hash_slot(0), 0);
}
#[test]
fn test_wal_hash_large_page_numbers() {
let values = [8192_u32, 65_536_u32, 2_147_483_648_u32, u32::MAX];
for value in values {
let slot = wal_index_hash_slot(value);
assert!(slot <= WAL_INDEX_HASH_MASK);
}
}
#[test]
fn test_wal_hash_table_insert_lookup() {
let mut seg = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
seg.insert(42).expect("insert should succeed");
let lookup = seg.lookup(42).expect("lookup should find inserted page");
assert_eq!(lookup.page_number, 42);
assert_eq!(lookup.one_based_index, 1);
}
#[test]
fn test_wal_hash_table_collision_chain() {
let mut seg = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
let first = 22_u32;
let second = first + 8192_u32; let start_slot = wal_index_hash_slot(first);
assert_eq!(start_slot, wal_index_hash_slot(second));
seg.insert(first).expect("first insert should succeed");
seg.insert(second).expect("second insert should succeed");
let first_lookup = seg.lookup(first).expect("first page should be found");
let second_lookup = seg.lookup(second).expect("second page should be found");
assert_ne!(first_lookup.one_based_index, second_lookup.one_based_index);
assert_eq!(first_lookup.slot, start_slot);
assert_eq!(
second_lookup.slot,
(start_slot + 1) & WAL_INDEX_HASH_MASK,
"second colliding key should linear-probe to next slot"
);
}
#[test]
fn test_shm_first_segment_usable_entries() {
assert_eq!(WAL_SHM_FIRST_HEADER_BYTES, 136);
assert_eq!(WAL_SHM_FIRST_HEADER_U32_SLOTS, 34);
assert_eq!(usable_page_entries(WalIndexSegmentKind::First), 4062);
}
#[test]
fn test_shm_first_segment_capacity_enforced() {
let mut first = WalIndexHashSegment::new(WalIndexSegmentKind::First);
for pgno in 1_u32..=u32::try_from(WAL_SHM_FIRST_USABLE_PAGE_ENTRIES).expect("fits") {
first
.insert(pgno)
.expect("entry within first-segment capacity must succeed");
}
assert_eq!(first.len(), WAL_SHM_FIRST_USABLE_PAGE_ENTRIES);
let overflow = first.insert(99_999).expect_err("4063rd entry must fail");
assert!(matches!(overflow, FrankenError::DatabaseFull));
}
#[test]
fn test_lookup_correctness_across_segments() {
let mut first = WalIndexHashSegment::new(WalIndexSegmentKind::First);
let mut second = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
for pgno in 1_u32..=u32::try_from(WAL_SHM_FIRST_USABLE_PAGE_ENTRIES).expect("fits") {
first
.insert(pgno)
.expect("first-segment insert should succeed");
}
second
.insert(1_000_001)
.expect("second-segment insert should succeed");
assert!(
first.lookup(42).is_some(),
"page in first segment must be found"
);
assert!(
second.lookup(1_000_001).is_some(),
"page in second segment must be found"
);
assert!(first.lookup(9_999_999).is_none());
assert!(second.lookup(9_999_999).is_none());
}
#[test]
fn test_shm_subsequent_segment_full_entries() {
assert_eq!(usable_page_entries(WalIndexSegmentKind::Subsequent), 4096);
assert_eq!(WAL_SHM_PAGE_ARRAY_BYTES, 16_384);
assert_eq!(WAL_SHM_HASH_BYTES, 16_384);
assert_eq!(WAL_SHM_SEGMENT_BYTES, 32 * 1024);
}
#[test]
fn test_shm_native_byte_order() {
let value = 0x12_34_56_78_u32;
let encoded = encode_native_u32(value);
assert_eq!(decode_native_u32(encoded), value);
if cfg!(target_endian = "little") {
assert_eq!(encoded, value.to_le_bytes());
} else {
assert_eq!(encoded, value.to_be_bytes());
}
}
#[test]
fn test_wal_hash_interop_c_sqlite() {
let cases = [
(1_u32, 383_u32),
(2, 766),
(22, 234),
(4096, (4096 * 383) & 8191),
(8193, (8193 * 383) & 8191),
];
for (pgno, expected_slot) in cases {
assert_eq!(wal_index_hash_slot(pgno), expected_slot, "pgno={pgno}");
}
}
#[test]
fn test_wal_index_header_layout() {
assert_eq!(WAL_INDEX_HDR_BYTES, 48);
assert_eq!(
2 * WAL_INDEX_HDR_BYTES + WAL_CKPT_INFO_BYTES,
WAL_SHM_FIRST_HEADER_BYTES
);
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 42,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 100,
n_page: 50,
a_frame_cksum: [0xAAAA_BBBB, 0xCCCC_DDDD],
a_salt: [0x1111_2222, 0x3333_4444],
a_cksum: [0x5555_6666, 0x7777_8888],
};
let bytes = hdr.to_bytes();
assert_eq!(bytes.len(), 48);
assert_eq!(decode_native_u32(read4(&bytes, 0)), WAL_INDEX_VERSION);
assert_eq!(u16::from_ne_bytes([bytes[14], bytes[15]]), 4096);
assert_eq!(decode_native_u32(read4(&bytes, 16)), 100);
assert_eq!(decode_native_u32(read4(&bytes, 20)), 50);
}
#[test]
fn test_wal_index_header_duplication() {
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 7,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 50,
n_page: 25,
a_frame_cksum: [1, 2],
a_salt: [3, 4],
a_cksum: [5, 6],
};
let ckpt = WalCkptInfo {
n_backfill: 0,
a_read_mark: [0; WAL_READ_MARK_COUNT],
a_lock: [0; WAL_LOCK_SLOT_COUNT],
n_backfill_attempted: 0,
not_used0: 0,
};
let mut buf = [0u8; WAL_SHM_FIRST_HEADER_BYTES];
write_shm_header(&mut buf, &hdr, &ckpt).expect("write should succeed");
assert!(wal_index_hdr_copies_match(&buf));
let (parsed_hdr, _parsed_ckpt) = parse_shm_header(&buf)
.expect("parse")
.expect("copies match");
assert_eq!(parsed_hdr.mx_frame, 50);
buf[WAL_INDEX_HDR_BYTES + 16] ^= 0xFF;
assert!(!wal_index_hdr_copies_match(&buf));
let result = parse_shm_header(&buf).expect("parse succeeds");
assert!(result.is_none(), "mismatched copies must be rejected");
}
#[test]
fn test_wal_ckpt_info_layout() {
assert_eq!(WAL_CKPT_INFO_BYTES, 40);
let ckpt = WalCkptInfo {
n_backfill: 42,
a_read_mark: [10, 20, 30, 40, 50],
a_lock: [1, 2, 3, 4, 5, 6, 7, 8],
n_backfill_attempted: 100,
not_used0: 0,
};
let bytes = ckpt.to_bytes();
assert_eq!(decode_native_u32(read4(&bytes, 0)), 42);
for i in 0..WAL_READ_MARK_COUNT {
let mark = decode_native_u32(read4(&bytes, 4 + i * 4));
let expected_mark = u32::try_from(i + 1).expect("fits") * 10;
assert_eq!(mark, expected_mark, "aReadMark[{i}]");
}
for i in 0..WAL_LOCK_SLOT_COUNT {
let expected_lock = u8::try_from(i + 1).expect("fits");
assert_eq!(bytes[24 + i], expected_lock, "aLock[{i}]");
}
assert_eq!(decode_native_u32(read4(&bytes, 32)), 100);
let mut full = [0u8; WAL_SHM_FIRST_HEADER_BYTES];
let dummy_hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 0,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 0,
n_page: 0,
a_frame_cksum: [0; 2],
a_salt: [0; 2],
a_cksum: [0; 2],
};
write_shm_header(&mut full, &dummy_hdr, &ckpt).expect("write");
assert_eq!(decode_native_u32(read4(&full, 96)), 42);
assert_eq!(decode_native_u32(read4(&full, 100)), 10);
assert_eq!(full[120], 1);
assert_eq!(decode_native_u32(read4(&full, 128)), 100);
}
#[test]
fn test_reader_marks_prevent_checkpoint_overwrite() {
let mut ckpt = WalCkptInfo {
n_backfill: 0,
a_read_mark: [0; WAL_READ_MARK_COUNT],
a_lock: [0; WAL_LOCK_SLOT_COUNT],
n_backfill_attempted: 0,
not_used0: 0,
};
ckpt.a_read_mark[0] = 50;
ckpt.a_read_mark[1] = 30;
let min_mark = ckpt
.a_read_mark
.iter()
.filter(|&&m| m > 0)
.copied()
.min()
.unwrap_or(0);
assert_eq!(
min_mark, 30,
"checkpoint limit should be oldest reader mark"
);
ckpt.a_read_mark = [0; WAL_READ_MARK_COUNT];
let min_mark_after = ckpt
.a_read_mark
.iter()
.filter(|&&m| m > 0)
.copied()
.min()
.unwrap_or(0);
assert_eq!(min_mark_after, 0, "no active readers = no checkpoint limit");
}
#[test]
fn test_lock_slot_mapping() {
assert_eq!(WAL_WRITE_LOCK, 0, "aLock[0] = WAL_WRITE_LOCK");
assert_eq!(WAL_CKPT_LOCK, 1, "aLock[1] = WAL_CKPT_LOCK");
assert_eq!(WAL_RECOVER_LOCK, 2, "aLock[2] = WAL_RECOVER_LOCK");
assert_eq!(WAL_READ_LOCK_BASE, 3, "aLock[3..7] = WAL_READ_LOCK(0..4)");
for i in 0..5_usize {
let lock_idx = WAL_READ_LOCK_BASE + i;
assert!(lock_idx < WAL_LOCK_SLOT_COUNT, "reader lock {i} in bounds");
}
}
#[test]
fn test_wal_index_header_round_trip() {
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 999,
is_init: 1,
big_end_cksum: 1,
sz_page: 8192,
mx_frame: 500,
n_page: 200,
a_frame_cksum: [0xDEAD_BEEF, 0xCAFE_BABE],
a_salt: [0x1234_5678, 0x9ABC_DEF0],
a_cksum: [0xFACE_FEED, 0xBEEF_DEAD],
};
let bytes = hdr.to_bytes();
let parsed = WalIndexHdr::from_bytes(&bytes).expect("round-trip parse");
assert_eq!(parsed, hdr);
}
#[test]
fn test_wal_ckpt_info_round_trip() {
let ckpt = WalCkptInfo {
n_backfill: 77,
a_read_mark: [10, 20, 30, 40, 50],
a_lock: [0, 1, 0, 1, 1, 0, 0, 1],
n_backfill_attempted: 80,
not_used0: 0,
};
let bytes = ckpt.to_bytes();
let parsed = WalCkptInfo::from_bytes(&bytes).expect("round-trip parse");
assert_eq!(parsed, ckpt);
}
#[test]
fn test_wal_index_iversion() {
assert_eq!(WAL_INDEX_VERSION, 3_007_000);
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 0,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 0,
n_page: 0,
a_frame_cksum: [0; 2],
a_salt: [0; 2],
a_cksum: [0; 2],
};
let bytes = hdr.to_bytes();
let parsed = WalIndexHdr::from_bytes(&bytes).expect("parse");
assert_eq!(parsed.i_version, 3_007_000);
}
#[test]
fn test_wal_index_native_byte_order_header() {
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 0x0102_0304,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 0,
n_page: 0,
a_frame_cksum: [0; 2],
a_salt: [0; 2],
a_cksum: [0; 2],
};
let bytes = hdr.to_bytes();
let raw = [bytes[8], bytes[9], bytes[10], bytes[11]];
assert_eq!(u32::from_ne_bytes(raw), 0x0102_0304);
if cfg!(target_endian = "little") {
assert_eq!(raw, 0x0102_0304_u32.to_le_bytes());
}
}
#[test]
fn test_wal_index_hdr_from_bytes_too_short() {
let buf = [0u8; WAL_INDEX_HDR_BYTES - 1];
let err = WalIndexHdr::from_bytes(&buf).unwrap_err();
assert!(err.to_string().contains("too small"));
}
#[test]
fn test_wal_ckpt_info_from_bytes_too_short() {
let buf = [0u8; WAL_CKPT_INFO_BYTES - 1];
let err = WalCkptInfo::from_bytes(&buf).unwrap_err();
assert!(err.to_string().contains("too small"));
}
#[test]
fn test_write_shm_header_too_short_buffer() {
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 0,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 0,
n_page: 0,
a_frame_cksum: [0; 2],
a_salt: [0; 2],
a_cksum: [0; 2],
};
let ckpt = WalCkptInfo {
n_backfill: 0,
a_read_mark: [0; WAL_READ_MARK_COUNT],
a_lock: [0; WAL_LOCK_SLOT_COUNT],
n_backfill_attempted: 0,
not_used0: 0,
};
let mut buf = [0u8; WAL_SHM_FIRST_HEADER_BYTES - 1];
let err = write_shm_header(&mut buf, &hdr, &ckpt).unwrap_err();
assert!(err.to_string().contains("too small"));
}
#[test]
fn test_hash_segment_is_empty_and_len() {
let mut seg = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
assert!(seg.is_empty());
assert_eq!(seg.len(), 0);
seg.insert(1).unwrap();
seg.insert(2).unwrap();
assert!(!seg.is_empty());
assert_eq!(seg.len(), 2);
}
#[test]
fn test_lookup_missing_page_returns_none() {
let mut seg = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
seg.insert(10).unwrap();
assert!(seg.lookup(10).is_some());
assert!(seg.lookup(99).is_none());
}
#[test]
fn test_duplicate_page_insert_returns_latest() {
let mut seg = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
seg.insert(42).unwrap();
seg.insert(42).unwrap();
let result = seg.lookup(42).expect("should find page");
assert_eq!(result.one_based_index, 2, "lookup returns latest entry");
}
#[test]
fn test_wal_index_segment_physical_layout() {
assert_eq!(WAL_SHM_PAGE_ARRAY_BYTES, 16_384);
assert_eq!(WAL_SHM_HASH_BYTES, 16_384);
assert_eq!(
WAL_SHM_PAGE_ARRAY_BYTES + WAL_SHM_HASH_BYTES,
WAL_SHM_SEGMENT_BYTES,
"page array + hash table = segment size"
);
}
#[test]
fn test_wal_ckpt_info_to_bytes_roundtrip() {
let ckpt = WalCkptInfo {
n_backfill: 42,
a_read_mark: [1, 2, 3, 4, 5],
a_lock: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80],
n_backfill_attempted: 99,
not_used0: 0,
};
let bytes = ckpt.to_bytes();
let parsed = WalCkptInfo::from_bytes(&bytes).unwrap();
assert_eq!(parsed, ckpt);
}
#[test]
fn test_wal_index_hdr_copies_match_mismatch() {
let mut buf = [0u8; 2 * WAL_INDEX_HDR_BYTES];
buf[..WAL_INDEX_HDR_BYTES].fill(0xAA);
buf[WAL_INDEX_HDR_BYTES..].fill(0xBB);
assert!(!wal_index_hdr_copies_match(&buf));
buf[WAL_INDEX_HDR_BYTES..].copy_from_slice(&buf[..WAL_INDEX_HDR_BYTES].to_vec());
assert!(wal_index_hdr_copies_match(&buf));
assert!(!wal_index_hdr_copies_match(&[0u8; WAL_INDEX_HDR_BYTES - 1]));
}
#[test]
fn test_parse_write_shm_header_roundtrip() {
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 7,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 100,
n_page: 50,
a_frame_cksum: [0x1234, 0x5678],
a_salt: [0xAAAA, 0xBBBB],
a_cksum: [0xCCCC, 0xDDDD],
};
let ckpt = WalCkptInfo {
n_backfill: 10,
a_read_mark: [0, 5, 10, 15, 20],
a_lock: [0; WAL_LOCK_SLOT_COUNT],
n_backfill_attempted: 10,
not_used0: 0,
};
let mut buf = [0u8; WAL_SHM_FIRST_HEADER_BYTES];
write_shm_header(&mut buf, &hdr, &ckpt).unwrap();
let (parsed_hdr, parsed_ckpt) = parse_shm_header(&buf).unwrap().unwrap();
assert_eq!(parsed_hdr, hdr);
assert_eq!(parsed_ckpt, ckpt);
}
#[test]
fn test_first_segment_capacity_less_than_subsequent() {
let first = WalIndexHashSegment::new(WalIndexSegmentKind::First);
let sub = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
assert!(first.capacity() < sub.capacity());
assert_eq!(first.kind(), WalIndexSegmentKind::First);
assert_eq!(sub.kind(), WalIndexSegmentKind::Subsequent);
assert_eq!(sub.capacity(), WAL_SHM_SUBSEQUENT_USABLE_PAGE_ENTRIES);
assert_eq!(first.capacity(), WAL_SHM_FIRST_USABLE_PAGE_ENTRIES);
}
#[test]
fn test_hash_slots_accessor_reflects_inserts() {
let mut seg = WalIndexHashSegment::new(WalIndexSegmentKind::Subsequent);
let slots_before = seg.hash_slots();
assert!(slots_before.iter().all(|&s| s == 0));
seg.insert(7).unwrap();
seg.insert(15).unwrap();
let slots_after = seg.hash_slots();
let non_zero: usize = slots_after.iter().filter(|&&s| s != 0).count();
assert_eq!(non_zero, 2);
let slot_7 = usize::try_from(wal_index_hash_slot(7)).unwrap();
assert_eq!(
slots_after[slot_7], 1,
"page 7 is first entry → one-based 1"
);
let slot_15 = usize::try_from(wal_index_hash_slot(15)).unwrap();
assert_eq!(
slots_after[slot_15], 2,
"page 15 is second entry → one-based 2"
);
}
#[test]
fn test_parse_shm_header_too_short_returns_error() {
let buf = [0u8; WAL_SHM_FIRST_HEADER_BYTES - 1];
let err = parse_shm_header(&buf).unwrap_err();
assert!(err.to_string().contains("too small"));
}
#[test]
fn test_from_bytes_accepts_oversized_buffers() {
let hdr = WalIndexHdr {
i_version: WAL_INDEX_VERSION,
unused: 0,
i_change: 55,
is_init: 1,
big_end_cksum: 0,
sz_page: 4096,
mx_frame: 10,
n_page: 5,
a_frame_cksum: [111, 222],
a_salt: [333, 444],
a_cksum: [555, 666],
};
let small = hdr.to_bytes();
let mut big = [0xFFu8; 128];
big[..WAL_INDEX_HDR_BYTES].copy_from_slice(&small);
let parsed = WalIndexHdr::from_bytes(&big).unwrap();
assert_eq!(parsed, hdr);
let ckpt = WalCkptInfo {
n_backfill: 9,
a_read_mark: [1, 2, 3, 4, 5],
a_lock: [0; WAL_LOCK_SLOT_COUNT],
n_backfill_attempted: 12,
not_used0: 0,
};
let small_ckpt = ckpt.to_bytes();
let mut big_ckpt = [0xFFu8; 128];
big_ckpt[..WAL_CKPT_INFO_BYTES].copy_from_slice(&small_ckpt);
let parsed_ckpt = WalCkptInfo::from_bytes(&big_ckpt).unwrap();
assert_eq!(parsed_ckpt, ckpt);
}
#[test]
fn test_wal_hash_lookup_fields_and_derives() {
let a = WalHashLookup {
slot: 42,
one_based_index: 7,
page_number: 100,
};
let b = a;
assert_eq!(a, b);
let c = WalHashLookup {
slot: 42,
one_based_index: 8,
page_number: 100,
};
assert_ne!(a, c);
assert_eq!(a.slot, 42);
assert_eq!(a.one_based_index, 7);
assert_eq!(a.page_number, 100);
let dbg = format!("{a:?}");
assert!(dbg.contains("WalHashLookup"));
}
}