use std::{borrow::Cow, sync::Arc};
use async_trait::async_trait;
use pliantdb_core::{
connection::Connection,
document::Document,
schema::{view, view::map, CollectionName, Key, Schema, ViewName},
};
use pliantdb_jobs::{Job, Keyed};
use sled::{
transaction::{ConflictableTransactionError, TransactionError, TransactionalTree},
IVec, Transactional, Tree,
};
use crate::{
database::{document_tree_name, Database},
views::{
view_document_map_tree_name, view_entries_tree_name, view_invalidated_docs_tree_name,
view_omitted_docs_tree_name, EntryMapping, Task, ViewEntry,
},
};
#[derive(Debug)]
pub struct Mapper<DB> {
pub storage: Database<DB>,
pub map: Map,
}
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub struct Map {
pub database: Arc<Cow<'static, str>>,
pub collection: CollectionName,
pub view_name: ViewName,
}
#[async_trait]
impl<DB> Job for Mapper<DB>
where
DB: Schema,
{
type Output = u64;
#[allow(clippy::clippy::too_many_lines)]
async fn execute(&mut self) -> anyhow::Result<Self::Output> {
let documents = self
.storage
.data
.storage
.sled()
.open_tree(document_tree_name(
&self.storage.data.name,
&self.map.collection,
))?;
let view_entries = self
.storage
.data
.storage
.sled()
.open_tree(view_entries_tree_name(
&self.storage.data.name,
&self.map.view_name,
))?;
let document_map =
self.storage
.data
.storage
.sled()
.open_tree(view_document_map_tree_name(
&self.storage.data.name,
&self.map.view_name,
))?;
let invalidated_entries =
self.storage
.data
.storage
.sled()
.open_tree(view_invalidated_docs_tree_name(
&self.storage.data.name,
&self.map.view_name,
))?;
let omitted_entries =
self.storage
.data
.storage
.sled()
.open_tree(view_omitted_docs_tree_name(
&self.storage.data.name,
&self.map.view_name,
))?;
let transaction_id = self
.storage
.last_transaction_id()
.await?
.expect("no way to have documents without a transaction");
let storage = self.storage.clone();
let map_request = self.map.clone();
tokio::task::spawn_blocking(move || {
map_view(
&invalidated_entries,
&document_map,
&documents,
&omitted_entries,
&view_entries,
&storage,
&map_request,
)
})
.await??;
self.storage
.data
.storage
.tasks()
.mark_view_updated(
self.map.database.clone(),
self.map.collection.clone(),
self.map.view_name.clone(),
transaction_id,
)
.await;
Ok(transaction_id)
}
}
fn map_view<DB: Schema>(
invalidated_entries: &Tree,
document_map: &Tree,
documents: &Tree,
omitted_entries: &Tree,
view_entries: &Tree,
storage: &Database<DB>,
map_request: &Map,
) -> anyhow::Result<()> {
let invalidated_ids = invalidated_entries
.iter()
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(|(key, _)| key)
.collect::<Vec<_>>();
if !invalidated_ids.is_empty() {
(
invalidated_entries,
document_map,
documents,
omitted_entries,
view_entries,
)
.transaction(
|(invalidated_entries, document_map, documents, omitted_entries, view_entries)| {
for document_id in &invalidated_ids {
DocumentRequest {
document_id,
map_request,
invalidated_entries,
document_map,
documents,
omitted_entries,
view_entries,
storage,
}
.map()?;
}
Ok(())
},
)
.map_err(|err| match err {
TransactionError::Abort(err) => err,
TransactionError::Storage(err) => anyhow::Error::from(err),
})?;
}
Ok(())
}
struct DocumentRequest<'a, DB> {
document_id: &'a IVec,
map_request: &'a Map,
invalidated_entries: &'a TransactionalTree,
document_map: &'a TransactionalTree,
documents: &'a TransactionalTree,
omitted_entries: &'a TransactionalTree,
view_entries: &'a TransactionalTree,
storage: &'a Database<DB>,
}
impl<'a, DB: Schema> DocumentRequest<'a, DB> {
fn map(&self) -> Result<(), ConflictableTransactionError<anyhow::Error>> {
self.invalidated_entries.remove(self.document_id)?;
let view = self
.storage
.data
.schema
.view_by_name(&self.map_request.view_name)
.unwrap();
let (doc_still_exists, map_result) =
if let Some(document) = self.documents.get(self.document_id)? {
let document =
bincode::deserialize::<Document<'_>>(&document).map_to_transaction_error()?;
(true, view.map(&document).map_to_transaction_error()?)
} else {
(false, None)
};
if let Some(map::Serialized { source, key, value }) = map_result {
self.omitted_entries.remove(self.document_id)?;
let keys: Vec<Cow<'_, [u8]>> = vec![Cow::Borrowed(&key)];
if let Some(existing_map) = self.document_map.insert(
self.document_id,
bincode::serialize(&keys).map_to_transaction_error()?,
)? {
remove_existing_view_entries_for_keys(
self.document_id,
&keys,
&existing_map,
self.view_entries,
view,
)?;
}
let entry_mapping = EntryMapping { source, value };
let view_entry = if let Some(existing_entry) = self.view_entries.get(&key)? {
let mut entry = bincode::deserialize::<ViewEntry>(&existing_entry)
.map_to_transaction_error()?;
let mut found = false;
for mapping in &mut entry.mappings {
if mapping.source == source {
found = true;
mapping.value = entry_mapping.value.clone();
break;
}
}
if !found {
entry.mappings.push(entry_mapping);
}
let mappings = entry
.mappings
.iter()
.map(|m| (key.as_slice(), m.value.as_slice()))
.collect::<Vec<_>>();
entry.reduced_value = view.reduce(&mappings, false).map_to_transaction_error()?;
entry
} else {
let reduced_value = view
.reduce(&[(&key, &entry_mapping.value)], false)
.map_to_transaction_error()?;
ViewEntry {
view_version: view.version(),
mappings: vec![entry_mapping],
reduced_value,
}
};
self.view_entries.insert(
key,
bincode::serialize(&view_entry).map_to_transaction_error()?,
)?;
} else {
if let Some(existing_map) = self.document_map.remove(self.document_id)? {
remove_existing_view_entries_for_keys(
self.document_id,
&[],
&existing_map,
self.view_entries,
view,
)?;
}
if doc_still_exists {
self.omitted_entries
.insert(self.document_id, IVec::default())?;
}
}
Ok(())
}
}
impl<DB> Keyed<Task> for Mapper<DB>
where
DB: Schema,
{
fn key(&self) -> Task {
Task::ViewMap(self.map.clone())
}
}
trait ToTransactionResult<T, E> {
fn map_to_transaction_error<RE: From<E>>(self) -> Result<T, ConflictableTransactionError<RE>>;
}
impl<T, E> ToTransactionResult<T, E> for Result<T, E> {
fn map_to_transaction_error<RE: From<E>>(self) -> Result<T, ConflictableTransactionError<RE>> {
self.map_err(|err| ConflictableTransactionError::Abort(RE::from(err)))
}
}
fn remove_existing_view_entries_for_keys(
document_id: &[u8],
keys: &[Cow<'_, [u8]>],
existing_map: &[u8],
view_entries: &TransactionalTree,
view: &dyn view::Serialized,
) -> Result<(), ConflictableTransactionError<anyhow::Error>> {
let existing_keys =
bincode::deserialize::<Vec<Cow<'_, [u8]>>>(existing_map).map_to_transaction_error()?;
if existing_keys == keys {
return Ok(());
}
assert_eq!(
existing_keys.len(),
1,
"need to add support for multi-emitted keys"
);
if let Some(existing_entry) = view_entries.get(&existing_keys[0])? {
let mut entry =
bincode::deserialize::<ViewEntry>(&existing_entry).map_to_transaction_error()?;
let document_id = u64::from_big_endian_bytes(document_id).unwrap();
entry.mappings.retain(|m| m.source != document_id);
if entry.mappings.is_empty() {
view_entries.remove(existing_keys[0].as_ref())?;
} else {
let mappings = entry
.mappings
.iter()
.map(|m| (existing_keys[0].as_ref(), m.value.as_slice()))
.collect::<Vec<_>>();
entry.reduced_value = view.reduce(&mappings, false).map_to_transaction_error()?;
view_entries.insert(
existing_keys[0].as_ref(),
bincode::serialize(&entry).map_to_transaction_error()?,
)?;
}
}
Ok(())
}