use std::collections::{BTreeSet, HashMap};
use std::future::Future;
use nodedb_types::Namespace;
use nodedb_types::value::Value;
use crate::error::LiteError;
use crate::storage::engine::{StorageEngine, WriteOp};
use super::catalog::IndexEngine;
use super::maintain::{IndexWriteOp, plan_index_ops, unique_candidates};
use super::store::IndexCatalog;
pub(crate) struct RowImage {
pub collection: String,
pub doc_id: String,
pub row: Option<Value>,
}
type RowEntryOps = (String, String, Vec<IndexWriteOp>);
type CollectionRowImages<'a> = (&'a str, Vec<(&'a str, Option<Value>)>);
impl IndexCatalog {
async fn plan_checked<R, F>(
&self,
engine: IndexEngine,
images: &[RowImage],
read_row: &R,
) -> Result<Vec<RowEntryOps>, LiteError>
where
R: Fn(String, String) -> F,
F: Future<Output = Result<Option<Value>, LiteError>>,
{
let (changes, candidates) = {
let state = self.lock();
let mut collection_slots: HashMap<&str, usize> = HashMap::new();
let mut collections: Vec<CollectionRowImages<'_>> = Vec::new();
for image in images {
let next_slot = collections.len();
let slot = *collection_slots
.entry(image.collection.as_str())
.or_insert_with(|| {
collections.push((&image.collection, Vec::new()));
next_slot
});
collections[slot]
.1
.push((image.doc_id.as_str(), image.row.clone()));
}
let mut candidates = Vec::new();
for (collection, rows) in &collections {
candidates.extend(unique_candidates(&state, collection, engine, rows)?);
}
let mut held: HashMap<(String, String), BTreeSet<Vec<u8>>> = HashMap::new();
let mut changes: Vec<RowEntryOps> = Vec::with_capacity(images.len());
for image in images {
let defs = state.defs_on(&image.collection, engine);
if defs.is_empty() {
continue;
}
let posting_key = (image.collection.clone(), image.doc_id.clone());
let old = held
.get(&posting_key)
.or_else(|| state.postings.get(&posting_key))
.cloned()
.unwrap_or_default();
let planned = plan_index_ops(&defs, &image.doc_id, &old, image.row.as_ref());
let mut now = old;
for op in &planned {
match op {
IndexWriteOp::Put(k) => now.insert(k.clone()),
IndexWriteOp::Delete(k) => now.remove(k),
};
}
held.insert(posting_key, now);
changes.push((image.collection.clone(), image.doc_id.clone(), planned));
}
(changes, candidates)
};
for candidate in &candidates {
let stored =
read_row(candidate.def.collection.clone(), candidate.other.clone()).await?;
if stored.is_some_and(|row| candidate.held_by(&row)) {
return Err(candidate.violation());
}
}
Ok(changes)
}
pub(crate) async fn check_rows<R, F>(
&self,
engine: IndexEngine,
images: &[RowImage],
read_row: R,
) -> Result<(), LiteError>
where
R: Fn(String, String) -> F,
F: Future<Output = Result<Option<Value>, LiteError>>,
{
let _ddl = self.durable_ddl.read().await;
if !images
.iter()
.any(|image| self.has_defs(&image.collection, engine))
{
return Ok(());
}
let _rows = self.durable_rows.lock().await;
self.plan_checked(engine, images, &read_row)
.await
.map(|_| ())
}
pub(crate) async fn commit_rows<S, R, F>(
&self,
storage: &S,
engine: IndexEngine,
ops: Vec<WriteOp>,
images: Vec<RowImage>,
read_row: R,
) -> Result<(), LiteError>
where
S: StorageEngine,
R: Fn(String, String) -> F,
F: Future<Output = Result<Option<Value>, LiteError>>,
{
self.commit_rows_with(storage, engine, ops, images, read_row, |ops| async move {
Ok(ops)
})
.await
}
pub(crate) async fn commit_rows_with<S, R, F, X, XF>(
&self,
storage: &S,
engine: IndexEngine,
ops: Vec<WriteOp>,
images: Vec<RowImage>,
read_row: R,
extra: X,
) -> Result<(), LiteError>
where
S: StorageEngine,
R: Fn(String, String) -> F,
F: Future<Output = Result<Option<Value>, LiteError>>,
X: FnOnce(Vec<WriteOp>) -> XF,
XF: Future<Output = Result<Vec<WriteOp>, LiteError>>,
{
let _ddl = self.durable_ddl.read().await;
self.commit_rows_held(storage, engine, ops, images, read_row, extra)
.await
}
pub(crate) async fn commit_rows_held<S, R, F, X, XF>(
&self,
storage: &S,
engine: IndexEngine,
mut ops: Vec<WriteOp>,
images: Vec<RowImage>,
read_row: R,
extra: X,
) -> Result<(), LiteError>
where
S: StorageEngine,
R: Fn(String, String) -> F,
F: Future<Output = Result<Option<Value>, LiteError>>,
X: FnOnce(Vec<WriteOp>) -> XF,
XF: Future<Output = Result<Vec<WriteOp>, LiteError>>,
{
let indexed = images
.iter()
.any(|image| self.has_defs(&image.collection, engine));
let sorted_touched = images
.iter()
.any(|image| self.has_sorted(&image.collection));
if !indexed && !sorted_touched {
return if ops.is_empty() {
Ok(())
} else {
storage.batch_write(&ops).await
};
}
let _rows = self.durable_rows.lock().await;
let changes = if indexed {
self.plan_checked(engine, &images, &read_row).await?
} else {
Vec::new()
};
for (_, _, entry_ops) in &changes {
for op in entry_ops {
ops.push(match op {
IndexWriteOp::Put(k) => WriteOp::Put {
ns: Namespace::Meta,
key: k.clone(),
value: Vec::new(),
},
IndexWriteOp::Delete(k) => WriteOp::Delete {
ns: Namespace::Meta,
key: k.clone(),
},
});
}
}
let ops = extra(ops).await?;
if !ops.is_empty() {
storage.batch_write(&ops).await?;
}
let mut state = self.lock();
let ((), _stored) = state.collecting(true, |state| {
for (collection, doc_id, entry_ops) in changes {
state.apply(&collection, &doc_id, entry_ops);
}
});
Ok(())
}
}