use std::collections::{BTreeMap, BTreeSet};
pub type KvIndexTree = BTreeMap<Vec<u8>, BTreeSet<Vec<u8>>>;
#[derive(Debug)]
pub struct KvFieldIndex {
field: String,
field_position: usize,
tree: KvIndexTree,
}
impl KvFieldIndex {
pub fn new(field: impl Into<String>, field_position: usize) -> Self {
Self {
field: field.into(),
field_position,
tree: BTreeMap::new(),
}
}
pub fn field(&self) -> &str {
&self.field
}
pub fn field_position(&self) -> usize {
self.field_position
}
pub fn entries(&self) -> &KvIndexTree {
&self.tree
}
pub fn insert(&mut self, field_value: Vec<u8>, primary_key: Vec<u8>) {
self.tree
.entry(field_value)
.or_default()
.insert(primary_key);
}
pub fn remove(&mut self, field_value: &[u8], primary_key: &[u8]) -> bool {
if let Some(keys) = self.tree.get_mut(field_value) {
let removed = keys.remove(primary_key);
if keys.is_empty() {
self.tree.remove(field_value);
}
removed
} else {
false
}
}
pub fn lookup_eq(&self, field_value: &[u8]) -> Vec<&[u8]> {
self.tree
.get(field_value)
.map(|keys| keys.iter().map(|k| k.as_slice()).collect())
.unwrap_or_default()
}
pub fn lookup_range(&self, lower: Option<&[u8]>, upper: Option<&[u8]>) -> Vec<(&[u8], &[u8])> {
use std::ops::Bound;
let lo = match lower {
Some(l) => Bound::Included(l.to_vec()),
None => Bound::Unbounded,
};
let hi = match upper {
Some(u) => Bound::Excluded(u.to_vec()),
None => Bound::Unbounded,
};
let mut results = Vec::new();
for (value, keys) in self.tree.range((lo, hi)) {
for key in keys {
results.push((value.as_slice(), key.as_slice()));
}
}
results
}
pub fn entry_count(&self) -> usize {
self.tree.values().map(|s| s.len()).sum()
}
pub fn distinct_values(&self) -> usize {
self.tree.len()
}
pub fn clear(&mut self) {
self.tree.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn field_index_insert_and_lookup() {
let mut idx = KvFieldIndex::new("region", 2);
idx.insert(b"us-east".to_vec(), b"key1".to_vec());
idx.insert(b"us-east".to_vec(), b"key2".to_vec());
idx.insert(b"eu-west".to_vec(), b"key3".to_vec());
let results = idx.lookup_eq(b"us-east");
assert_eq!(results.len(), 2);
assert!(results.contains(&b"key1".as_slice()));
assert!(results.contains(&b"key2".as_slice()));
let results = idx.lookup_eq(b"eu-west");
assert_eq!(results.len(), 1);
let results = idx.lookup_eq(b"ap-south");
assert!(results.is_empty());
}
#[test]
fn field_index_remove() {
let mut idx = KvFieldIndex::new("status", 1);
idx.insert(b"active".to_vec(), b"k1".to_vec());
idx.insert(b"active".to_vec(), b"k2".to_vec());
assert!(idx.remove(b"active", b"k1"));
assert_eq!(idx.lookup_eq(b"active").len(), 1);
assert!(idx.remove(b"active", b"k2"));
assert!(idx.lookup_eq(b"active").is_empty());
assert_eq!(idx.distinct_values(), 0);
assert!(!idx.remove(b"active", b"k3"));
}
#[test]
fn field_index_range_lookup() {
let mut idx = KvFieldIndex::new("score", 0);
for i in 0u32..10 {
idx.insert(i.to_be_bytes().to_vec(), format!("k{i}").into_bytes());
}
let results = idx.lookup_range(Some(&3u32.to_be_bytes()), Some(&7u32.to_be_bytes()));
assert_eq!(results.len(), 4); }
#[test]
fn entries_expose_every_indexed_pair() {
let mut idx = KvFieldIndex::new("region", 0);
idx.insert(b"us".to_vec(), b"k1".to_vec());
idx.insert(b"us".to_vec(), b"k2".to_vec());
idx.insert(b"eu".to_vec(), b"k3".to_vec());
let pairs: usize = idx.entries().values().map(|pks| pks.len()).sum();
assert_eq!(pairs, idx.entry_count());
assert_eq!(idx.entries().len(), 2, "two distinct values");
}
}