use std::collections::{BTreeSet, HashMap};
use std::ops::Bound;
use std::sync::Arc;
use parking_lot::RwLock;
use chronix_core::{SegmentId, Timestamp};
#[derive(Debug, Clone, Eq)]
pub struct TimeIndexEntry {
pub segment_id: SegmentId,
pub min_ts: Timestamp,
pub max_ts: Timestamp,
}
impl PartialEq for TimeIndexEntry {
fn eq(&self, other: &Self) -> bool {
self.min_ts == other.min_ts && self.segment_id == other.segment_id
}
}
impl PartialOrd for TimeIndexEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TimeIndexEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.min_ts
.cmp(&other.min_ts)
.then_with(|| self.segment_id.cmp(&other.segment_id))
}
}
impl TimeIndexEntry {
#[inline]
#[must_use]
pub fn overlaps(&self, start: Timestamp, end: Timestamp) -> bool {
self.min_ts <= end && self.max_ts >= start
}
}
#[derive(Debug, Clone)]
pub struct TimeIndex {
entries: Arc<RwLock<BTreeSet<TimeIndexEntry>>>,
segment_meta: Arc<RwLock<HashMap<SegmentId, (Timestamp, Timestamp)>>>,
}
impl TimeIndex {
#[must_use]
pub fn new() -> Self {
Self {
entries: Arc::new(RwLock::new(BTreeSet::new())),
segment_meta: Arc::new(RwLock::new(HashMap::new())),
}
}
#[must_use]
pub fn segments_for_range(&self, start_ts: Timestamp, end_ts: Timestamp) -> Vec<SegmentId> {
let entries = self.entries.read();
if entries.is_empty() {
return Vec::new();
}
let upper = TimeIndexEntry {
segment_id: SegmentId(u64::MAX),
min_ts: end_ts,
max_ts: Timestamp::MAX,
};
entries
.range((Bound::Unbounded, Bound::Included(&upper)))
.filter(|e| e.max_ts >= start_ts)
.map(|e| e.segment_id)
.collect()
}
pub fn add_segment(&self, entry: TimeIndexEntry) {
let seg_id = entry.segment_id;
let meta_val = (entry.min_ts, entry.max_ts);
let mut entries = self.entries.write();
let mut meta = self.segment_meta.write();
entries.insert(entry);
meta.insert(seg_id, meta_val);
}
#[must_use]
pub fn remove_segment(&self, segment_id: SegmentId) -> bool {
let mut entries = self.entries.write();
let mut meta = self.segment_meta.write();
if let Some(&(min_ts, max_ts)) = meta.get(&segment_id) {
let key = TimeIndexEntry {
segment_id,
min_ts,
max_ts,
};
let removed = entries.remove(&key);
if removed {
meta.remove(&segment_id);
}
removed
} else {
false
}
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
#[must_use]
pub fn all_entries(&self) -> Vec<TimeIndexEntry> {
self.entries.read().iter().cloned().collect()
}
}
impl Default for TimeIndex {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(id: u64, min: i64, max: i64) -> TimeIndexEntry {
TimeIndexEntry {
segment_id: SegmentId(id),
min_ts: min,
max_ts: max,
}
}
#[test]
fn empty_index_returns_nothing() {
let idx = TimeIndex::new();
let result = idx.segments_for_range(0, 1000);
assert!(result.is_empty());
}
#[test]
fn single_segment_match() {
let idx = TimeIndex::new();
idx.add_segment(entry(1, 100, 200));
assert_eq!(idx.segments_for_range(100, 200), vec![SegmentId(1)]);
assert_eq!(idx.segments_for_range(150, 300), vec![SegmentId(1)]);
assert!(idx.segments_for_range(0, 99).is_empty());
assert!(idx.segments_for_range(201, 300).is_empty());
assert_eq!(idx.segments_for_range(200, 300), vec![SegmentId(1)]);
assert_eq!(idx.segments_for_range(0, 100), vec![SegmentId(1)]);
}
#[test]
fn multiple_segments_pruning() {
let idx = TimeIndex::new();
idx.add_segment(entry(1, 100, 200));
idx.add_segment(entry(2, 300, 400));
idx.add_segment(entry(3, 500, 600));
idx.add_segment(entry(4, 700, 800));
let result = idx.segments_for_range(250, 550);
assert_eq!(result, vec![SegmentId(2), SegmentId(3)]);
let result = idx.segments_for_range(0, 1000);
assert_eq!(
result,
vec![SegmentId(1), SegmentId(2), SegmentId(3), SegmentId(4)]
);
assert!(idx.segments_for_range(900, 1000).is_empty());
}
#[test]
fn overlapping_segments() {
let idx = TimeIndex::new();
idx.add_segment(entry(1, 100, 300));
idx.add_segment(entry(2, 200, 400));
idx.add_segment(entry(3, 350, 500));
let result = idx.segments_for_range(250, 350);
assert_eq!(result, vec![SegmentId(1), SegmentId(2), SegmentId(3)]);
}
#[test]
fn add_maintains_sort_order() {
let idx = TimeIndex::new();
idx.add_segment(entry(3, 500, 600));
idx.add_segment(entry(1, 100, 200));
idx.add_segment(entry(2, 300, 400));
let entries = idx.all_entries();
assert_eq!(entries[0].min_ts, 100);
assert_eq!(entries[1].min_ts, 300);
assert_eq!(entries[2].min_ts, 500);
}
#[test]
fn remove_segment() {
let idx = TimeIndex::new();
idx.add_segment(entry(1, 100, 200));
idx.add_segment(entry(2, 300, 400));
assert!(idx.remove_segment(SegmentId(1)));
assert_eq!(idx.len(), 1);
assert!(!idx.remove_segment(SegmentId(99)));
}
#[test]
fn hundred_segments_pruning() {
let idx = TimeIndex::new();
for i in 0u64..100 {
#[allow(clippy::cast_possible_wrap)]
let start = (i as i64) * 1000;
idx.add_segment(entry(i, start, start + 500));
}
assert_eq!(idx.len(), 100);
let result = idx.segments_for_range(5000, 7500);
assert_eq!(result, vec![SegmentId(5), SegmentId(6), SegmentId(7)]);
let result = idx.segments_for_range(50250, 50250);
assert_eq!(result, vec![SegmentId(50)]);
}
#[test]
fn entry_overlaps() {
let e = entry(1, 100, 200);
assert!(e.overlaps(100, 200)); assert!(e.overlaps(50, 150)); assert!(e.overlaps(150, 250)); assert!(e.overlaps(120, 180)); assert!(e.overlaps(50, 250)); assert!(!e.overlaps(0, 99)); assert!(!e.overlaps(201, 300)); }
}