use std::path::{Path, PathBuf};
use crate::page_cache::PageCache;
use crate::storage::page::{PageType, PAGE_SIZE};
use crate::storage::v4::engine::{Engine, EngineConfig, DEFAULT_NAMESPACE_ID};
use crate::storage::v4::store::V4_MAGIC;
use crate::storage::v4::wal::FlushPolicy;
use crate::{Error, Result};
const V3_MAGIC: [u8; 8] = *b"EMDBPAGE";
const LEGACY_LOG_MAGIC: [u8; 8] = *b"EMDB\0\0\0\0";
pub(crate) fn migrate_v3_to_v4_if_needed(path: &Path, flags: u32) -> Result<()> {
let metadata = match std::fs::metadata(path) {
Ok(meta) => meta,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(Error::Io(err)),
};
if metadata.len() == 0 {
return Ok(());
}
let bytes = std::fs::read(path)?;
if bytes.len() < 8 {
return Err(Error::MagicMismatch);
}
if bytes[0..8] == V4_MAGIC {
return Ok(());
}
if bytes[0..8] == LEGACY_LOG_MAGIC {
crate::storage::migrate::migrate_if_needed(path, flags)?;
} else if bytes[0..8] != V3_MAGIC {
return Err(Error::MagicMismatch);
}
migrate_v3_to_v4(path, flags)
}
fn migrate_v3_to_v4(path: &Path, flags: u32) -> Result<()> {
use std::sync::Arc;
let tmp_path = with_suffix(path, "v4tmp");
let backup_path = with_suffix(path, "v3bak");
let _removed_tmp = std::fs::remove_file(&tmp_path);
let _removed_backup = std::fs::remove_file(&backup_path);
let owned: Vec<V3Record> = read_all_v3_records(path, flags)?;
let engine_config = EngineConfig {
path: tmp_path.clone(),
flags,
page_io_mode: crate::storage::v4::io::IoMode::Buffered,
wal_io_mode: crate::storage::v4::io::IoMode::Buffered,
flush_policy: FlushPolicy::Manual,
page_cache_pages: 0,
value_cache_bytes: 0,
bloom_initial_capacity: owned.len() as u64,
};
let engine = match Engine::open(engine_config) {
Ok(e) => e,
Err(err) => {
let _cleanup = std::fs::remove_file(&tmp_path);
return Err(err);
}
};
for (key, value, expires_at) in &owned {
let exp = expires_at.unwrap_or(0);
if let Err(err) = engine.insert(DEFAULT_NAMESPACE_ID, key, value, exp) {
drop(engine);
let _cleanup = std::fs::remove_file(&tmp_path);
return Err(err);
}
}
if let Err(err) = engine.flush() {
drop(engine);
let _cleanup = std::fs::remove_file(&tmp_path);
return Err(err);
}
drop(engine);
if let Err(err) = std::fs::rename(path, &backup_path) {
let _cleanup = std::fs::remove_file(&tmp_path);
return Err(Error::Io(err));
}
if let Err(err) = std::fs::rename(&tmp_path, path) {
let _restore = std::fs::rename(&backup_path, path);
return Err(Error::Io(err));
}
let _removed_backup = std::fs::remove_file(&backup_path);
let v3_wal = with_suffix(path, "wal");
let _removed_v3_wal = std::fs::remove_file(&v3_wal);
let _ = (
Arc::<PageCache>::new(PageCache::new(0)),
PageType::Header,
PAGE_SIZE,
);
Ok(())
}
type V3Record = (Vec<u8>, Vec<u8>, Option<u64>);
fn read_all_v3_records(path: &Path, flags: u32) -> Result<Vec<V3Record>> {
use crate::storage::page_store::PageStorage;
use crate::storage::Storage;
let mut store = PageStorage::new(
path.to_path_buf(),
crate::storage::FlushPolicy::Manual,
flags,
#[cfg(feature = "mmap")]
false,
)?;
use crate::storage::Op;
let mut staged: std::collections::HashMap<Vec<u8>, (Vec<u8>, Option<u64>)> =
std::collections::HashMap::new();
store.replay(&mut |op: Op| -> Result<()> {
match op {
Op::Insert {
key,
value,
expires_at,
} => {
let _previous = staged.insert(key, (value, expires_at));
}
Op::Remove { key } => {
let _removed = staged.remove(&key);
}
Op::Clear => {
staged.clear();
}
Op::Checkpoint { .. } | Op::BatchBegin { .. } | Op::BatchEnd { .. } => {}
}
Ok(())
})?;
Ok(staged
.into_iter()
.map(|(k, (v, exp))| (k, v, exp))
.collect())
}
fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
let mut out = path.to_path_buf();
let original_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("emdb");
out.set_file_name(format!("{original_name}.{suffix}"));
out
}
#[cfg(test)]
mod tests {
use super::{migrate_v3_to_v4_if_needed, V3_MAGIC};
use crate::Emdb;
fn tmp_path(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0_u128, |d| d.as_nanos());
p.push(format!("emdb-v4-migrate-{name}-{nanos}.emdb"));
p
}
#[test]
fn missing_file_is_a_noop() {
let path = tmp_path("noop");
let result = migrate_v3_to_v4_if_needed(path.as_path(), 0);
assert!(result.is_ok());
}
#[test]
fn already_v4_file_is_a_noop() {
let path = tmp_path("already-v4");
{
let db = match Emdb::builder().path(path.clone()).prefer_v4(true).build() {
Ok(db) => db,
Err(err) => panic!("v4 build should succeed: {err}"),
};
assert!(db.flush().is_ok());
}
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(err) => panic!("read should succeed: {err}"),
};
assert_eq!(&bytes[0..8], &super::V4_MAGIC);
let result = migrate_v3_to_v4_if_needed(path.as_path(), 0);
assert!(result.is_ok());
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.v4.wal", path.display()));
let _ = std::fs::remove_file(format!("{}.lock", path.display()));
}
#[test]
fn v3_file_migrates_records_visibly_through_v4_open() {
let path = tmp_path("v3-to-v4");
{
let db = match Emdb::open(&path) {
Ok(db) => db,
Err(err) => panic!("v0.6 open should succeed: {err}"),
};
for i in 0_u32..32 {
let key = format!("k{i:02}");
let value = format!("v{i:02}");
let _ = db.insert(key.as_bytes(), value.as_bytes());
}
assert!(db.flush().is_ok());
}
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(err) => panic!("read should succeed: {err}"),
};
assert_eq!(&bytes[0..8], &V3_MAGIC);
let db = match Emdb::builder().path(path.clone()).prefer_v4(true).build() {
Ok(db) => db,
Err(err) => panic!("v4 build (with migration) should succeed: {err}"),
};
for i in 0_u32..32 {
let key = format!("k{i:02}");
let fetched = db.get(key.as_bytes());
match fetched {
Ok(Some(v)) => assert_eq!(v.as_slice(), format!("v{i:02}").as_bytes()),
Ok(None) => panic!("key {key} missing after v3 → v4 migration"),
Err(err) => panic!("get should succeed: {err}"),
}
}
let len = match db.len() {
Ok(n) => n,
Err(err) => panic!("len should succeed: {err}"),
};
assert_eq!(len, 32);
drop(db);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(err) => panic!("read should succeed: {err}"),
};
assert_eq!(&bytes[0..8], &super::V4_MAGIC);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.v4.wal", path.display()));
let _ = std::fs::remove_file(format!("{}.lock", path.display()));
let _ = std::fs::remove_file(format!("{}.wal", path.display()));
let _ = std::fs::remove_file(format!("{}.v3bak", path.display()));
}
}