use std::path::Path;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use super::SummaryEntry;
use crate::error::{Error, Result};
use crate::storage::sstable::index_reader::{parse_big_index_entry, PartitionIndexEntry};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SummaryInterval {
pub start_position: u64,
pub end_position: Option<u64>,
pub sample_index: usize,
}
pub(crate) fn find_interval_for_key(
entries: &[SummaryEntry],
key: &[u8],
) -> Option<SummaryInterval> {
use crate::util::cassandra_murmur3::cmp_partition_keys_by_token;
use std::cmp::Ordering;
if entries.is_empty() {
return None;
}
let le_count = entries.partition_point(|e| {
cmp_partition_keys_by_token(&e.partition_key, key) != Ordering::Greater
});
let floor = le_count.saturating_sub(1);
Some(SummaryInterval {
start_position: entries[floor].position,
end_position: entries.get(floor + 1).map(|e| e.position),
sample_index: floor,
})
}
#[derive(Debug)]
pub struct IntervalLookup {
pub entry: Option<PartitionIndexEntry>,
pub entries_touched: usize,
}
const BASE_SAMPLING_LEVEL: u32 = 128;
fn last_interval_entry_cap(min_index_interval: u32, sampling_level: u32) -> usize {
let effective_sampling_level = sampling_level.clamp(1, BASE_SAMPLING_LEVEL);
let effective_interval = (min_index_interval as u64).saturating_mul(BASE_SAMPLING_LEVEL as u64)
/ (effective_sampling_level as u64);
usize::try_from(effective_interval.saturating_add(1)).unwrap_or(usize::MAX)
}
pub(crate) fn scan_interval_bytes(bytes: &[u8], key: &[u8], entry_cap: usize) -> IntervalLookup {
let mut remaining = bytes;
let mut entries_touched = 0usize;
while !remaining.is_empty() && entries_touched < entry_cap {
match parse_big_index_entry(remaining) {
Ok((rest, entry)) => {
if rest.len() >= remaining.len() {
break;
}
entries_touched += 1;
if entry.key_digest.as_ref() == key {
return IntervalLookup {
entry: Some(entry),
entries_touched,
};
}
remaining = rest;
}
Err(_) => break,
}
}
IntervalLookup {
entry: None,
entries_touched,
}
}
pub async fn lookup_key_in_interval(
index_path: &Path,
interval: SummaryInterval,
key: &[u8],
min_index_interval: u32,
sampling_level: u32,
) -> Result<IntervalLookup> {
let mut file = File::open(index_path).await?;
let file_len = file.metadata().await?.len();
match interval.end_position {
Some(end) if end <= interval.start_position => {
return Err(Error::corruption(format!(
"lookup_key_in_interval: interval end_position {end} does not exceed \
start_position {} — non-monotone Summary.db sample ordering (corrupt input)",
interval.start_position
)));
}
Some(end) if end > file_len => {
return Err(Error::corruption(format!(
"lookup_key_in_interval: interval end_position {end} exceeds Index.db file \
length {file_len} (corrupt Summary.db sample position)"
)));
}
_ => {}
}
file.seek(std::io::SeekFrom::Start(interval.start_position))
.await?;
let mut buffer = Vec::new();
let entry_cap = match interval.end_position {
Some(end) => {
let len = (end - interval.start_position) as usize;
buffer.resize(len, 0);
file.read_exact(&mut buffer).await?;
usize::MAX
}
None => {
file.read_to_end(&mut buffer).await?;
last_interval_entry_cap(min_index_interval, sampling_level)
}
};
crate::observability::add_counter(
crate::observability::catalog::INDEX_INTERVAL_PARSES_TOTAL,
1,
&[],
);
Ok(scan_interval_bytes(&buffer, key, entry_cap))
}
#[cfg(test)]
mod tests {
use super::*;
fn token_ordered_samples(keys: &[&[u8]]) -> Vec<SummaryEntry> {
use crate::util::cassandra_murmur3::cmp_partition_keys_by_token;
let mut keys: Vec<Vec<u8>> = keys.iter().map(|k| k.to_vec()).collect();
keys.sort_by(|a, b| cmp_partition_keys_by_token(a, b));
keys.into_iter()
.enumerate()
.map(|(i, partition_key)| SummaryEntry {
partition_key,
position: (i as u64) * 128,
})
.collect()
}
#[test]
fn find_by_key_empty_summary_returns_none() {
assert_eq!(find_interval_for_key(&[], b"anything"), None);
}
#[test]
fn find_by_key_present_sample_bounds_one_interval() {
let samples = token_ordered_samples(&[b"aaaa", b"bbbb", b"cccc", b"dddd"]);
for i in 0..samples.len() {
let iv = find_interval_for_key(&samples, &samples[i].partition_key)
.expect("present key must resolve an interval");
assert_eq!(iv.sample_index, i, "floor must be the exact sample");
assert_eq!(iv.start_position, samples[i].position);
assert_eq!(
iv.end_position,
samples.get(i + 1).map(|e| e.position),
"end is next sample position, or None (EOF) for the last sample"
);
}
}
#[test]
fn find_by_key_between_samples_floors_to_lower() {
use crate::util::cassandra_murmur3::cmp_partition_keys_by_token;
use std::cmp::Ordering;
let samples = token_ordered_samples(&[b"k0", b"k1", b"k2", b"k3", b"k4"]);
for probe in [b"zz".as_slice(), b"m", b"q7", b"abcd", b"7", b"XYZ"] {
let iv = match find_interval_for_key(&samples, probe) {
Some(iv) => iv,
None => continue,
};
let floor = iv.sample_index;
assert_ne!(
cmp_partition_keys_by_token(&samples[floor].partition_key, probe),
Ordering::Greater,
"floor sample must be <= probe in token order"
);
if let Some(next) = samples.get(floor + 1) {
assert_eq!(
cmp_partition_keys_by_token(&next.partition_key, probe),
Ordering::Greater,
"the sample after the floor must be > probe in token order"
);
}
}
}
#[test]
fn find_by_key_below_first_sample_clamps_to_zero() {
use crate::util::cassandra_murmur3::cmp_partition_keys_by_token;
use std::cmp::Ordering;
let pool: [&[u8]; 6] = [b"p1", b"p2", b"p3", b"p4", b"p5", b"p6"];
let all = token_ordered_samples(&pool); let below = all[0].partition_key.clone(); let samples: Vec<SummaryEntry> = all[1..].to_vec(); assert_eq!(
cmp_partition_keys_by_token(&below, &samples[0].partition_key),
Ordering::Less,
"constructed probe must sort below the first remaining sample"
);
let iv = find_interval_for_key(&samples, &below).expect("interval");
assert_eq!(iv.sample_index, 0, "below-first key clamps to sample 0");
assert_eq!(iv.start_position, samples[0].position);
}
fn encode_entry(key: &[u8], data_offset: u64) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(key.len() as u16).to_be_bytes());
out.extend_from_slice(key);
out.extend_from_slice(&crate::parser::vint::encode_vuint(data_offset));
out.extend_from_slice(&crate::parser::vint::encode_vuint(0));
out
}
fn build_interval(keys_offsets: &[(&[u8], u64)]) -> Vec<u8> {
let mut buf = Vec::new();
for (k, off) in keys_offsets {
buf.extend_from_slice(&encode_entry(k, *off));
}
buf
}
#[test]
fn scan_finds_present_key_and_counts_bounded_entries() {
let bytes = build_interval(&[
(b"alpha", 0),
(b"bravo", 100),
(b"charlie", 250),
(b"delta", 400),
]);
let res = scan_interval_bytes(&bytes, b"charlie", 128);
let entry = res.entry.expect("present key must be found");
assert_eq!(entry.key_digest.as_ref(), b"charlie");
assert_eq!(entry.data_offset, 250);
assert_eq!(
res.entries_touched, 3,
"touched only up to and including the match"
);
}
#[test]
fn scan_absent_key_within_interval_is_authoritative_none() {
let bytes = build_interval(&[(b"alpha", 0), (b"bravo", 100), (b"charlie", 250)]);
let res = scan_interval_bytes(&bytes, b"zzz_absent", 128);
assert!(res.entry.is_none(), "absent key resolves to None");
assert_eq!(
res.entries_touched, 3,
"touched all interval entries, bounded"
);
}
#[test]
fn scan_respects_entry_cap() {
let bytes = build_interval(&[
(b"k0", 0),
(b"k1", 10),
(b"k2", 20),
(b"k3", 30),
(b"k4", 40),
]);
let res = scan_interval_bytes(&bytes, b"k4", 2);
assert!(res.entry.is_none(), "cap reached before the match");
assert_eq!(res.entries_touched, 2, "scan stopped at the cap");
}
#[test]
fn scan_stops_on_truncated_tail() {
let mut bytes = build_interval(&[(b"alpha", 0), (b"bravo", 100)]);
bytes.extend_from_slice(&[0x00, 0x40]); let res = scan_interval_bytes(&bytes, b"missing", 128);
assert!(res.entry.is_none());
assert_eq!(res.entries_touched, 2, "only the two whole entries counted");
}
#[tokio::test]
async fn lookup_reads_only_the_bounded_range_from_disk() {
let prefix = build_interval(&[(b"before0", 0), (b"before1", 5)]);
let interval = build_interval(&[(b"target", 900), (b"neighbor", 950)]);
let trailing = build_interval(&[(b"after0", 1000)]);
let mut file_bytes = prefix.clone();
file_bytes.extend_from_slice(&interval);
file_bytes.extend_from_slice(&trailing);
let dir = std::env::temp_dir().join(format!(
"cqlite-2412-interval-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("nb-1-big-Index.db");
std::fs::write(&path, &file_bytes).unwrap();
let iv = SummaryInterval {
start_position: prefix.len() as u64,
end_position: Some((prefix.len() + interval.len()) as u64),
sample_index: 1,
};
let res = lookup_key_in_interval(&path, iv, b"target", 128, 128)
.await
.expect("interval read");
let entry = res.entry.expect("target present in the interval");
assert_eq!(entry.key_digest.as_ref(), b"target");
assert_eq!(entry.data_offset, 900);
assert_eq!(res.entries_touched, 1, "matched the first interval entry");
let res2 = lookup_key_in_interval(&path, iv, b"after0", 128, 128)
.await
.expect("interval read");
assert!(
res2.entry.is_none(),
"a key past end_position is not read (authoritative miss for this interval)"
);
let _ = std::fs::remove_dir_all(&dir);
}
fn build_n_entries(n: usize, target_index: usize, target_key: &[u8]) -> (Vec<u8>, u64) {
let mut buf = Vec::new();
let mut target_end = 0u64;
for i in 0..n {
let key: Vec<u8> = if i == target_index {
target_key.to_vec()
} else {
format!("k{i:08}").into_bytes()
};
buf.extend_from_slice(&encode_entry(&key, i as u64));
if i == target_index {
target_end = buf.len() as u64;
}
}
(buf, target_end)
}
async fn write_temp_index(name: &str, bytes: &[u8]) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"cqlite-2412-ds-{name}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("nb-1-big-Index.db");
std::fs::write(&path, bytes).unwrap();
path
}
#[tokio::test]
async fn downsampled_end_bounded_interval_resolves_key_past_the_old_cap() {
const MIN_INDEX_INTERVAL: u32 = 128;
const SAMPLING_LEVEL: u32 = 32; const N: usize = 200; const TARGET_INDEX: usize = 150; let target_key = b"the-present-partition";
let (bytes, target_end) = build_n_entries(N, TARGET_INDEX, target_key);
let path = write_temp_index("bounded-present", &bytes).await;
let iv = SummaryInterval {
start_position: 0,
end_position: Some(bytes.len() as u64), sample_index: 0,
};
let _ = target_end;
let res = lookup_key_in_interval(&path, iv, target_key, MIN_INDEX_INTERVAL, SAMPLING_LEVEL)
.await
.expect("interval read");
assert!(
res.entry.is_some(),
"a present key at index {TARGET_INDEX} (past the pre-fix {}-entry cap) in a \
downsampled end-bounded interval MUST resolve — a miss here is the false \
authoritative-absence data-loss bug (roborev job 1709)",
MIN_INDEX_INTERVAL + 1
);
assert_eq!(res.entry.unwrap().key_digest.as_ref(), target_key);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[tokio::test]
async fn downsampled_end_bounded_interval_absent_key_is_still_authoritative() {
const MIN_INDEX_INTERVAL: u32 = 128;
const SAMPLING_LEVEL: u32 = 32;
const N: usize = 200;
let (bytes, _) = build_n_entries(N, usize::MAX, b"unused");
let path = write_temp_index("bounded-absent", &bytes).await;
let iv = SummaryInterval {
start_position: 0,
end_position: Some(bytes.len() as u64),
sample_index: 0,
};
let res = lookup_key_in_interval(
&path,
iv,
b"zzz-genuinely-absent-key",
MIN_INDEX_INTERVAL,
SAMPLING_LEVEL,
)
.await
.expect("interval read");
assert!(
res.entry.is_none(),
"a genuinely absent key in a downsampled end-bounded interval must still \
resolve to an authoritative absence"
);
assert_eq!(
res.entries_touched, N,
"the WHOLE interval's {N} entries must be scanned (no premature cap), \
proving the absence is genuinely exhaustive, not cap-truncated"
);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn last_interval_entry_cap_is_downsampling_adjusted() {
assert_eq!(last_interval_entry_cap(128, 128), 129);
assert_eq!(last_interval_entry_cap(128, 32), 513);
assert_eq!(last_interval_entry_cap(128, 1), 16385);
assert_eq!(last_interval_entry_cap(128, 0), 16385);
assert_eq!(last_interval_entry_cap(128, 9999), 129);
}
#[tokio::test]
async fn downsampled_last_interval_resolves_key_past_the_old_cap() {
const MIN_INDEX_INTERVAL: u32 = 128;
const SAMPLING_LEVEL: u32 = 32; const N: usize = 200;
const TARGET_INDEX: usize = 150;
let target_key = b"present-in-last-interval";
let (bytes, _) = build_n_entries(N, TARGET_INDEX, target_key);
let path = write_temp_index("last-present", &bytes).await;
let iv = SummaryInterval {
start_position: 0,
end_position: None, sample_index: 0,
};
let res = lookup_key_in_interval(&path, iv, target_key, MIN_INDEX_INTERVAL, SAMPLING_LEVEL)
.await
.expect("interval read");
assert!(
res.entry.is_some(),
"a present key past the pre-fix cap in a downsampled LAST interval must \
resolve too (the generous, downsampling-adjusted cap applies here)"
);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[tokio::test]
async fn end_position_beyond_file_length_is_a_clean_error_not_a_huge_alloc() {
let bytes = build_interval(&[(b"alpha", 0), (b"bravo", 100)]);
let path = write_temp_index("beyond-eof", &bytes).await;
const HOSTILE_END: u64 = 1 << 30;
let iv = SummaryInterval {
start_position: 0,
end_position: Some(HOSTILE_END),
sample_index: 0,
};
let res = lookup_key_in_interval(&path, iv, b"alpha", 128, 128).await;
let err = res.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("exceeds Index.db file length"),
"an end_position ({HOSTILE_END}) far beyond the file's real length ({}) must \
fail via the metadata-length pre-check (fast, no huge alloc attempted), got: {msg}",
bytes.len()
);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[tokio::test]
async fn non_monotone_interval_is_a_clean_error_never_authoritative() {
let bytes = build_interval(&[(b"alpha", 0), (b"bravo", 100), (b"charlie", 250)]);
let path = write_temp_index("non-monotone", &bytes).await;
let iv_equal = SummaryInterval {
start_position: 50,
end_position: Some(50),
sample_index: 0,
};
let res_equal = lookup_key_in_interval(&path, iv_equal, b"anything", 128, 128).await;
assert!(
res_equal.is_err(),
"end_position == start_position must fail closed, not silently read to EOF"
);
let iv_backwards = SummaryInterval {
start_position: 100,
end_position: Some(10),
sample_index: 0,
};
let res_backwards =
lookup_key_in_interval(&path, iv_backwards, b"anything", 128, 128).await;
assert!(
res_backwards.is_err(),
"end_position < start_position must fail closed, not silently read to EOF"
);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
}