use std::collections::BTreeMap;
use super::field::KvIndexTree;
#[derive(Debug)]
pub struct KvCompositeIndex {
fields: Vec<String>,
field_positions: Vec<usize>,
tree: KvIndexTree,
}
impl KvCompositeIndex {
pub fn new(fields: Vec<String>, field_positions: Vec<usize>) -> Self {
Self {
fields,
field_positions,
tree: BTreeMap::new(),
}
}
pub fn fields(&self) -> &[String] {
&self.fields
}
pub fn field_positions(&self) -> &[usize] {
&self.field_positions
}
pub fn entries(&self) -> &KvIndexTree {
&self.tree
}
fn build_key(values: &[&[u8]]) -> Vec<u8> {
let mut key = Vec::new();
for (i, v) in values.iter().enumerate() {
if i > 0 {
key.push(0); }
key.extend_from_slice(v);
}
key
}
pub fn insert(&mut self, field_values: &[&[u8]], primary_key: Vec<u8>) {
let key = Self::build_key(field_values);
self.tree.entry(key).or_default().insert(primary_key);
}
pub fn insert_raw(&mut self, composite_key: Vec<u8>, primary_key: Vec<u8>) {
self.tree
.entry(composite_key)
.or_default()
.insert(primary_key);
}
pub fn remove(&mut self, field_values: &[&[u8]], primary_key: &[u8]) -> bool {
let key = Self::build_key(field_values);
if let Some(keys) = self.tree.get_mut(&key) {
let removed = keys.remove(primary_key);
if keys.is_empty() {
self.tree.remove(&key);
}
removed
} else {
false
}
}
pub fn lookup_eq(&self, field_values: &[&[u8]]) -> Vec<&[u8]> {
let key = Self::build_key(field_values);
self.tree
.get(&key)
.map(|keys| keys.iter().map(|k| k.as_slice()).collect())
.unwrap_or_default()
}
pub fn lookup_prefix(&self, prefix_values: &[&[u8]]) -> Vec<&[u8]> {
let prefix = Self::build_key(prefix_values);
let mut results = Vec::new();
for (composite_key, primary_keys) in self.tree.range(prefix.clone()..) {
if !composite_key.starts_with(&prefix) {
break;
}
for pk in primary_keys {
results.push(pk.as_slice());
}
}
results
}
pub fn entry_count(&self) -> usize {
self.tree.values().map(|s| s.len()).sum()
}
pub fn clear(&mut self) {
self.tree.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn composite_index_insert_and_exact_lookup() {
let mut ci = KvCompositeIndex::new(vec!["region".into(), "status".into()], vec![0, 1]);
ci.insert(&[b"us-east", b"active"], b"k1".to_vec());
ci.insert(&[b"us-east", b"inactive"], b"k2".to_vec());
ci.insert(&[b"eu-west", b"active"], b"k3".to_vec());
let results = ci.lookup_eq(&[b"us-east", b"active"]);
assert_eq!(results.len(), 1);
assert_eq!(results[0], b"k1");
let results = ci.lookup_eq(&[b"eu-west", b"active"]);
assert_eq!(results.len(), 1);
assert_eq!(results[0], b"k3");
assert!(ci.lookup_eq(&[b"ap-south", b"active"]).is_empty());
}
#[test]
fn composite_index_prefix_lookup() {
let mut ci = KvCompositeIndex::new(vec!["region".into(), "status".into()], vec![0, 1]);
ci.insert(&[b"us-east", b"active"], b"k1".to_vec());
ci.insert(&[b"us-east", b"inactive"], b"k2".to_vec());
ci.insert(&[b"eu-west", b"active"], b"k3".to_vec());
let results = ci.lookup_prefix(&[b"us-east"]);
assert_eq!(results.len(), 2);
}
#[test]
fn composite_index_remove() {
let mut ci = KvCompositeIndex::new(vec!["a".into(), "b".into()], vec![0, 1]);
ci.insert(&[b"x", b"y"], b"k1".to_vec());
assert_eq!(ci.entry_count(), 1);
assert!(ci.remove(&[b"x", b"y"], b"k1"));
assert_eq!(ci.entry_count(), 0);
}
#[test]
fn raw_entries_roundtrip_reproduces_lookups() {
let mut ci = KvCompositeIndex::new(vec!["a".into(), "b".into()], vec![0, 1]);
ci.insert(&[b"x", b"y"], b"k1".to_vec());
ci.insert(&[b"x\0z", b"y"], b"k2".to_vec());
let exported: Vec<(Vec<u8>, Vec<u8>)> = ci
.entries()
.iter()
.flat_map(|(key, pks)| pks.iter().map(|pk| (key.clone(), pk.clone())))
.collect();
let mut restored = KvCompositeIndex::new(vec!["a".into(), "b".into()], vec![0, 1]);
for (key, pk) in exported {
restored.insert_raw(key, pk);
}
assert_eq!(restored.lookup_eq(&[b"x", b"y"]), vec![b"k1".as_slice()]);
assert_eq!(restored.lookup_eq(&[b"x\0z", b"y"]), vec![b"k2".as_slice()]);
assert_eq!(restored.entry_count(), ci.entry_count());
}
}