use regolith::{
CompactionStyle, CompressionType, Db, DurabilityMode, FifoCompactionOptions, Options,
};
use tempfile::TempDir;
use super::{assert_range_present, opts, sst_format_versions, write_range};
fn tuned(set: impl FnOnce(&mut Options)) -> Options {
let mut o = opts();
set(&mut o);
o
}
fn seeded_dir(n: usize, base: &Options) -> TempDir {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), base.clone()).unwrap();
write_range(&db, 0, n);
db.compact_range(None, None).unwrap();
db.close().unwrap();
drop(db);
dir
}
fn assert_option_change_preserves_data(label: &str, from: Options, to: Options) {
const SEEDED: usize = 400;
const ALL: usize = 800;
let stage = |s: &str| format!("{label}: {s}");
let dir = seeded_dir(SEEDED, &from);
let db = Db::open(dir.path(), to)
.unwrap_or_else(|e| panic!("{label}: reopening with the new options failed: {e}"));
assert_range_present(&db, 0, SEEDED, &stage("after the change"));
write_range(&db, SEEDED, ALL);
assert_range_present(&db, 0, ALL, &stage("files from both option sets"));
db.compact_range(None, None).unwrap();
assert_range_present(&db, 0, ALL, &stage("after compacting the mix"));
db.close().unwrap();
drop(db);
let db = Db::open(dir.path(), from)
.unwrap_or_else(|e| panic!("{label}: reopening with the original options failed: {e}"));
assert_range_present(&db, 0, ALL, &stage("after changing back"));
db.close().unwrap();
}
#[test]
fn reopening_with_a_resized_write_buffer_keeps_every_key() {
let small = tuned(|o| o.write_buffer_size = 4 * 1024);
let large = tuned(|o| o.write_buffer_size = 8 * 1024 * 1024);
assert_option_change_preserves_data("4 KiB -> 8 MiB", small.clone(), large.clone());
assert_option_change_preserves_data("8 MiB -> 4 KiB", large, small);
}
#[test]
fn reopening_with_a_different_block_or_bloom_geometry_keeps_every_key() {
let tight = tuned(|o| {
o.block_size = 1024;
o.bloom_bits_per_key = 4;
});
let loose = tuned(|o| {
o.block_size = 64 * 1024;
o.bloom_bits_per_key = 20;
});
assert_option_change_preserves_data(
"1 KiB/4 bits -> 64 KiB/20 bits",
tight.clone(),
loose.clone(),
);
assert_option_change_preserves_data("64 KiB/20 bits -> 1 KiB/4 bits", loose, tight);
}
#[test]
fn reopening_with_a_different_compression_codec_keeps_every_key() {
let codecs = [
CompressionType::None,
CompressionType::Snappy,
CompressionType::Lz4,
];
for from in codecs {
for to in codecs {
if from == to {
continue;
}
let mk = |c| tuned(|o| o.compression = c);
assert_option_change_preserves_data(&format!("{from:?} -> {to:?}"), mk(from), mk(to));
}
}
}
#[test]
fn reopening_under_a_different_compaction_style_keeps_every_key() {
let leveled = tuned(|o| o.compaction_style = CompactionStyle::Level);
let universal = tuned(|o| o.compaction_style = CompactionStyle::Universal);
let fifo = tuned(|o| {
o.compaction_style = CompactionStyle::Fifo;
o.fifo_compaction_options = FifoCompactionOptions {
max_table_files_size: 1 << 30,
};
});
let probe = seeded_dir(400, &leveled);
let db = Db::open(probe.path(), leveled.clone()).unwrap();
let below_l0: u64 = (1..=6)
.map(|level| {
db.get_int_property(&format!("regolith.num-files-at-level{level}"))
.unwrap()
})
.sum();
assert!(
below_l0 > 0,
"the leveled seed left every file at L0, so switching style proves nothing"
);
db.close().unwrap();
drop(db);
drop(probe);
assert_option_change_preserves_data("level -> universal", leveled.clone(), universal.clone());
assert_option_change_preserves_data("level -> fifo", leveled.clone(), fifo.clone());
assert_option_change_preserves_data("universal -> level", universal, leveled.clone());
assert_option_change_preserves_data("fifo -> level", fifo, leveled);
}
#[test]
fn reopening_that_toggles_the_partitioned_index_reads_both_layouts() {
let flat = tuned(|o| o.l0_compaction_trigger = 1000);
let partitioned = tuned(|o| {
o.l0_compaction_trigger = 1000;
o.partitioned_index = true;
o.metadata_block_size = 512;
});
for (label, from, to, new_version) in [
("flat -> partitioned", &flat, &partitioned, 6u8),
("partitioned -> flat", &partitioned, &flat, 5u8),
] {
let stage = |s: &str| format!("{label}: {s}");
let dir = seeded_dir(400, from);
let db = Db::open(dir.path(), to.clone()).unwrap();
assert_range_present(&db, 0, 400, label);
write_range(&db, 400, 1200);
let versions = sst_format_versions(dir.path());
assert!(
versions.len() > 1 && versions.contains(&new_version),
"{label}: expected both footer versions on disk after the toggle, found {versions:?}"
);
assert_range_present(&db, 0, 1200, &stage("mixed index layouts"));
db.compact_range(None, None).unwrap();
assert_range_present(&db, 0, 1200, &stage("after compacting the mix"));
db.close().unwrap();
drop(db);
let db = Db::open(dir.path(), from.clone()).unwrap();
assert_range_present(&db, 0, 1200, &stage("after changing back"));
db.close().unwrap();
}
}
#[test]
fn reopening_with_a_different_durability_mode_keeps_every_key() {
let eventual = tuned(|o| o.durability = DurabilityMode::Eventual);
let immediate = tuned(|o| o.durability = DurabilityMode::Immediate);
assert_option_change_preserves_data(
"eventual -> immediate",
eventual.clone(),
immediate.clone(),
);
assert_option_change_preserves_data("immediate -> eventual", immediate, eventual);
}