mod util;
use pretty_assertions::assert_eq;
use rocksdb::{
checkpoint::{Checkpoint, TransactionDBCheckpoint},
OptimisticTransactionDB, Options, TransactionDB, TransactionDBOptions, DB,
};
use std::fs;
use util::DBPath;
#[test]
pub fn test_single_checkpoint() {
const PATH_PREFIX: &str = "_rust_rocksdb_cp_single_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db1"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db = DB::open(&opts, &db_path).unwrap();
db.put(b"k1", b"v1").unwrap();
db.put(b"k2", b"v2").unwrap();
db.put(b"k3", b"v3").unwrap();
db.put(b"k4", b"v4").unwrap();
let cp1 = Checkpoint::new(&db).unwrap();
let cp1_path = DBPath::new(&format!("{PATH_PREFIX}cp1"));
cp1.create_checkpoint(&cp1_path).unwrap();
let cp = DB::open_default(&cp1_path).unwrap();
assert_eq!(cp.get(b"k1").unwrap().unwrap(), b"v1");
assert_eq!(cp.get(b"k2").unwrap().unwrap(), b"v2");
assert_eq!(cp.get(b"k3").unwrap().unwrap(), b"v3");
assert_eq!(cp.get(b"k4").unwrap().unwrap(), b"v4");
}
#[test]
pub fn test_multi_checkpoints() {
const PATH_PREFIX: &str = "_rust_rocksdb_cp_multi_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db1"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db = DB::open(&opts, &db_path).unwrap();
db.put(b"k1", b"v1").unwrap();
db.put(b"k2", b"v2").unwrap();
db.put(b"k3", b"v3").unwrap();
db.put(b"k4", b"v4").unwrap();
let cp1 = Checkpoint::new(&db).unwrap();
let cp1_path = DBPath::new(&format!("{PATH_PREFIX}cp1"));
cp1.create_checkpoint(&cp1_path).unwrap();
let cp = DB::open_default(&cp1_path).unwrap();
assert_eq!(cp.get(b"k1").unwrap().unwrap(), b"v1");
assert_eq!(cp.get(b"k2").unwrap().unwrap(), b"v2");
assert_eq!(cp.get(b"k3").unwrap().unwrap(), b"v3");
assert_eq!(cp.get(b"k4").unwrap().unwrap(), b"v4");
db.put(b"k1", b"modified").unwrap();
db.put(b"k2", b"changed").unwrap();
db.put(b"k5", b"v5").unwrap();
db.put(b"k6", b"v6").unwrap();
let cp2 = Checkpoint::new(&db).unwrap();
let cp2_path = DBPath::new(&format!("{PATH_PREFIX}cp2"));
cp2.create_checkpoint(&cp2_path).unwrap();
let cp = DB::open_default(&cp2_path).unwrap();
assert_eq!(cp.get(b"k1").unwrap().unwrap(), b"modified");
assert_eq!(cp.get(b"k2").unwrap().unwrap(), b"changed");
assert_eq!(cp.get(b"k5").unwrap().unwrap(), b"v5");
assert_eq!(cp.get(b"k6").unwrap().unwrap(), b"v6");
}
#[test]
pub fn test_checkpoint_with_log_size_zero_forces_flush() {
const PATH_PREFIX: &str = "_rust_rocksdb_cp_log_size_zero_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db = DB::open(&opts, &db_path).unwrap();
db.put(b"flushed_key", b"flushed_value").unwrap();
db.flush().unwrap();
db.put(b"memtable_key", b"memtable_value").unwrap();
let cp = Checkpoint::new(&db).unwrap();
let cp_path = DBPath::new(&format!("{PATH_PREFIX}cp"));
cp.create_checkpoint_with_log_size(&cp_path, 0).unwrap();
let wal_files: Vec<_> = fs::read_dir((&cp_path).as_ref())
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "log"))
.collect();
assert_eq!(
wal_files.len(),
1,
"Checkpoint should contain exactly one WAL file"
);
let wal_metadata = wal_files[0].metadata().unwrap();
assert_eq!(
wal_metadata.len(),
0,
"WAL file should be empty when flush is forced"
);
let cp_db = DB::open_default(&cp_path).unwrap();
assert_eq!(
cp_db.get(b"flushed_key").unwrap().unwrap(),
b"flushed_value"
);
assert_eq!(
cp_db.get(b"memtable_key").unwrap().unwrap(),
b"memtable_value"
);
}
#[test]
pub fn test_checkpoint_with_large_log_size_skips_flush() {
const PATH_PREFIX: &str = "_rust_rocksdb_cp_log_size_large_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db = DB::open(&opts, &db_path).unwrap();
db.put(b"flushed_key", b"flushed_value").unwrap();
db.flush().unwrap();
db.put(b"memtable_key", b"memtable_value").unwrap();
let cp = Checkpoint::new(&db).unwrap();
let cp_path = DBPath::new(&format!("{PATH_PREFIX}cp"));
let large_log_size = u64::MAX;
cp.create_checkpoint_with_log_size(&cp_path, large_log_size)
.unwrap();
let wal_files: Vec<_> = fs::read_dir((&cp_path).as_ref())
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "log"))
.collect();
assert_eq!(
wal_files.len(),
1,
"Checkpoint should contain exactly one WAL file"
);
let wal_metadata = wal_files[0].metadata().unwrap();
assert!(wal_metadata.len() > 0, "WAL file should not be empty");
let cp_db = DB::open_default(&cp_path).unwrap();
assert_eq!(
cp_db.get(b"flushed_key").unwrap().unwrap(),
b"flushed_value"
);
assert_eq!(
cp_db.get(b"memtable_key").unwrap().unwrap(),
b"memtable_value"
);
}
#[test]
pub fn test_optimistic_transaction_db_checkpoint_with_log_size_zero_forces_flush() {
const PATH_PREFIX: &str = "_rust_rocksdb_otxn_cp_log_size_zero_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db: OptimisticTransactionDB = OptimisticTransactionDB::open(&opts, &db_path).unwrap();
db.put(b"flushed_key", b"flushed_value").unwrap();
db.flush().unwrap();
db.put(b"memtable_key", b"memtable_value").unwrap();
let cp = Checkpoint::new(&db).unwrap();
let cp_path = DBPath::new(&format!("{PATH_PREFIX}cp"));
cp.create_checkpoint_with_log_size(&cp_path, 0).unwrap();
let wal_files: Vec<_> = fs::read_dir((&cp_path).as_ref())
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "log"))
.collect();
assert_eq!(
wal_files.len(),
1,
"Checkpoint should contain exactly one WAL file"
);
let wal_metadata = wal_files[0].metadata().unwrap();
assert_eq!(
wal_metadata.len(),
0,
"WAL file should be empty when flush is forced"
);
let cp_db: OptimisticTransactionDB = OptimisticTransactionDB::open_default(&cp_path).unwrap();
assert_eq!(
cp_db.get(b"flushed_key").unwrap().unwrap(),
b"flushed_value"
);
assert_eq!(
cp_db.get(b"memtable_key").unwrap().unwrap(),
b"memtable_value"
);
}
#[test]
pub fn test_optimistic_transaction_db_checkpoint_with_large_log_size_skips_flush() {
const PATH_PREFIX: &str = "_rust_rocksdb_otxn_cp_log_size_large_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db: OptimisticTransactionDB = OptimisticTransactionDB::open(&opts, &db_path).unwrap();
db.put(b"flushed_key", b"flushed_value").unwrap();
db.flush().unwrap();
db.put(b"memtable_key", b"memtable_value").unwrap();
let cp = Checkpoint::new(&db).unwrap();
let cp_path = DBPath::new(&format!("{PATH_PREFIX}cp"));
let large_log_size = u64::MAX;
cp.create_checkpoint_with_log_size(&cp_path, large_log_size)
.unwrap();
let wal_files: Vec<_> = fs::read_dir((&cp_path).as_ref())
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "log"))
.collect();
assert_eq!(
wal_files.len(),
1,
"Checkpoint should contain exactly one WAL file"
);
let wal_metadata = wal_files[0].metadata().unwrap();
assert!(wal_metadata.len() > 0, "WAL file should not be empty");
let cp_db: OptimisticTransactionDB = OptimisticTransactionDB::open_default(&cp_path).unwrap();
assert_eq!(
cp_db.get(b"flushed_key").unwrap().unwrap(),
b"flushed_value"
);
assert_eq!(
cp_db.get(b"memtable_key").unwrap().unwrap(),
b"memtable_value"
);
}
#[test]
pub fn test_transaction_db_checkpoint() {
const PATH_PREFIX: &str = "_rust_rocksdb_txn_cp_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db: TransactionDB =
TransactionDB::open(&opts, &TransactionDBOptions::default(), &db_path).unwrap();
db.put(b"k1", b"v1").unwrap();
let txn = db.transaction();
txn.put(b"k2", b"v2").unwrap();
txn.commit().unwrap();
let checkpoint = TransactionDBCheckpoint::new(&db).unwrap();
let checkpoint_path = DBPath::new(&format!("{PATH_PREFIX}cp"));
checkpoint.create_checkpoint(&checkpoint_path).unwrap();
let checkpoint_db: TransactionDB =
TransactionDB::open(&opts, &TransactionDBOptions::default(), &checkpoint_path).unwrap();
assert_eq!(checkpoint_db.get(b"k1").unwrap().unwrap(), b"v1");
assert_eq!(checkpoint_db.get(b"k2").unwrap().unwrap(), b"v2");
}
#[test]
pub fn test_transaction_db_checkpoint_with_log_size() {
const PATH_PREFIX: &str = "_rust_rocksdb_txn_cp_log_size_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db: TransactionDB =
TransactionDB::open(&opts, &TransactionDBOptions::default(), &db_path).unwrap();
db.put(b"first_key", b"first_value").unwrap();
db.put(b"memtable_key", b"memtable_value").unwrap();
let checkpoint = TransactionDBCheckpoint::new(&db).unwrap();
let checkpoint_path = DBPath::new(&format!("{PATH_PREFIX}cp"));
checkpoint
.create_checkpoint_with_log_size(&checkpoint_path, 0)
.unwrap();
let checkpoint_db: TransactionDB =
TransactionDB::open(&opts, &TransactionDBOptions::default(), &checkpoint_path).unwrap();
assert_eq!(
checkpoint_db.get(b"first_key").unwrap().unwrap(),
b"first_value"
);
assert_eq!(
checkpoint_db.get(b"memtable_key").unwrap().unwrap(),
b"memtable_value"
);
}
#[test]
pub fn test_checkpoint_wal_truncation_loses_memtable_data() {
const PATH_PREFIX: &str = "_rust_rocksdb_cp_wal_truncate_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db = DB::open(&opts, &db_path).unwrap();
db.put(b"flushed_key", b"flushed_value").unwrap();
db.flush().unwrap();
db.put(b"memtable_key", b"memtable_value").unwrap();
let cp = Checkpoint::new(&db).unwrap();
let large_log_size = u64::MAX;
let cp_intact_path = DBPath::new(&format!("{PATH_PREFIX}cp_intact"));
cp.create_checkpoint_with_log_size(&cp_intact_path, large_log_size)
.unwrap();
let cp_truncated_path = DBPath::new(&format!("{PATH_PREFIX}cp_truncated"));
cp.create_checkpoint_with_log_size(&cp_truncated_path, large_log_size)
.unwrap();
let wal_files: Vec<_> = fs::read_dir((&cp_truncated_path).as_ref())
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "log"))
.map(|entry| entry.path())
.collect();
for wal_file in &wal_files {
fs::write(wal_file, b"").unwrap();
}
let cp_db_intact = DB::open_default(&cp_intact_path).unwrap();
assert_eq!(
cp_db_intact.get(b"flushed_key").unwrap().unwrap(),
b"flushed_value"
);
assert_eq!(
cp_db_intact.get(b"memtable_key").unwrap().unwrap(),
b"memtable_value",
"memtable_key should be present when WAL is intact"
);
let cp_db_truncated = DB::open_default(&cp_truncated_path).unwrap();
assert_eq!(
cp_db_truncated.get(b"flushed_key").unwrap().unwrap(),
b"flushed_value"
);
assert!(
cp_db_truncated.get(b"memtable_key").unwrap().is_none(),
"memtable_key should be absent when WAL is truncated"
);
}
#[test]
#[ignore]
fn test_checkpoint_wal_over_threshold_is_flushed() {
const PATH_PREFIX: &str = "_rust_rocksdb_cp_wal_threshold_";
let db_path = DBPath::new(&format!("{PATH_PREFIX}db"));
let mut opts = Options::default();
opts.create_if_missing(true);
let db = DB::open(&opts, &db_path).unwrap();
let threshold = 50 * 1024 * 1024_u64; let value = vec![b'x'; 1024]; let mut i = 0;
loop {
let key = format!("key_{i:08}");
db.put(key.as_bytes(), &value).unwrap();
i += 1;
if i % 1000 == 0 {
let wal_size: u64 = fs::read_dir((&db_path).as_ref())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "log"))
.map(|e| e.metadata().unwrap().len())
.sum();
if wal_size > threshold {
break;
}
}
}
let cp = Checkpoint::new(&db).unwrap();
let cp_path = DBPath::new(&format!("{PATH_PREFIX}cp"));
cp.create_checkpoint_with_log_size(&cp_path, threshold)
.unwrap();
let cp_wal_size: u64 = fs::read_dir((&cp_path).as_ref())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "log"))
.map(|e| e.metadata().unwrap().len())
.sum();
assert_eq!(
cp_wal_size, 0,
"Checkpoint WAL should be empty when WAL size exceeds log_size_for_flush threshold"
);
let cp_sst_count = fs::read_dir((&cp_path).as_ref())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "sst"))
.count();
assert!(
cp_sst_count > 0,
"Checkpoint should contain SST files when flush is triggered"
);
let cp_db = DB::open_default(&cp_path).unwrap();
assert!(cp_db.get(b"key_00000000").unwrap().is_some());
}
#[test]
fn test_checkpoint_outlive_db() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/fail/checkpoint_outlive_db.rs");
}