use std::fmt;
use std::fs;
#[cfg(unix)]
use std::fs::File;
use std::path::{Path, PathBuf};
use crate::error::WalError;
use crate::lsn::Lsn;
use crate::segment::SegmentReader;
const SEGMENT_ID_WIDTH: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct SegmentId(u64);
impl SegmentId {
pub const FIRST: SegmentId = SegmentId(1);
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn raw(self) -> u64 {
self.0
}
pub fn next(self) -> Self {
Self(self.0 + 1)
}
pub fn saturating_prev(self) -> Self {
Self(self.0.saturating_sub(1))
}
}
impl fmt::Display for SegmentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct SegmentEntry {
pub id: SegmentId,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct SegmentDir {
root: PathBuf,
}
impl SegmentDir {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn root(&self) -> &Path {
&self.root
}
#[cfg(unix)]
pub fn sync_dir(&self) -> Result<(), WalError> {
File::open(&self.root)?.sync_all()?;
Ok(())
}
#[cfg(not(unix))]
pub fn sync_dir(&self) -> Result<(), WalError> {
Ok(())
}
pub fn path_for(&self, id: SegmentId) -> PathBuf {
self.root
.join(format!("{:0width$}.wal", id.0, width = SEGMENT_ID_WIDTH))
}
pub fn id_of(path: &Path) -> Option<SegmentId> {
path.extension()
.and_then(|s| s.to_str())
.filter(|ext| *ext == "wal")?;
path.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.parse::<u64>().ok())
.map(SegmentId)
}
pub fn list(&self) -> Result<Vec<SegmentEntry>, WalError> {
let mut out: Vec<SegmentEntry> = fs::read_dir(&self.root)?
.filter_map(|e| e.ok())
.filter_map(|e| {
let path = e.path();
let id = Self::id_of(&path)?;
Some(SegmentEntry { id, path })
})
.collect();
out.sort_by_key(|e| e.id);
Ok(out)
}
pub fn base_lsn(segment: &Path) -> Result<Lsn, WalError> {
let reader = SegmentReader::open(segment)?;
Ok(reader.header().base_lsn)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn segment_id_path_round_trip() {
let dir = SegmentDir::new("/tmp");
let id = SegmentId::new(42);
let path = dir.path_for(id);
assert_eq!(path.to_str().unwrap(), "/tmp/0000000042.wal");
assert_eq!(SegmentDir::id_of(&path), Some(id));
}
#[test]
fn id_of_rejects_non_wal_files() {
assert_eq!(SegmentDir::id_of(Path::new("/tmp/0000000001.txt")), None);
assert_eq!(SegmentDir::id_of(Path::new("/tmp/notanumber.wal")), None);
assert_eq!(SegmentDir::id_of(Path::new("/tmp/CURRENT")), None);
}
#[test]
fn saturating_prev_does_not_underflow() {
assert_eq!(SegmentId::new(0).saturating_prev(), SegmentId::new(0));
assert_eq!(SegmentId::new(1).saturating_prev(), SegmentId::new(0));
assert_eq!(SegmentId::new(7).saturating_prev(), SegmentId::new(6));
}
}