use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy)]
pub struct IndexEntry {
pub sequence: u64,
pub file_offset: u64,
pub timestamp_ns: u64,
}
impl IndexEntry {
pub const SERIALIZED_SIZE: usize = 24;
pub fn serialize(&self, buf: &mut [u8]) {
buf[0..8].copy_from_slice(&self.sequence.to_le_bytes());
buf[8..16].copy_from_slice(&self.file_offset.to_le_bytes());
buf[16..24].copy_from_slice(&self.timestamp_ns.to_le_bytes());
}
pub fn deserialize(buf: &[u8]) -> Self {
let record_id = u64::from_le_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
]);
let file_offset = u64::from_le_bytes([
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
]);
let timestamp_ns = u64::from_le_bytes([
buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
]);
Self {
sequence: record_id,
file_offset,
timestamp_ns,
}
}
}
#[derive(Debug, Clone)]
pub struct SparseIndex {
entries: Vec<IndexEntry>,
stride: u32,
}
impl SparseIndex {
pub const DEFAULT_STRIDE: u32 = 1024;
pub fn new(stride: u32) -> Self {
Self {
entries: Vec::new(),
stride,
}
}
pub fn add(&mut self, sequence: u64, file_offset: u64, timestamp_ns: u64) {
self.entries.push(IndexEntry {
sequence,
file_offset,
timestamp_ns,
});
}
pub fn should_index(&self, record_count: u64) -> bool {
record_count % self.stride as u64 == 0
}
pub fn find_anchor(&self, record_id: u64) -> Option<(IndexEntry, usize)> {
if self.entries.is_empty() {
return None;
}
let idx = match self
.entries
.binary_search_by(|e| e.sequence.cmp(&record_id))
{
Ok(pos) => pos, Err(pos) if pos > 0 => pos - 1, _ => return None, };
Some((self.entries[idx], idx))
}
pub fn find_by_time(&self, timestamp_ns: u64) -> usize {
match self
.entries
.binary_search_by(|e| e.timestamp_ns.cmp(×tamp_ns))
{
Ok(pos) => pos,
Err(pos) => pos.saturating_sub(1), }
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn stride(&self) -> u32 {
self.stride
}
pub fn save(&self, path: &Path) -> std::io::Result<()> {
let mut buf = Vec::with_capacity(self.entries.len() * IndexEntry::SERIALIZED_SIZE + 4);
buf.extend_from_slice(&self.stride.to_le_bytes());
for entry in &self.entries {
let mut entry_buf = [0u8; IndexEntry::SERIALIZED_SIZE];
entry.serialize(&mut entry_buf);
buf.extend_from_slice(&entry_buf);
}
fs::write(path, &buf)?;
Ok(())
}
pub fn load(path: &Path) -> std::io::Result<Self> {
let data = fs::read(path)?;
if data.len() < 4 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"index file too short",
));
}
let stride = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
let entry_size = IndexEntry::SERIALIZED_SIZE;
let num_entries = (data.len() - 4) / entry_size;
let mut entries = Vec::with_capacity(num_entries);
for i in 0..num_entries {
let start = 4 + i * entry_size;
entries.push(IndexEntry::deserialize(&data[start..start + entry_size]));
}
Ok(Self { entries, stride })
}
pub fn index_path(segment_path: &Path) -> PathBuf {
let stem = segment_path.file_stem().unwrap().to_str().unwrap();
segment_path.with_file_name(format!("{}.idx", stem))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_entry_round_trip() {
let entry = IndexEntry {
sequence: 42,
file_offset: 1024,
timestamp_ns: 5000,
};
let mut buf = [0u8; IndexEntry::SERIALIZED_SIZE];
entry.serialize(&mut buf);
let decoded = IndexEntry::deserialize(&buf);
assert_eq!(decoded.sequence, 42);
assert_eq!(decoded.file_offset, 1024);
assert_eq!(decoded.timestamp_ns, 5000);
}
#[test]
fn find_anchor_exact_match() {
let mut idx = SparseIndex::new(10);
for i in 0..5 {
idx.add(i * 10, i * 100, i * 1000);
}
let (anchor, pos) = idx.find_anchor(20).unwrap();
assert_eq!(anchor.sequence, 20);
assert_eq!(pos, 2);
}
#[test]
fn find_anchor_between() {
let mut idx = SparseIndex::new(10);
idx.add(0, 0, 0);
idx.add(10, 100, 1000);
idx.add(20, 200, 2000);
let (anchor, _) = idx.find_anchor(15).unwrap();
assert_eq!(anchor.sequence, 10);
}
#[test]
fn find_anchor_before_first() {
let mut idx = SparseIndex::new(10);
idx.add(10, 100, 1000);
assert!(idx.find_anchor(5).is_none());
}
#[test]
fn save_and_load() {
let dir = tempfile::tempdir().unwrap();
let idx_path = dir.path().join("test.idx");
let mut idx = SparseIndex::new(1024);
idx.add(0, 128, 0);
idx.add(1024, 256000, 5000);
idx.save(&idx_path).unwrap();
let loaded = SparseIndex::load(&idx_path).unwrap();
assert_eq!(loaded.stride, 1024);
assert_eq!(loaded.entries.len(), 2);
assert_eq!(loaded.entries[0].sequence, 0);
assert_eq!(loaded.entries[1].sequence, 1024);
}
#[test]
fn index_path_from_segment() {
let seg = Path::new("/data/segment-00000001.log");
let idx = SparseIndex::index_path(seg);
assert_eq!(idx, Path::new("/data/segment-00000001.idx"));
}
}