use crate::idmap::IdMap;
use crate::interner::Interner;
use crate::types::{Value, ValueKey};
use crate::v8::seam::ColumnsView;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Default)]
pub struct PropertyIndex {
enabled: BTreeSet<(String, String)>,
forward: BTreeMap<(String, String), BTreeMap<ValueKey, BTreeSet<u32>>>,
reverse: BTreeMap<(String, String, u32), ValueKey>,
}
impl PropertyIndex {
pub fn new() -> Self {
Self::default()
}
pub fn is_enabled(&self, label: &str, field: &str) -> bool {
self.enabled
.contains(&(label.to_string(), field.to_string()))
}
pub fn field_indexed(&self, field: &str) -> bool {
self.enabled.iter().any(|(_, f)| f == field)
}
pub fn has_label(&self, label: &str) -> bool {
self.enabled.iter().any(|(l, _)| l == label)
}
pub fn enabled_pairs(&self) -> impl Iterator<Item = &(String, String)> {
self.enabled.iter()
}
pub fn enable(&mut self, label: &str, field: &str) -> bool {
self.enabled.insert((label.to_string(), field.to_string()))
}
pub fn disable(&mut self, label: &str, field: &str) -> bool {
let key = (label.to_string(), field.to_string());
let was = self.enabled.remove(&key);
self.forward.remove(&key);
self.reverse
.retain(|(l, f, _), _| !(l == label && f == field));
was
}
pub fn set(&mut self, label: &str, field: &str, id: u32, value: &Value) {
if !self.is_enabled(label, field) {
return;
}
self.remove_node(label, field, id);
if let Some(vk) = ValueKey::from_value(value) {
self.forward
.entry((label.to_string(), field.to_string()))
.or_default()
.entry(vk.clone())
.or_default()
.insert(id);
self.reverse
.insert((label.to_string(), field.to_string(), id), vk);
}
}
pub fn remove_node_all(&mut self, id: u32) {
let pairs: Vec<(String, String)> = self
.reverse
.keys()
.filter(|(_, _, i)| *i == id)
.map(|(l, f, _)| (l.clone(), f.clone()))
.collect();
for (l, f) in pairs {
self.remove_node(&l, &f, id);
}
}
pub fn remove_node(&mut self, label: &str, field: &str, id: u32) {
let rkey = (label.to_string(), field.to_string(), id);
if let Some(old) = self.reverse.remove(&rkey) {
let fkey = (label.to_string(), field.to_string());
if let Some(by_value) = self.forward.get_mut(&fkey) {
if let Some(ids) = by_value.get_mut(&old) {
ids.remove(&id);
if ids.is_empty() {
by_value.remove(&old);
}
}
}
}
}
pub fn lookup(&self, label: &str, field: &str, value: &Value) -> Vec<u32> {
let Some(vk) = ValueKey::from_value(value) else {
return Vec::new();
};
self.forward
.get(&(label.to_string(), field.to_string()))
.and_then(|by_value| by_value.get(&vk))
.map(|ids| ids.iter().copied().collect())
.unwrap_or_default()
}
pub fn rebuild_all(
&mut self,
ids: &IdMap,
labels: &[u32],
syms: &Interner,
props: ColumnsView<'_>,
) {
if self.enabled.is_empty() {
return;
}
let enabled_vec: Vec<(String, String)> = self.enabled.iter().cloned().collect();
for pair in &enabled_vec {
self.forward.remove(pair);
}
self.reverse
.retain(|(l, f, _), _| !enabled_vec.iter().any(|(el, ef)| el == l && ef == f));
let n = ids.len() as u32;
for id in 0..n {
let Some(&sym) = labels.get(id as usize) else {
continue;
};
if sym == u32::MAX {
continue;
}
let Some(label) = syms.resolve(sym) else {
continue;
};
for (lbl, field) in &enabled_vec {
if lbl == label {
if let Some(vr) = props.get(id, field) {
let value = vr.into_value();
self.set(lbl, field, id, &value);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(x: &str) -> Value {
Value::Str(x.into())
}
#[test]
fn lookup_returns_only_matching_ids_ascending() {
let mut ix = PropertyIndex::new();
ix.enable("Person", "city");
ix.set("Person", "city", 3, &s("austin"));
ix.set("Person", "city", 1, &s("austin"));
ix.set("Person", "city", 2, &s("boston"));
assert_eq!(ix.lookup("Person", "city", &s("austin")), vec![1, 3]);
assert_eq!(ix.lookup("Person", "city", &s("boston")), vec![2]);
assert_eq!(
ix.lookup("Person", "city", &s("nowhere")),
Vec::<u32>::new()
);
}
#[test]
fn undeclared_pair_indexes_nothing() {
let mut ix = PropertyIndex::new();
ix.set("Person", "city", 1, &s("austin"));
assert!(!ix.is_enabled("Person", "city"));
assert_eq!(ix.lookup("Person", "city", &s("austin")), Vec::<u32>::new());
}
#[test]
fn update_moves_id_to_new_value_bucket() {
let mut ix = PropertyIndex::new();
ix.enable("Person", "city");
ix.set("Person", "city", 1, &s("austin"));
ix.set("Person", "city", 1, &s("boston"));
assert_eq!(ix.lookup("Person", "city", &s("austin")), Vec::<u32>::new());
assert_eq!(ix.lookup("Person", "city", &s("boston")), vec![1]);
}
#[test]
fn remove_node_drops_entry() {
let mut ix = PropertyIndex::new();
ix.enable("Person", "city");
ix.set("Person", "city", 1, &s("austin"));
ix.set("Person", "city", 2, &s("austin"));
ix.remove_node("Person", "city", 1);
assert_eq!(ix.lookup("Person", "city", &s("austin")), vec![2]);
}
#[test]
fn non_scalar_values_are_skipped() {
let mut ix = PropertyIndex::new();
ix.enable("Post", "tags");
ix.set("Post", "tags", 1, &Value::List(vec![s("a"), s("b")]));
assert_eq!(
ix.lookup("Post", "tags", &Value::List(vec![s("a")])),
Vec::<u32>::new()
);
}
#[test]
fn int_and_bool_keys_index() {
let mut ix = PropertyIndex::new();
ix.enable("N", "age");
ix.enable("N", "active");
ix.set("N", "age", 1, &Value::Int(30));
ix.set("N", "age", 2, &Value::Int(30));
ix.set("N", "active", 1, &Value::Bool(true));
assert_eq!(ix.lookup("N", "age", &Value::Int(30)), vec![1, 2]);
assert_eq!(ix.lookup("N", "active", &Value::Bool(true)), vec![1]);
}
#[test]
fn remove_node_all_drops_every_field_for_id() {
let mut ix = PropertyIndex::new();
ix.enable("Person", "city");
ix.enable("Person", "team");
ix.set("Person", "city", 1, &s("austin"));
ix.set("Person", "team", 1, &s("blue"));
ix.set("Person", "city", 2, &s("austin"));
ix.remove_node_all(1);
assert_eq!(ix.lookup("Person", "city", &s("austin")), vec![2]);
assert_eq!(ix.lookup("Person", "team", &s("blue")), Vec::<u32>::new());
}
#[test]
fn disable_clears_entries_and_declaration() {
let mut ix = PropertyIndex::new();
ix.enable("Person", "city");
ix.set("Person", "city", 1, &s("austin"));
assert!(ix.disable("Person", "city"));
assert!(!ix.is_enabled("Person", "city"));
assert_eq!(ix.lookup("Person", "city", &s("austin")), Vec::<u32>::new());
}
}