use crate::lifecycle::{QueryError, Store, StoreError, StoreState};
use crate::meta::Predicate;
use crate::planner::PlanError;
use super::{DocId, IngestError, PurgeError};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeleteMatchingReport {
deleted_ids: Vec<DocId>,
generation: u64,
}
impl DeleteMatchingReport {
#[must_use]
pub fn deleted_ids(&self) -> &[DocId] {
&self.deleted_ids
}
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
}
#[derive(Debug)]
pub enum DeleteMatchingError {
Predicate(PlanError),
Query(QueryError),
Delete(IngestError),
Purge(PurgeError),
}
impl std::fmt::Display for DeleteMatchingError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Predicate(error) => write!(formatter, "delete filter: {error}"),
Self::Query(error) => write!(formatter, "delete match: {error}"),
Self::Delete(error) => write!(formatter, "delete: {error}"),
Self::Purge(error) => write!(formatter, "delete purge: {error}"),
}
}
}
impl std::error::Error for DeleteMatchingError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Predicate(error) => Some(error),
Self::Query(error) => Some(error),
Self::Delete(error) => Some(error),
Self::Purge(error) => Some(error),
}
}
}
impl From<StoreError> for DeleteMatchingError {
fn from(error: StoreError) -> Self {
Self::Purge(PurgeError::Store(error))
}
}
impl Store {
pub fn delete_matching(
&self,
predicate: &Predicate,
) -> Result<DeleteMatchingReport, DeleteMatchingError> {
#[cfg(feature = "graph-cypher")]
{
self.with_native_document_recovery(
|| self.delete_matching_attempt(predicate),
|error| matches!(error, DeleteMatchingError::Delete(error) if error.is_native_reclaim_debt()),
DeleteMatchingError::Delete,
)
}
#[cfg(not(feature = "graph-cypher"))]
self.delete_matching_attempt(predicate)
}
fn delete_matching_attempt(
&self,
predicate: &Predicate,
) -> Result<DeleteMatchingReport, DeleteMatchingError> {
crate::planner::validate_predicate(predicate, &self.schema)
.map_err(DeleteMatchingError::Predicate)?;
let _maintenance = self
.maintenance
.lock()
.map_err(|_| StoreError::Synchronization {
component: "delete matching",
})?;
#[cfg(feature = "graph-cypher")]
let mut native_writer = self.native_document_writer()?;
#[cfg(feature = "graph-cypher")]
let native_deleted = if let Some(writer) =
native_writer.as_mut().and_then(|slot| slot.as_mut())
{
let ids = {
let active = self
.active
.lock()
.map_err(|_| StoreError::Synchronization {
component: "active segment",
})?;
let current = active.as_ref().ok_or(StoreError::Closed)?;
let snapshot = self
.snapshot
.read()
.map_err(|_| StoreError::Synchronization {
component: "published snapshot",
})?;
let snapshot = snapshot.as_ref().ok_or(StoreError::Closed)?;
self.live_document_ids_matching(¤t.segment, snapshot.segments(), predicate)
.map_err(DeleteMatchingError::Query)?
};
if !ids.is_empty()
&& self
.documents_have_native_nodes(&ids)
.map_err(DeleteMatchingError::Delete)?
{
self.prepare_purge_intent(
&ids,
self.available_purge_bytes()
.map_err(DeleteMatchingError::Purge)?,
self.vfs.as_ref(),
)
.map_err(DeleteMatchingError::Purge)?;
self.delete_native_documents_locked(&super::DeleteBatch::new(ids.clone()), writer)
.map_err(DeleteMatchingError::Delete)?;
Some(ids)
} else {
None
}
} else {
None
};
let state = self
.state
.lock()
.map_err(|_| StoreError::Synchronization { component: "state" })?;
match *state {
StoreState::Open => {}
StoreState::Closing => return Err(StoreError::Closing.into()),
StoreState::Closed => return Err(StoreError::Closed.into()),
}
let writer_lock = self
.writer_lock
.lock()
.map_err(|_| StoreError::Synchronization {
component: "writer lock",
})?;
if writer_lock.is_none() {
return Err(StoreError::ReadOnly.into());
}
self.require_no_snapshot_views()?;
let mut wal = self
.wal_writer
.lock()
.map_err(|_| StoreError::Synchronization {
component: "WAL writer",
})?;
let writer = wal.as_mut().ok_or(StoreError::ReadOnly)?;
let (generation, active) = {
let active = self
.active
.lock()
.map_err(|_| StoreError::Synchronization {
component: "active segment",
})?;
let current = active.as_ref().ok_or(StoreError::Closed)?;
(current.generation, std::sync::Arc::clone(¤t.segment))
};
let snapshot = self
.snapshot
.read()
.map_err(|_| StoreError::Synchronization {
component: "published snapshot",
})?
.as_ref()
.cloned()
.ok_or(StoreError::Closed)?;
let ids = self
.live_document_ids_matching(&active, snapshot.segments(), predicate)
.map_err(DeleteMatchingError::Query)?;
#[cfg(feature = "graph-cypher")]
let (ids, already_deleted) = match native_deleted {
Some(ids) => (ids, true),
None => (ids, false),
};
#[cfg(not(feature = "graph-cypher"))]
let already_deleted = false;
drop(snapshot);
drop(active);
let report = if ids.is_empty() {
DeleteMatchingReport {
deleted_ids: ids,
generation,
}
} else {
self.delete_and_purge_locked(writer, ids, already_deleted)?
};
drop(wal);
drop(writer_lock);
drop(state);
Ok(report)
}
fn delete_and_purge_locked(
&self,
writer: &mut super::StoreWal,
ids: Vec<DocId>,
already_deleted: bool,
) -> Result<DeleteMatchingReport, DeleteMatchingError> {
let vfs = self.vfs.as_ref();
let available = self
.available_purge_bytes()
.map_err(DeleteMatchingError::Purge)?;
let token = self
.schedule_purge_locked(&ids, available, vfs, writer)
.map_err(DeleteMatchingError::Purge)?;
if !already_deleted && let Err(error) = self.delete_with_writer(writer, &ids, &[]) {
self.abandon_scheduled_purge_locked(vfs, writer)
.map_err(DeleteMatchingError::Purge)?;
return Err(DeleteMatchingError::Delete(error));
}
let purged = self
.complete_physical_purge_locked(vfs, writer, token)
.map_err(DeleteMatchingError::Purge)?;
Ok(DeleteMatchingReport {
deleted_ids: ids,
generation: purged.generation(),
})
}
}