use std::io::{Read, Seek, SeekFrom};
use super::{
entry::{Lsn, WalEntry},
format::{WalHeader, WAL_HEADER_SIZE},
writer::verify_checksum,
};
use crate::{persistence::binary::io::read_u32, StorageError};
#[derive(Debug)]
pub enum ReadResult {
Entry(WalEntry),
Eof,
Corruption { position: u64 },
}
pub struct WalReader<R: Read + Seek> {
reader: R,
header: WalHeader,
position: u64,
entries_read: u64,
last_lsn: Option<Lsn>,
}
impl<R: Read + Seek> WalReader<R> {
pub fn open(mut reader: R) -> Result<Self, StorageError> {
let header = WalHeader::read(&mut reader)?;
Ok(Self {
reader,
header,
position: WAL_HEADER_SIZE as u64,
entries_read: 0,
last_lsn: None,
})
}
pub fn read_entry(&mut self) -> Result<ReadResult, StorageError> {
let len = match read_u32(&mut self.reader) {
Ok(len) => len,
Err(_) => {
return Ok(ReadResult::Eof);
}
};
let checksum = match read_u32(&mut self.reader) {
Ok(crc) => crc,
Err(_) => {
return Ok(ReadResult::Corruption { position: self.position });
}
};
let mut data = vec![0u8; len as usize];
if self.reader.read_exact(&mut data).is_err() {
return Ok(ReadResult::Corruption { position: self.position });
}
if !verify_checksum(&data, checksum) {
return Ok(ReadResult::Corruption { position: self.position });
}
let mut data_reader = &data[..];
let entry = WalEntry::deserialize(&mut data_reader)?;
self.position += 4 + 4 + len as u64; self.entries_read += 1;
self.last_lsn = Some(entry.lsn);
Ok(ReadResult::Entry(entry))
}
pub fn read_all(&mut self) -> Result<Vec<WalEntry>, StorageError> {
let mut entries = Vec::new();
loop {
match self.read_entry()? {
ReadResult::Entry(entry) => entries.push(entry),
ReadResult::Eof => break,
ReadResult::Corruption { position: _ } => break,
}
}
Ok(entries)
}
pub fn header(&self) -> &WalHeader {
&self.header
}
pub fn entries_read(&self) -> u64 {
self.entries_read
}
pub fn last_lsn(&self) -> Option<Lsn> {
self.last_lsn
}
pub fn position(&self) -> u64 {
self.position
}
pub fn seek_to(&mut self, position: u64) -> Result<(), StorageError> {
if position < WAL_HEADER_SIZE as u64 {
return Err(StorageError::IoError("Cannot seek before WAL header".to_string()));
}
self.reader
.seek(SeekFrom::Start(position))
.map_err(|e| StorageError::IoError(e.to_string()))?;
self.position = position;
Ok(())
}
pub fn reset(&mut self) -> Result<(), StorageError> {
self.seek_to(WAL_HEADER_SIZE as u64)?;
self.entries_read = 0;
self.last_lsn = None;
Ok(())
}
}
pub struct WalIterator<'a, R: Read + Seek> {
reader: &'a mut WalReader<R>,
}
impl<R: Read + Seek> WalReader<R> {
pub fn iter(&mut self) -> WalIterator<'_, R> {
WalIterator { reader: self }
}
}
impl<R: Read + Seek> Iterator for WalIterator<'_, R> {
type Item = Result<WalEntry, StorageError>;
fn next(&mut self) -> Option<Self::Item> {
match self.reader.read_entry() {
Ok(ReadResult::Entry(entry)) => Some(Ok(entry)),
Ok(ReadResult::Eof) => None,
Ok(ReadResult::Corruption { position }) => {
Some(Err(StorageError::IoError(format!("WAL corruption at position {}", position))))
}
Err(e) => Some(Err(e)),
}
}
}
pub fn find_recovery_point<R: Read + Seek>(reader: R) -> Result<RecoveryInfo, StorageError> {
let mut wal_reader = WalReader::open(reader)?;
let mut last_valid_position = WAL_HEADER_SIZE as u64;
let mut last_lsn = None;
let mut entry_count = 0;
loop {
match wal_reader.read_entry()? {
ReadResult::Entry(entry) => {
last_valid_position = wal_reader.position();
last_lsn = Some(entry.lsn);
entry_count += 1;
}
ReadResult::Eof => break,
ReadResult::Corruption { position: _ } => break,
}
}
Ok(RecoveryInfo { last_valid_position, last_lsn, entry_count })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecoveryInfo {
pub last_valid_position: u64,
pub last_lsn: Option<Lsn>,
pub entry_count: u64,
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use vibesql_types::SqlValue;
use super::*;
use crate::wal::{entry::WalOp, writer::WalWriter};
fn create_test_wal() -> Cursor<Vec<u8>> {
let buf = Vec::new();
let cursor = Cursor::new(buf);
let mut writer = WalWriter::create(cursor).unwrap();
for i in 1..=5 {
let entry = WalEntry::new(
i,
1234567890 + i,
WalOp::Insert { table_id: 1, row_id: i, values: vec![SqlValue::Integer(i as i64)] },
);
writer.append(&entry).unwrap();
}
writer.flush().unwrap();
Cursor::new(writer.into_inner().into_inner())
}
#[test]
fn test_read_all_entries() {
let cursor = create_test_wal();
let mut reader = WalReader::open(cursor).unwrap();
let entries = reader.read_all().unwrap();
assert_eq!(entries.len(), 5);
for (i, entry) in entries.iter().enumerate() {
assert_eq!(entry.lsn, (i + 1) as u64);
}
}
#[test]
fn test_read_entry_by_entry() {
let cursor = create_test_wal();
let mut reader = WalReader::open(cursor).unwrap();
for i in 1..=5 {
match reader.read_entry().unwrap() {
ReadResult::Entry(entry) => {
assert_eq!(entry.lsn, i);
}
other => panic!("Expected Entry, got {:?}", other),
}
}
match reader.read_entry().unwrap() {
ReadResult::Eof => {}
other => panic!("Expected Eof, got {:?}", other),
}
}
#[test]
fn test_iterator() {
let cursor = create_test_wal();
let mut reader = WalReader::open(cursor).unwrap();
let entries: Vec<_> = reader.iter().collect();
assert_eq!(entries.len(), 5);
for entry in entries {
assert!(entry.is_ok());
}
}
#[test]
fn test_header_info() {
let cursor = create_test_wal();
let reader = WalReader::open(cursor).unwrap();
assert_eq!(reader.header().version, super::super::format::WAL_VERSION);
}
#[test]
fn test_recovery_info() {
let cursor = create_test_wal();
let info = find_recovery_point(cursor).unwrap();
assert_eq!(info.entry_count, 5);
assert_eq!(info.last_lsn, Some(5));
assert!(info.last_valid_position > WAL_HEADER_SIZE as u64);
}
#[test]
fn test_corruption_detection_truncated_data() {
let mut buf = create_test_wal().into_inner();
buf.truncate(WAL_HEADER_SIZE + 20);
let cursor = Cursor::new(buf);
let mut reader = WalReader::open(cursor).unwrap();
let entries = reader.read_all().unwrap();
assert!(entries.len() <= 5);
}
#[test]
fn test_corruption_detection_bad_checksum() {
let mut buf = create_test_wal().into_inner();
if buf.len() > WAL_HEADER_SIZE + 50 {
buf[WAL_HEADER_SIZE + 45] ^= 0xFF; }
let cursor = Cursor::new(buf);
let info = find_recovery_point(cursor).unwrap();
assert!(info.entry_count < 5);
}
#[test]
fn test_reset() {
let cursor = create_test_wal();
let mut reader = WalReader::open(cursor).unwrap();
let entries1 = reader.read_all().unwrap();
assert_eq!(entries1.len(), 5);
reader.reset().unwrap();
let entries2 = reader.read_all().unwrap();
assert_eq!(entries2.len(), 5);
assert_eq!(entries1, entries2);
}
}