use super::*;
use crate::cache::Cache;
use crate::descriptor_table::DescriptorTable;
use crate::fs::StdFs;
use crate::path::Path;
use crate::table::Writer;
#[cfg(feature = "columnar")]
use crate::{SeqNo, hash::hash64};
use alloc::sync::Arc;
use test_log::test;
#[cfg(feature = "metrics")]
use crate::metrics::Metrics;
fn output_at(path: &Path, id: TableId) -> crate::Result<ClaimedFile> {
use crate::fs::Fs;
Ok(ClaimedFile {
id,
path: path.to_path_buf(),
file: StdFs.open(path, &FsOpenOptions::new().write(true).create_new(true))?,
durable_entry: false,
writing: crate::table::file_pool::Writing::alone(id),
})
}
fn recover_at(file: &Path, checksum: Checksum, table_id: TableId) -> crate::Result<Table> {
#[cfg(feature = "metrics")]
let metrics = Arc::new(Metrics::default());
let mut params = crate::table::RecoverParams::new(
file.to_path_buf(),
checksum,
table_id,
Arc::new(StdFs),
crate::comparator::default_comparator(),
Arc::new(Cache::with_capacity_bytes(1_000_000)),
);
params.descriptor_table = Some(Arc::new(DescriptorTable::new(10)));
#[cfg(feature = "metrics")]
{
params.metrics = metrics;
}
Table::recover(params)
}
#[cfg(feature = "columnar")]
#[test]
fn relocate_reuses_blocks_and_masks_deleted_rows() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let src_path = dir.path().join("src");
let out_path = dir.path().join("out");
let n = 96u32;
let deleted = [4u32, 7, 40, 95];
let mut writer = Writer::new(src_path.clone(), 0, 0, Arc::new(StdFs))?
.use_columnar(true)
.use_zone_map(true);
for i in 0..n {
let key = format!("k{i:04}").into_bytes();
writer.write(InternalValue::from_components(
key,
b"val",
1,
crate::ValueType::Value,
))?;
}
let (_, src_checksum) = writer.finish()?.expect("source table written");
let source = recover_at(&src_path, src_checksum, 0)?;
let mut bitmap = DeleteBitmap::new();
for &row in &deleted {
bitmap.insert(row);
}
let out_checksum = source
.relocate_columnar_with_deletes(
|| output_at(&out_path, 1),
&StdFs,
&bitmap,
SyncMode::Normal,
None,
)?
.checksum;
let relocated = recover_at(&out_path, out_checksum, 1)?;
assert_eq!(relocated.metadata.id, 1, "meta carries the new table id");
assert!(relocated.metadata.columnar, "columnar flag preserved");
for i in 0..n {
let key = format!("k{i:04}").into_bytes();
let got = relocated.get(&key, SeqNo::MAX, hash64(&key))?;
if deleted.contains(&i) {
assert!(
got.is_none(),
"deleted row {i} must read absent after relocate"
);
} else {
let got = got.expect("live row must survive relocate");
assert_eq!(&*got.value, b"val", "live value preserved verbatim");
}
}
Ok(())
}
#[cfg(feature = "columnar")]
#[test]
fn a_relocated_copy_records_what_its_folder_was_writing_below_it() -> crate::Result<()> {
use crate::table::file_pool::TableFilePools;
let dir = tempfile::tempdir()?;
let fs: Arc<dyn crate::fs::Fs> = Arc::new(StdFs);
let pools = TableFilePools::new(crate::SequenceNumberCounter::default(), 0, SyncMode::Normal);
let claim = || pools.claim(dir.path(), &fs, None, true);
let earlier = claim()?;
let source_file = claim()?;
let (source_id, source_path) = (source_file.id, source_file.path.clone());
let mut writer = Writer::into_claimed(source_file, 0, Arc::clone(&fs))?
.use_columnar(true)
.use_zone_map(true);
for i in 0..96u32 {
writer.write(InternalValue::from_components(
format!("k{i:04}").into_bytes(),
b"val",
1,
crate::ValueType::Value,
))?;
}
let (_, source_checksum) = writer.finish()?.expect("source table written");
let source = recover_at(&source_path, source_checksum, source_id)?;
assert_eq!(
source.metadata.writing_below,
vec![earlier.id],
"self-check: the source records the file written below it"
);
drop(earlier);
let pending = claim()?;
let mut bitmap = DeleteBitmap::new();
bitmap.insert(3);
let relocated =
source.relocate_columnar_with_deletes(claim, &StdFs, &bitmap, SyncMode::Normal, None)?;
let copy = recover_at(&relocated.path, relocated.checksum, relocated.id)?;
assert_eq!(copy.metadata.writing_below, vec![pending.id]);
Ok(())
}
#[cfg(all(feature = "columnar", feature = "std"))]
#[test]
fn a_paced_relocation_reports_the_reads_of_its_source() -> crate::Result<()> {
use crate::table::util::{Pacer, ReadPacer};
use core::sync::atomic::{AtomicU64, Ordering};
struct Recorder {
bytes: AtomicU64,
timed: AtomicU64,
}
impl ReadPacer for Recorder {
fn active(&self) -> bool {
true
}
fn pace(&self, _offset: u64, len: u64) {
self.bytes.fetch_add(len, Ordering::Relaxed);
}
fn read_took(&self, _elapsed: core::time::Duration) {
self.timed.fetch_add(1, Ordering::Relaxed);
}
}
let dir = tempfile::tempdir()?;
let src_path = dir.path().join("src");
let out_path = dir.path().join("out");
let mut writer = Writer::new(src_path.clone(), 0, 0, Arc::new(StdFs))?
.use_columnar(true)
.use_zone_map(true);
for i in 0..2_000u32 {
writer.write(InternalValue::from_components(
format!("k{i:06}").into_bytes(),
format!("value-{i}-payload").into_bytes(),
1,
crate::ValueType::Value,
))?;
}
let (_, src_checksum) = writer.finish()?.expect("source table written");
let source = recover_at(&src_path, src_checksum, 0)?;
let mut bitmap = DeleteBitmap::new();
bitmap.insert(3);
let recorder = Arc::new(Recorder {
bytes: AtomicU64::new(0),
timed: AtomicU64::new(0),
});
let pace: Pacer = recorder.clone();
source.relocate_columnar_with_deletes(
|| output_at(&out_path, 1),
&StdFs,
&bitmap,
SyncMode::Normal,
Some(&pace),
)?;
let file_len = std::fs::metadata(&src_path)?.len();
let paced = recorder.bytes.load(Ordering::Relaxed);
assert!(
paced * 2 > file_len,
"the section copy was not paced: {paced} of {file_len} bytes",
);
assert!(
recorder.timed.load(Ordering::Relaxed) > 0,
"no read was timed"
);
Ok(())
}
#[cfg(feature = "columnar")]
#[test]
fn relocated_mor_table_passes_metadata_bounds_cross_check() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let src_path = dir.path().join("src");
let out_path = dir.path().join("out");
let n = 96u32;
let mut writer = Writer::new(src_path.clone(), 0, 0, Arc::new(StdFs))?
.use_columnar(true)
.use_zone_map(true);
for i in 0..n {
writer.write(InternalValue::from_components(
format!("k{i:04}").into_bytes(),
b"val",
1,
crate::ValueType::Value,
))?;
}
let (_, src_checksum) = writer.finish()?.expect("source table written");
let source = recover_at(&src_path, src_checksum, 0)?;
let mut bitmap = DeleteBitmap::new();
for &row in &[4u32, 7, 40, 95] {
bitmap.insert(row);
}
let out_checksum = source
.relocate_columnar_with_deletes(
|| output_at(&out_path, 1),
&StdFs,
&bitmap,
SyncMode::Normal,
None,
)?
.checksum;
let relocated = recover_at(&out_path, out_checksum, 1)?;
if let Err((gate, e)) = relocated.verify_reconcile_gates(None, false) {
panic!("a healthy relocated table must pass every gate, {gate:?} refused it: {e}");
}
Ok(())
}
#[cfg(feature = "columnar")]
#[test]
fn a_relocated_table_carries_a_complete_single_output_lineage() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let src_path = dir.path().join("src");
let out_path = dir.path().join("out");
let mut writer = Writer::new(src_path.clone(), 7, 0, Arc::new(StdFs))?
.use_columnar(true)
.use_zone_map(true);
for i in 0..96u32 {
writer.write(InternalValue::from_components(
format!("k{i:04}").into_bytes(),
b"val",
1,
crate::ValueType::Value,
))?;
}
let (_, src_checksum) = writer.finish()?.expect("source table written");
let source = recover_at(&src_path, src_checksum, 7)?;
let mut bitmap = DeleteBitmap::new();
bitmap.insert(4);
let out_checksum = source
.relocate_columnar_with_deletes(
|| output_at(&out_path, 9),
&StdFs,
&bitmap,
SyncMode::Normal,
None,
)?
.checksum;
let relocated = recover_at(&out_path, out_checksum, 9)?;
assert_eq!(
relocated.metadata.lineage.as_deref(),
Some(&[7u64][..]),
"the copy's lineage names exactly its source",
);
assert_eq!(
relocated.metadata.lineage_prev, None,
"a single-output run has no previous output",
);
assert!(
relocated.metadata.lineage_last,
"the single output closes its run",
);
assert_eq!(
relocated.metadata.recency, 7,
"the copy's content position is its source's, not its own newer id",
);
Ok(())
}
#[cfg(feature = "columnar")]
#[test]
fn relocate_rejects_a_restricted_view() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let src_path = dir.path().join("src");
let mut writer = Writer::new(src_path.clone(), 0, 0, Arc::new(StdFs))?
.use_columnar(true)
.use_zone_map(true);
for i in 0..32u32 {
writer.write(InternalValue::from_components(
format!("k{i:04}").into_bytes(),
b"val",
1,
crate::ValueType::Value,
))?;
}
let (_, checksum) = writer.finish()?.expect("table written");
let source = recover_at(&src_path, checksum, 0)?;
let restricted = source.reopen_restricted(crate::UserKey::from(&b"k0016"[..]))?;
let out_path = dir.path().join("out");
let mut bitmap = DeleteBitmap::new();
bitmap.insert(0);
let err = restricted
.relocate_columnar_with_deletes(
|| output_at(&out_path, 1),
&StdFs,
&bitmap,
SyncMode::Normal,
None,
)
.unwrap_err();
assert!(
matches!(err, crate::Error::FeatureUnsupported(_)),
"a restricted view must be rejected, got {err:?}",
);
Ok(())
}
#[test]
fn relocate_rejects_row_major_segment() -> crate::Result<()> {
let dir = tempfile::tempdir()?;
let src_path = dir.path().join("src");
let mut writer = Writer::new(src_path.clone(), 0, 0, Arc::new(StdFs))?.use_zone_map(true);
writer.write(InternalValue::from_components(
b"a",
b"v",
1,
crate::ValueType::Value,
))?;
let (_, checksum) = writer.finish()?.expect("table written");
let source = recover_at(&src_path, checksum, 0)?;
let out_path = dir.path().join("out");
let mut bitmap = DeleteBitmap::new();
bitmap.insert(0);
let err = source
.relocate_columnar_with_deletes(
|| output_at(&out_path, 1),
&StdFs,
&bitmap,
SyncMode::Normal,
None,
)
.unwrap_err();
assert!(
matches!(err, crate::Error::FeatureUnsupported(_)),
"row-major segment must be rejected, got {err:?}",
);
Ok(())
}
#[cfg(feature = "columnar")]
#[test]
fn relocated_mor_table_passes_the_blob_link_cross_check() -> crate::Result<()> {
use crate::blob_tree::handle::BlobIndirection;
use crate::coding::Encode;
use crate::vlog::ValueHandle;
let dir = tempfile::tempdir()?;
let src_path = dir.path().join("src");
let out_path = dir.path().join("out");
let n = 8u32;
let mut writer = Writer::new(src_path.clone(), 0, 0, Arc::new(StdFs))?
.use_columnar(true)
.use_zone_map(true);
let mut bytes_sum = 0u64;
let mut on_disk_sum = 0u64;
for i in 0..n {
let ind = BlobIndirection {
vhandle: ValueHandle {
blob_file_id: 5,
offset: u64::from(i) * 100,
on_disk_size: 40,
},
size: 80,
};
bytes_sum += u64::from(ind.size);
on_disk_sum += u64::from(ind.vhandle.on_disk_size);
let mut val = alloc::vec::Vec::new();
ind.encode_into(&mut val)?;
writer.write(InternalValue::from_components(
format!("k{i:04}").into_bytes(),
val,
1,
crate::ValueType::Indirection,
))?;
}
writer.link_blob_file(crate::table::writer::LinkedFile {
blob_file_id: 5,
len: n as usize,
bytes: bytes_sum,
on_disk_bytes: on_disk_sum,
first_key: b"k0000".into(),
last_key: format!("k{:04}", n - 1).into_bytes().into(),
});
let (_, src_checksum) = writer.finish()?.expect("source table written");
let source = recover_at(&src_path, src_checksum, 0)?;
let mut bitmap = DeleteBitmap::new();
bitmap.insert(3);
let out_checksum = source
.relocate_columnar_with_deletes(
|| output_at(&out_path, 1),
&StdFs,
&bitmap,
SyncMode::Normal,
None,
)?
.checksum;
let relocated = recover_at(&out_path, out_checksum, 1)?;
relocated
.verify_blob_links()
.expect("a healthy relocated MoR table passes the blob-link cross-check");
Ok(())
}