use std::collections::BTreeSet;
use std::sync::Arc;
use nodedb_types::value::Value;
use crate::error::LiteError;
use crate::storage::engine::WriteOp;
use super::catalog::{IndexDef, IndexEngine};
use super::document::{entry_keys, index_values};
use super::key;
use super::store::{IndexCatalog, IndexState};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum IndexWriteOp {
Put(Vec<u8>),
Delete(Vec<u8>),
}
pub(crate) fn plan_index_ops(
defs: &[Arc<IndexDef>],
doc_id: &str,
old_keys: &BTreeSet<Vec<u8>>,
new: Option<&Value>,
) -> Vec<IndexWriteOp> {
let new_keys: BTreeSet<Vec<u8>> = match new {
Some(row) => defs
.iter()
.flat_map(|def| entry_keys(def, doc_id, row))
.collect(),
None => BTreeSet::new(),
};
old_keys
.difference(&new_keys)
.map(|k| IndexWriteOp::Delete(k.clone()))
.chain(
new_keys
.difference(old_keys)
.map(|k| IndexWriteOp::Put(k.clone())),
)
.collect()
}
pub(super) fn ids_under<'a>(
state: &'a IndexState,
index_prefix: &'a [u8],
prefix: Vec<u8>,
) -> impl Iterator<Item = &'a str> + 'a {
let scan = prefix.clone();
state
.entries
.range(scan..)
.take_while(move |k| k.starts_with(&prefix))
.filter_map(move |k| key::entry_doc_id(k, index_prefix))
}
pub(crate) struct UniqueCandidate {
pub def: Arc<IndexDef>,
pub value: Value,
pub other: String,
}
impl UniqueCandidate {
pub(crate) fn held_by(&self, stored: &Value) -> bool {
index_values(&self.def, stored)
.iter()
.any(|held| held.eq_coerced(&self.value))
}
pub(crate) fn violation(&self) -> LiteError {
unique_violation(&self.def, &self.value, &self.other)
}
}
pub(super) fn unique_candidates(
state: &IndexState,
collection: &str,
engine: IndexEngine,
rows: &[(&str, Option<Value>)],
) -> Result<Vec<UniqueCandidate>, LiteError> {
let defs: Vec<Arc<IndexDef>> = state
.defs_on(collection, engine)
.into_iter()
.filter(|d| d.unique)
.collect();
let written: BTreeSet<&str> = rows.iter().map(|(id, _)| *id).collect();
let mut candidates = Vec::new();
for def in &defs {
let prefix = def.entry_prefix();
let mut claimed: Vec<(&str, Value)> = Vec::new();
for (id, row) in rows {
claimed.retain(|(other, _)| other != id);
let Some(row) = row else {
continue;
};
for value in index_values(def, row) {
if let Some((other, _)) = claimed
.iter()
.find(|(owner, held)| owner != id && held.eq_coerced(&value))
{
return Err(unique_violation(def, &value, other));
}
for encoded in key::encode_coercible(&value) {
let mut probe = prefix.clone();
probe.extend_from_slice(&encoded);
for other in ids_under(state, &prefix, probe) {
let gone = state
.tombstoned
.contains(&(collection.to_string(), other.to_string()));
if written.contains(other) || gone {
continue;
}
candidates.push(UniqueCandidate {
def: Arc::clone(def),
value: value.clone(),
other: other.to_string(),
});
}
}
claimed.push((id, value));
}
}
}
Ok(candidates)
}
fn unique_violation(def: &IndexDef, value: &Value, other: &str) -> LiteError {
LiteError::UniqueViolation {
collection: def.collection.clone(),
detail: format!(
"index '{}': key ({})=({}) already exists in document '{other}'",
def.name,
def.field_spec(),
display_value(value)
),
}
}
fn display_value(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => format!("{other:?}"),
}
}
impl IndexCatalog {
pub(crate) fn resync<'a>(
&self,
collection: &str,
ids: impl IntoIterator<Item = &'a str>,
read_row: impl Fn(&str) -> Option<Value>,
) {
let mut state = self.lock();
let defs = state.defs_on(collection, IndexEngine::Document);
if defs.is_empty() {
return;
}
let empty = BTreeSet::new();
for id in ids {
let posting_key = (collection.to_string(), id.to_string());
let row = if state.tombstoned.contains(&posting_key) {
None
} else {
read_row(id)
};
let old = state.postings.get(&posting_key).unwrap_or(&empty);
let ops = plan_index_ops(&defs, id, old, row.as_ref());
state.apply(collection, id, ops);
}
}
pub(crate) fn tombstone<'a>(&self, collection: &str, ids: impl IntoIterator<Item = &'a str>) {
let ids: Vec<&str> = ids.into_iter().collect();
{
let mut state = self.lock();
for id in &ids {
state
.tombstoned
.insert((collection.to_string(), (*id).to_string()));
}
}
self.resync(collection, ids, |_| None);
}
pub(crate) fn revive(&self, collection: &str, id: &str) {
self.lock()
.tombstoned
.remove(&(collection.to_string(), id.to_string()));
}
pub(crate) fn check_unique(
&self,
collection: &str,
rows: &[(&str, Option<Value>)],
read_row: impl Fn(&str) -> Option<Value>,
) -> Result<(), LiteError> {
let candidates = unique_candidates(&self.lock(), collection, IndexEngine::Document, rows)?;
for c in candidates {
if read_row(&c.other).is_some_and(|stored| c.held_by(&stored)) {
return Err(c.violation());
}
}
Ok(())
}
pub(crate) fn install(
&self,
def: Arc<IndexDef>,
rows: impl IntoIterator<Item = (String, Value)>,
) -> Result<(u64, Vec<WriteOp>), LiteError> {
let rows: Vec<(String, Value)> = rows.into_iter().collect();
if def.unique {
let mut seen: Vec<(&str, Value)> = Vec::new();
for (id, row) in &rows {
for value in index_values(&def, row) {
if let Some((other, _)) = seen
.iter()
.find(|(owner, v)| owner != id && v.eq_coerced(&value))
{
return Err(unique_violation(&def, &value, other));
}
seen.push((id, value));
}
}
}
let mut state = self.lock();
let durable = def.engine != IndexEngine::Document;
let (built, ops) = state.collecting(durable, |state| -> Result<u64, LiteError> {
if let Some(existing) = state.defs.get(&def.name).cloned() {
state.clear_prefix(&existing.entry_prefix());
}
state.clear_prefix(&def.entry_prefix());
state.put_def(Arc::clone(&def))?;
let mut count = 0u64;
for (id, row) in &rows {
let ops: Vec<IndexWriteOp> = entry_keys(&def, id, row)
.into_iter()
.map(IndexWriteOp::Put)
.collect();
count += ops.len() as u64;
state.apply(&def.collection, id, ops);
}
Ok(count)
});
Ok((built?, ops))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::index::catalog::canonical_field;
fn def(field: &str, unique: bool) -> Arc<IndexDef> {
let (path, is_array) = canonical_field(field);
Arc::new(IndexDef {
name: format!("idx_{field}"),
collection: "c".into(),
path,
unique,
case_insensitive: false,
is_array,
predicate: None,
engine: IndexEngine::Document,
})
}
fn row(field: &str, v: Value) -> Value {
Value::Object(HashMap::from([(field.to_string(), v)]))
}
#[test]
fn an_update_replaces_the_old_entry() {
let defs = vec![def("n", false)];
let old_row = row("n", Value::Integer(1));
let old: BTreeSet<Vec<u8>> = entry_keys(&defs[0], "d", &old_row);
let ops = plan_index_ops(&defs, "d", &old, Some(&row("n", Value::Integer(2))));
assert_eq!(ops.len(), 2);
assert!(matches!(ops[0], IndexWriteOp::Delete(_)));
assert!(matches!(ops[1], IndexWriteOp::Put(_)));
assert!(plan_index_ops(&defs, "d", &old, Some(&old_row)).is_empty());
}
#[test]
fn a_unique_index_refuses_a_second_holder() {
let catalog = IndexCatalog::new();
let d = def("email", true);
let stored = row("email", Value::String("a@x".into()));
catalog
.install(Arc::clone(&d), [("d1".to_string(), stored.clone())])
.expect("install");
let read = |id: &str| (id == "d1").then(|| stored.clone());
let dup = row("email", Value::String("a@x".into()));
let err = catalog
.check_unique("c", &[("d2", Some(dup.clone()))], read)
.expect_err("duplicate");
assert!(matches!(err, LiteError::UniqueViolation { .. }), "{err}");
catalog
.check_unique("c", &[("d1", Some(dup))], read)
.expect("same document");
}
#[test]
fn a_document_may_repeat_an_element_of_a_unique_array() {
let catalog = IndexCatalog::new();
let tags = |t: &[&str]| {
row(
"tags",
Value::Array(t.iter().map(|s| Value::String((*s).into())).collect()),
)
};
let stored = tags(&["a", "a"]);
catalog
.install(def("tags[]", true), [("d1".to_string(), stored.clone())])
.expect("one document repeating an element");
let read = |id: &str| (id == "d1").then(|| stored.clone());
catalog
.check_unique("c", &[("d2", Some(tags(&["b", "b"])))], read)
.expect("repeats within the written document");
assert!(
catalog
.check_unique("c", &[("d2", Some(tags(&["a"])))], read)
.is_err(),
"another document still may not share the element"
);
}
#[test]
fn install_refuses_rows_that_already_collide() {
let catalog = IndexCatalog::new();
let rows = [
("d1".to_string(), row("n", Value::Integer(5))),
("d2".to_string(), row("n", Value::String("5".into()))),
];
assert!(catalog.install(def("n", true), rows).is_err());
assert!(catalog.defs().is_empty());
}
}