use super::edit::{VersionEdit, replay_edits};
use crate::fs::{Fs, FsOpenOptions, SyncMode};
use std::io::{Seek, SeekFrom};
use std::path::Path;
pub fn append_edit(
fs: &dyn Fs,
path: &Path,
edit: &VersionEdit,
scratch: &mut Vec<u8>,
sync_mode: SyncMode,
) -> crate::Result<()> {
let mut file = fs
.open(
path,
&FsOpenOptions::new().write(true).create(true).append(true),
)
.map_err(crate::Error::Io)?;
edit.append_to(&mut file, scratch)?;
file.sync_all_with(sync_mode).map_err(crate::Error::Io)?;
Ok(())
}
pub fn replay_log(fs: &dyn Fs, path: &Path) -> crate::Result<Vec<VersionEdit>> {
match fs.open(path, &FsOpenOptions::new().read(true)) {
Ok(mut file) => replay_edits(&mut file),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(crate::Error::Io(e)),
}
}
pub fn log_size(fs: &dyn Fs, path: &Path) -> crate::Result<u64> {
match fs.open(path, &FsOpenOptions::new().read(true)) {
Ok(mut file) => file.seek(SeekFrom::End(0)).map_err(crate::Error::Io),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
Err(e) => Err(crate::Error::Io(e)),
}
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test code")]
mod tests {
use super::super::edit::{ChangedLevel, TableDesc, VersionEdit};
use super::*;
use crate::fs::StdFs;
fn edit(id: u64) -> VersionEdit {
VersionEdit {
new_version_id: id,
changed_levels: vec![ChangedLevel {
level: 0,
runs: vec![vec![TableDesc {
id,
checksum: u128::from(id) * 7,
global_seqno: id * 10,
}]],
}],
..Default::default()
}
}
#[test]
fn append_then_replay_roundtrips_all_edits() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("edits-0");
let mut scratch = Vec::new();
let edits: Vec<VersionEdit> = (1..=4).map(edit).collect();
for e in &edits {
append_edit(&StdFs, &path, e, &mut scratch, SyncMode::Normal).expect("append");
}
let replayed = replay_log(&StdFs, &path).expect("replay");
assert_eq!(replayed, edits, "append+replay must round-trip in order");
}
#[test]
fn replay_absent_log_is_empty() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("edits-missing");
assert!(replay_log(&StdFs, &path).expect("replay").is_empty());
assert_eq!(log_size(&StdFs, &path).expect("size"), 0);
}
#[test]
fn torn_tail_record_is_dropped_on_replay() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("edits-torn");
let mut scratch = Vec::new();
for i in 1..=2 {
append_edit(&StdFs, &path, &edit(i), &mut scratch, SyncMode::Normal).expect("append");
}
let clean = log_size(&StdFs, &path).expect("size");
append_edit(&StdFs, &path, &edit(3), &mut scratch, SyncMode::Normal).expect("append");
let f = std::fs::OpenOptions::new()
.write(true)
.open(&path)
.expect("open");
f.set_len(clean + 5).expect("truncate");
drop(f);
let replayed = replay_log(&StdFs, &path).expect("replay");
assert_eq!(
replayed,
vec![edit(1), edit(2)],
"torn tail dropped, clean prefix kept",
);
}
#[test]
fn log_size_grows_with_appends() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("edits-size");
let mut scratch = Vec::new();
let s0 = log_size(&StdFs, &path).expect("size");
append_edit(&StdFs, &path, &edit(1), &mut scratch, SyncMode::Normal).expect("append");
let s1 = log_size(&StdFs, &path).expect("size");
append_edit(&StdFs, &path, &edit(2), &mut scratch, SyncMode::Normal).expect("append");
let s2 = log_size(&StdFs, &path).expect("size");
assert_eq!(s0, 0);
assert!(s1 > s0 && s2 > s1, "log grows with each appended edit");
}
}