use thiserror::Error;
use crate::{
storage::StorageError,
supertable::{
QueryError,
error::BuildError,
manifest::ManifestLoadError,
wal::{
persistence::WalStoreError,
pipeline::{AppendPhaseError, TombstonePhaseError},
state_doc::WalId,
},
},
};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MutationStats {
pub(crate) wal_id: WalId,
pub(crate) matched: usize,
pub(crate) n_tombstoned: usize,
pub(crate) n_not_found: usize,
}
impl MutationStats {
pub fn matched(&self) -> usize {
self.matched
}
pub fn n_tombstoned(&self) -> usize {
self.n_tombstoned
}
pub fn n_not_found(&self) -> usize {
self.n_not_found
}
#[cfg(feature = "remote")]
pub(crate) fn from_remote(matched: usize, n_tombstoned: usize, n_not_found: usize) -> Self {
Self {
wal_id: WalId(0),
matched,
n_tombstoned,
n_not_found,
}
}
}
pub const MAX_TARGETS_PER_MUTATION: usize = 100_000;
#[derive(Debug, Error)]
pub enum MutationError {
#[error("predicate evaluation failed: {0}")]
PredicateEval(#[from] QueryError),
#[error("table was dropped and purged while this handle was open")]
TableGone,
#[error("predicate matched {matched} rows; mutation cap is {cap}")]
MatchCountExceedsCap { matched: usize, cap: usize },
#[error("cardinality mismatch: predicate matched {matched} rows; new_rows has {new_rows}")]
CardinalityMismatch { matched: usize, new_rows: usize },
#[error("new_rows schema does not match the supertable's user schema: {0}")]
SchemaMismatch(String),
#[error("supertable has no storage attached; delete / update requires durable storage")]
NoStorageAttached,
#[error("storage error: {0}")]
Storage(#[from] StorageError),
#[error("WAL store error: {0}")]
WalStore(#[from] WalStoreError),
#[error("append phase failed: {0}")]
AppendPhase(#[from] AppendPhaseError),
#[error("tombstone phase failed: {0}")]
TombstonePhase(#[from] TombstonePhaseError),
#[error("failed to refresh to the latest manifest for target resolution: {0}")]
TargetResolve(#[source] ManifestLoadError),
}
impl MutationError {
pub(crate) fn over_budget(&self) -> Option<&str> {
match self {
MutationError::PredicateEval(q) => q.over_budget(),
_ => None,
}
}
pub(crate) fn is_conflict(&self) -> bool {
match self {
MutationError::Storage(e) => e.is_conflict(),
MutationError::WalStore(e) => e.is_conflict(),
MutationError::AppendPhase(e) => e.is_conflict(),
MutationError::TombstonePhase(e) => e.is_conflict(),
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingUpdate {
pub matched: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingDelete {
pub matched: usize,
}
#[derive(Debug, Clone)]
pub struct CommitResult {
pub wal_ids: Vec<WalId>,
pub outcomes: Vec<MutationStats>,
}
#[derive(Debug, Error)]
pub enum CommitError {
#[error("append-phase commit failed: {0}")]
AppendFlush(BuildError),
#[error("partial commit: {committed} of {total} mutations completed before {cause}")]
PartialCommit {
committed_wal_ids: Vec<WalId>,
committed: usize,
total: usize,
cause: Box<MutationError>,
},
}
impl CommitError {
pub(crate) fn over_budget(&self) -> Option<&str> {
match self {
CommitError::AppendFlush(b) => b.over_budget(),
CommitError::PartialCommit { cause, .. } => cause.over_budget(),
}
}
pub(crate) fn is_conflict(&self) -> bool {
match self {
CommitError::AppendFlush(b) => b.is_conflict(),
CommitError::PartialCommit { cause, .. } => cause.is_conflict(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn over_budget_only_from_predicate_eval() {
let budgeted = MutationError::PredicateEval(QueryError::OverBudget("cap".into()));
assert_eq!(budgeted.over_budget(), Some("cap"));
assert_eq!(
MutationError::MatchCountExceedsCap { matched: 5, cap: 3 }.over_budget(),
None
);
}
}