use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::sync::{Arc, Mutex, MutexGuard};
use nodedb_types::Namespace;
use crate::error::LiteError;
use crate::nodedb::LockExt;
use crate::storage::engine::{StorageEngine, WriteOp};
use super::catalog::{IndexDef, IndexEngine};
use super::key;
use super::maintain::IndexWriteOp;
use super::sorted_def::SortedIndexDef;
struct Dirty {
seq: u64,
value: Option<Vec<u8>>,
}
#[derive(Default)]
pub(crate) struct IndexState {
pub(super) defs: BTreeMap<String, Arc<IndexDef>>,
pub(super) sorted: BTreeMap<String, Arc<SortedIndexDef>>,
pub(super) entries: BTreeSet<Vec<u8>>,
pub(super) postings: HashMap<(String, String), BTreeSet<Vec<u8>>>,
pub(super) tombstoned: HashSet<(String, String)>,
dirty: HashMap<Vec<u8>, Dirty>,
next_seq: u64,
sink: Option<Vec<WriteOp>>,
}
impl IndexState {
pub(super) fn defs_on(&self, collection: &str, engine: IndexEngine) -> Vec<Arc<IndexDef>> {
self.defs
.values()
.filter(|d| d.collection == collection && d.engine == engine)
.cloned()
.collect()
}
fn mark(&mut self, key: Vec<u8>, value: Option<Vec<u8>>) {
if let Some(sink) = &mut self.sink {
sink.push(match value {
Some(value) => WriteOp::Put {
ns: Namespace::Meta,
key,
value,
},
None => WriteOp::Delete {
ns: Namespace::Meta,
key,
},
});
return;
}
self.next_seq += 1;
let seq = self.next_seq;
self.dirty.insert(key, Dirty { seq, value });
}
pub(super) fn scratch(&self, defs: &[Arc<IndexDef>]) -> IndexState {
let mut scratch = IndexState::default();
for def in defs {
scratch.defs.insert(def.name.clone(), Arc::clone(def));
let prefix = def.entry_prefix();
for entry in self
.entries
.range(prefix.clone()..)
.take_while(|k| k.starts_with(&prefix))
{
if let Some(parsed) = key::parse_entry(entry) {
scratch
.postings
.entry((parsed.collection.to_string(), parsed.doc_id.to_string()))
.or_default()
.insert(entry.clone());
}
scratch.entries.insert(entry.clone());
}
}
scratch
}
pub(super) fn collecting<R>(
&mut self,
durable: bool,
f: impl FnOnce(&mut Self) -> R,
) -> (R, Vec<WriteOp>) {
if !durable {
return (f(self), Vec::new());
}
let outer = self.sink.replace(Vec::new());
let result = f(self);
let ops = self.sink.take().unwrap_or_default();
self.sink = outer;
(result, ops)
}
pub(super) fn apply(&mut self, collection: &str, doc_id: &str, ops: Vec<IndexWriteOp>) {
if ops.is_empty() {
return;
}
let posting_key = (collection.to_string(), doc_id.to_string());
for op in ops {
match op {
IndexWriteOp::Put(entry) => {
self.postings
.entry(posting_key.clone())
.or_default()
.insert(entry.clone());
self.entries.insert(entry.clone());
self.mark(entry, Some(Vec::new()));
}
IndexWriteOp::Delete(entry) => {
if let Some(keys) = self.postings.get_mut(&posting_key) {
keys.remove(&entry);
if keys.is_empty() {
self.postings.remove(&posting_key);
}
}
self.entries.remove(&entry);
self.mark(entry, None);
}
}
}
}
pub(super) fn clear_prefix(&mut self, prefix: &[u8]) {
let doomed: Vec<Vec<u8>> = self
.entries
.range(prefix.to_vec()..)
.take_while(|k| k.starts_with(prefix))
.cloned()
.collect();
for entry in doomed {
if let Some(parsed) = key::parse_entry(&entry) {
let posting_key = (parsed.collection.to_string(), parsed.doc_id.to_string());
if let Some(keys) = self.postings.get_mut(&posting_key) {
keys.remove(&entry);
if keys.is_empty() {
self.postings.remove(&posting_key);
}
}
}
self.entries.remove(&entry);
self.mark(entry, None);
}
}
pub(super) fn put_def(&mut self, def: Arc<IndexDef>) -> Result<(), LiteError> {
let bytes = def.encode()?;
self.mark(key::def_key(&def.collection, &def.name), Some(bytes));
self.defs.insert(def.name.clone(), def);
Ok(())
}
pub(super) fn remove_def(&mut self, name: &str) -> Option<Arc<IndexDef>> {
let def = self.defs.remove(name)?;
self.clear_prefix(&def.entry_prefix());
self.mark(key::def_key(&def.collection, &def.name), None);
Some(def)
}
}
pub(crate) struct IndexFlush {
written: Vec<(Vec<u8>, u64)>,
}
pub(crate) struct LoadOutcome {
pub rebuild_all: bool,
}
#[derive(Default)]
pub struct IndexCatalog {
state: Mutex<IndexState>,
pub(crate) durable_ddl: tokio::sync::RwLock<()>,
pub(crate) durable_rows: tokio::sync::Mutex<()>,
pub(crate) bitemporal_build: tokio::sync::RwLock<()>,
}
impl IndexCatalog {
pub fn new() -> Self {
Self::default()
}
pub(super) fn lock(&self) -> MutexGuard<'_, IndexState> {
self.state.lock_or_recover()
}
pub(crate) async fn load<S: StorageEngine>(
&self,
storage: &S,
) -> Result<LoadOutcome, LiteError> {
let format = storage.get(Namespace::Meta, &key::format_key()).await?;
let defs = storage
.scan_prefix(Namespace::Meta, &key::defs_prefix())
.await?;
let entries = storage
.scan_prefix(Namespace::Meta, &key::entries_prefix())
.await?;
let sorted = storage
.scan_prefix(Namespace::Meta, &key::sorted_defs_prefix())
.await?
.iter()
.map(|(_, bytes)| SortedIndexDef::decode(bytes))
.collect::<Result<Vec<_>, _>>()?;
let current = format.as_deref() == Some(key::FORMAT_VERSION.to_be_bytes().as_slice());
let stored_nothing = defs.is_empty() && entries.is_empty();
let decoded = defs
.iter()
.map(|(_, bytes)| IndexDef::decode(bytes))
.collect::<Result<Vec<_>, _>>()?;
let (rebuild_all, fresh) = {
let mut state = self.lock();
for def in decoded {
state.defs.insert(def.name.clone(), Arc::new(def));
}
for def in sorted {
state.sorted.insert(def.name.clone(), Arc::new(def));
}
for (entry, _) in entries {
if current && let Some(parsed) = key::parse_entry(&entry) {
state
.postings
.entry((parsed.collection.to_string(), parsed.doc_id.to_string()))
.or_default()
.insert(entry.clone());
state.entries.insert(entry);
} else {
state.mark(entry, None);
}
}
let rebuild_all = !current && !state.defs.is_empty();
let fresh = !current && stored_nothing;
if !current && !fresh {
state.mark(
key::format_key(),
Some(key::FORMAT_VERSION.to_be_bytes().to_vec()),
);
}
(rebuild_all, fresh)
};
if fresh {
storage
.put(
Namespace::Meta,
&key::format_key(),
&key::FORMAT_VERSION.to_be_bytes(),
)
.await?;
}
Ok(LoadOutcome { rebuild_all })
}
pub(crate) fn stage_flush(&self, ops: &mut Vec<WriteOp>) -> IndexFlush {
let state = self.lock();
let mut written = Vec::with_capacity(state.dirty.len());
for (k, dirty) in &state.dirty {
ops.push(match &dirty.value {
Some(value) => WriteOp::Put {
ns: Namespace::Meta,
key: k.clone(),
value: value.clone(),
},
None => WriteOp::Delete {
ns: Namespace::Meta,
key: k.clone(),
},
});
written.push((k.clone(), dirty.seq));
}
IndexFlush { written }
}
pub(crate) fn mark_flushed(&self, flush: IndexFlush) {
let mut state = self.lock();
for (k, seq) in flush.written {
if state.dirty.get(&k).is_some_and(|d| d.seq == seq) {
state.dirty.remove(&k);
}
}
}
pub(crate) fn is_indexed(&self, collection: &str) -> bool {
self.has_defs(collection, IndexEngine::Document)
}
pub(crate) fn has_defs(&self, collection: &str, engine: IndexEngine) -> bool {
self.lock()
.defs
.values()
.any(|d| d.collection == collection && d.engine == engine)
}
pub(crate) fn defs(&self) -> Vec<Arc<IndexDef>> {
self.lock().defs.values().cloned().collect()
}
pub(crate) fn def_named(&self, name: &str) -> Option<Arc<IndexDef>> {
self.lock().defs.get(name).cloned()
}
pub(crate) fn def_on_field(&self, collection: &str, field_spec: &str) -> Option<Arc<IndexDef>> {
self.lock()
.defs
.values()
.find(|d| d.collection == collection && d.field_spec() == field_spec)
.cloned()
}
pub(crate) fn sorted_on(&self, collection: &str) -> Vec<Arc<SortedIndexDef>> {
self.lock()
.sorted
.values()
.filter(|d| d.collection == collection)
.cloned()
.collect()
}
pub(crate) fn sorted_named(&self, name: &str) -> Option<Arc<SortedIndexDef>> {
self.lock().sorted.get(name).cloned()
}
pub(crate) fn has_sorted(&self, collection: &str) -> bool {
self.lock()
.sorted
.values()
.any(|d| d.collection == collection)
}
pub(crate) fn sorted_by_collection(&self) -> HashMap<String, Vec<Arc<SortedIndexDef>>> {
let mut grouped: HashMap<String, Vec<Arc<SortedIndexDef>>> = HashMap::new();
for def in self.lock().sorted.values() {
grouped
.entry(def.collection.clone())
.or_default()
.push(Arc::clone(def));
}
grouped
}
pub(crate) fn planner_specs(&self, collection: &str) -> Vec<nodedb_sql::types::IndexSpec> {
self.lock()
.defs
.values()
.filter(|d| {
d.collection == collection
&& matches!(d.engine, IndexEngine::Document | IndexEngine::Strict)
})
.map(|d| d.planner_spec())
.collect()
}
}