use super::*;
use crate::TreeType;
fn empty_version(id: u64) -> Version {
Version::new(id, TreeType::Standard)
}
#[expect(clippy::expect_used, reason = "test code")]
fn l0_table(
fs: &Arc<dyn crate::fs::Fs>,
id: TableId,
key: &[u8],
recency: Option<TableId>,
) -> crate::Result<Table> {
use crate::{InternalValue, ValueType};
let path = std::path::absolute(format!("/l0/{id}"))?;
let mut writer =
crate::table::Writer::new(path.clone(), id, 0, Arc::clone(fs))?.use_recency(recency);
writer.write(InternalValue::from_components(
key.to_vec(),
id.to_le_bytes().to_vec(),
7,
ValueType::Value,
))?;
let (_, checksum) = writer.finish()?.expect("the table is not empty");
Table::recover(crate::table::RecoverParams::new(
path,
checksum,
id,
Arc::clone(fs),
crate::comparator::default_comparator(),
Arc::new(crate::Cache::with_capacity_bytes(1_000_000)),
))
}
fn memfs() -> crate::Result<Arc<dyn crate::fs::Fs>> {
use crate::fs::Fs;
let fs = crate::fs::MemFs::new();
fs.create_dir_all(&std::path::absolute("/l0")?)?;
Ok(Arc::new(fs))
}
fn l0_with(runs: Vec<Vec<Table>>) -> Version {
let mut levels = vec![Level::from_runs(
runs.into_iter()
.filter_map(Run::new)
.map(Arc::new)
.collect(),
)];
levels.extend((1..DEFAULT_LEVEL_COUNT).map(|_| Level::empty()));
Version::from_levels(
0,
TreeType::Standard,
levels,
BlobFileList::new(crate::HashMap::default()),
FragmentationMap::default(),
)
}
fn l0_ids(version: &Version) -> Vec<Vec<TableId>> {
version
.l0()
.iter()
.map(|run| run.iter().map(Table::id).collect())
.collect()
}
#[test]
fn open_keeps_the_persisted_l0_order_of_a_table_without_a_recency_key() -> crate::Result<()> {
use super::recovery::{RecoveredTable, Recovery, RecoveryStats};
let fs = memfs()?;
let flush = l0_table(&fs, 1, b"k", Some(1))?;
let output = l0_table(&fs, 2, b"k", None)?;
let entry = |table: &Table| RecoveredTable {
id: table.id(),
checksum: table.checksum(),
global_seqno: 0,
};
let mut table_ids = vec![vec![vec![entry(&flush)], vec![entry(&output)]]];
table_ids.resize(usize::from(DEFAULT_LEVEL_COUNT), Vec::new());
let recovery = Recovery {
tree_type: TreeType::Standard,
snapshot_id: 0,
curr_version_id: 0,
table_ids,
blob_file_ids: Vec::new(),
gc_stats: FragmentationMap::default(),
restrictions: crate::HashMap::default(),
blob_restrictions: crate::HashMap::default(),
retention_floor: 0,
dicts: Vec::new(),
stats: RecoveryStats::default(),
};
let version = Version::from_recovery(recovery, &[flush, output], &[])?;
assert_eq!(l0_ids(&version), vec![vec![1], vec![2]]);
Ok(())
}
#[test]
fn a_flush_keeps_the_l0_order_of_a_table_without_a_recency_key() -> crate::Result<()> {
let fs = memfs()?;
let flushed = l0_table(&fs, 1, b"k", Some(1))?;
let output = l0_table(&fs, 2, b"k", None)?;
let newer = l0_table(&fs, 3, b"z", Some(3))?;
let version = l0_with(vec![vec![flushed], vec![output]]);
let cmp = crate::comparator::default_comparator();
let version = version.with_new_l0_run(&[newer], None, None, &TransformContext::new(&*cmp));
let ids = l0_ids(&version);
let at = |id: TableId| ids.iter().position(|run| run.contains(&id));
assert!(
at(1) < at(2),
"the flush stays ahead of the table it overlaps: {ids:?}"
);
Ok(())
}
fn merge_into_l0(version: &Version, inputs: &[TableId], output: Table) -> Version {
let cmp = crate::comparator::default_comparator();
version.with_merge(
inputs,
&[output],
0,
None,
Vec::new(),
&crate::HashSet::default(),
&TransformContext::new(&*cmp),
)
}
#[test]
fn an_intra_l0_output_stays_behind_a_flush_that_landed_in_an_inputs_run() -> crate::Result<()> {
let fs = memfs()?;
let flush = l0_table(&fs, 9, b"m", Some(9))?;
let i = l0_table(&fs, 2, b"a", Some(2))?;
let j = l0_table(&fs, 1, b"m", Some(1))?;
let version = l0_with(vec![vec![i, flush], vec![j]]);
let output = l0_table(&fs, 10, b"m", Some(2))?;
let version = merge_into_l0(&version, &[2, 1], output);
assert_eq!(l0_ids(&version), vec![vec![9], vec![10]]);
Ok(())
}
#[test]
fn a_healed_l0_table_keeps_its_place() -> crate::Result<()> {
let fs = memfs()?;
let x = l0_table(&fs, 5, b"k", None)?;
let t = l0_table(&fs, 3, b"k", None)?;
let y = l0_table(&fs, 1, b"k", None)?;
let version = l0_with(vec![vec![x], vec![t], vec![y]]);
let healed = l0_table(&fs, 9, b"k", Some(3))?;
let version = merge_into_l0(&version, &[3], healed);
assert_eq!(l0_ids(&version), vec![vec![5], vec![9], vec![1]]);
Ok(())
}
#[test]
fn an_intra_l0_output_goes_ahead_of_the_older_tables_behind_its_inputs() -> crate::Result<()> {
let fs = memfs()?;
let input = l0_table(&fs, 4, b"k", Some(4))?;
let older = l0_table(&fs, 3, b"k", Some(3))?;
let legacy = l0_table(&fs, 2, b"k", None)?;
let version = l0_with(vec![vec![input], vec![older], vec![legacy]]);
let output = l0_table(&fs, 5, b"k", Some(4))?;
let version = merge_into_l0(&version, &[4], output);
assert_eq!(l0_ids(&version), vec![vec![5], vec![3], vec![2]]);
Ok(())
}
#[test]
fn encode_into_refuses_a_level_with_more_than_255_runs() -> crate::Result<()> {
use crate::{AbstractTree, Config, SequenceNumberCounter, fs::StdFs};
let dir = tempfile::tempdir()?;
let tree = Config::new(
dir.path(),
SequenceNumberCounter::default(),
SequenceNumberCounter::default(),
)
.open()?;
for i in 0..256u64 {
let key = format!("k{i:05}");
tree.insert(key.as_bytes(), key.as_bytes(), 2 * i + 1);
tree.insert(b"zzz", key.as_bytes(), 2 * i + 2);
tree.flush_active_memtable(2 * i + 2)?;
}
let version = tree.current_version();
assert_eq!(version.l0().run_count(), 256);
assert!(!version.fits_snapshot());
let out = tempfile::tempdir()?;
let mut writer = crate::manifest_blocks::writer::ManifestArchiveWriter::create(
&out.path().join("v0"),
&StdFs,
Arc::new(crate::runtime_config::RuntimeConfig::default()),
None,
crate::fs::SyncMode::Normal,
)?;
let Err(crate::Error::Io(err)) = version.encode_into(&mut writer, "default") else {
panic!("a level of 256 runs must be refused");
};
assert_eq!(err.kind(), crate::io::ErrorKind::InvalidInput);
Ok(())
}
#[test]
fn set_retention_floor_below_the_current_one_is_a_no_op() {
let mut v = empty_version(1).with_retention_floor(40);
v.set_retention_floor(10);
assert_eq!(v.retention_floor(), 40, "the floor never goes down");
v.set_retention_floor(40);
assert_eq!(v.retention_floor(), 40, "equal is not above");
}
#[test]
fn set_retention_floor_on_a_sole_owner_writes_through() {
let mut v = empty_version(1);
let before = Arc::as_ptr(&v.inner);
v.set_retention_floor(40);
assert_eq!(v.retention_floor(), 40);
assert!(
core::ptr::eq(before, Arc::as_ptr(&v.inner)),
"a sole owner keeps its allocation instead of rebuilding it",
);
}
#[test]
fn set_retention_floor_on_a_shared_handle_leaves_the_other_owner_alone() {
let mut v = empty_version(1);
let shared = v.clone();
v.set_retention_floor(40);
assert_eq!(
v.retention_floor(),
40,
"the raised handle carries the floor"
);
assert_eq!(
shared.retention_floor(),
0,
"the other owner must not see a floor it never recorded",
);
assert!(
!core::ptr::eq(Arc::as_ptr(&v.inner), Arc::as_ptr(&shared.inner)),
"a shared handle rebuilds rather than mutating in place",
);
}