Skip to main content

prolly/prolly/
error.rs

1//! Error types for Prolly Trees
2
3use super::cid::Cid;
4use serde::{Deserialize, Serialize};
5
6/// A mutation to apply to the tree
7///
8/// Represents a single operation in a batch mutation: either an upsert (insert or update)
9/// or a delete operation.
10///
11#[derive(Clone, Debug, PartialEq)]
12pub enum Mutation {
13    /// Insert or update a key-value pair
14    Upsert { key: Vec<u8>, val: Vec<u8> },
15    /// Delete a key
16    Delete { key: Vec<u8> },
17}
18
19impl Mutation {
20    /// Get the key for this mutation
21    ///
22    pub fn key(&self) -> &[u8] {
23        match self {
24            Mutation::Upsert { key, .. } => key,
25            Mutation::Delete { key } => key,
26        }
27    }
28
29    /// Check if this is a delete mutation
30    pub fn is_delete(&self) -> bool {
31        matches!(self, Mutation::Delete { .. })
32    }
33}
34
35/// Difference between two trees
36///
37/// Represents a single change between a base tree and another tree.
38///
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub enum Diff {
41    /// Entry exists in the new tree but not in the base tree
42    Added { key: Vec<u8>, val: Vec<u8> },
43    /// Entry exists in the base tree but not in the new tree
44    Removed { key: Vec<u8>, val: Vec<u8> },
45    /// Entry exists in both trees but with different values
46    Changed {
47        key: Vec<u8>,
48        old: Vec<u8>,
49        new: Vec<u8>,
50    },
51}
52
53impl Diff {
54    /// Borrow the key affected by this diff entry.
55    pub fn key(&self) -> &[u8] {
56        match self {
57            Diff::Added { key, .. } | Diff::Removed { key, .. } | Diff::Changed { key, .. } => key,
58        }
59    }
60}
61
62/// Merge conflict information
63///
64/// Contains all the information needed to resolve a conflict during a three-way merge.
65///
66#[derive(Clone, Debug)]
67pub struct Conflict {
68    /// The key where the conflict occurred
69    pub key: Vec<u8>,
70    /// The value in the base tree (None if key didn't exist in base)
71    pub base: Option<Vec<u8>>,
72    /// The value in the left tree (None if the key is absent)
73    pub left: Option<Vec<u8>>,
74    /// The value in the right tree (None if the key is absent)
75    pub right: Option<Vec<u8>>,
76}
77
78/// Resolution for a standard three-way merge conflict.
79///
80/// `Value` keeps the key with the provided value, `Delete` removes the key,
81/// and `Unresolved` returns [`Error::Conflict`] to the caller.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub enum Resolution {
84    /// Keep the key with this value.
85    Value(Vec<u8>),
86    /// Delete the key from the merged tree.
87    Delete,
88    /// Leave the conflict unresolved.
89    Unresolved,
90}
91
92impl Resolution {
93    /// Resolve the conflict to a concrete value.
94    pub fn value(value: impl Into<Vec<u8>>) -> Self {
95        Self::Value(value.into())
96    }
97
98    /// Resolve the conflict by deleting the key.
99    pub fn delete() -> Self {
100        Self::Delete
101    }
102
103    /// Leave the conflict unresolved.
104    pub fn unresolved() -> Self {
105        Self::Unresolved
106    }
107}
108
109/// Conflict resolution strategy
110///
111/// A function that takes a conflict and returns an explicit resolution.
112/// If [`Resolution::Unresolved`] is returned, the merge will fail with a
113/// `Conflict` error.
114///
115///
116/// # Example
117/// ```
118/// use prolly::{Resolution, Resolver};
119///
120/// // Always prefer the left value
121/// let prefer_left: Resolver = Box::new(|conflict| {
122///     match &conflict.left {
123///         Some(value) => Resolution::value(value.clone()),
124///         None => Resolution::delete(),
125///     }
126/// });
127///
128/// // Always prefer the right value
129/// let prefer_right: Resolver = Box::new(|conflict| {
130///     match &conflict.right {
131///         Some(value) => Resolution::value(value.clone()),
132///         None => Resolution::delete(),
133///     }
134/// });
135///
136/// // Concatenate values
137/// let concat: Resolver = Box::new(|conflict| {
138///     match (&conflict.left, &conflict.right) {
139///         (Some(left), Some(right)) => {
140///             let mut result = left.clone();
141///             result.extend(right);
142///             Resolution::value(result)
143///         }
144///         _ => Resolution::unresolved(),
145///     }
146/// });
147/// ```
148pub type Resolver = Box<dyn Fn(&Conflict) -> Resolution + Send + Sync>;
149
150/// Ready-made standard merge resolvers.
151pub mod resolver {
152    use super::{Conflict, Resolution};
153
154    /// Prefer the left side. If left deleted the key, delete it.
155    pub fn prefer_left(conflict: &Conflict) -> Resolution {
156        match &conflict.left {
157            Some(value) => Resolution::value(value.clone()),
158            None => Resolution::delete(),
159        }
160    }
161
162    /// Prefer the right side. If right deleted the key, delete it.
163    pub fn prefer_right(conflict: &Conflict) -> Resolution {
164        match &conflict.right {
165            Some(value) => Resolution::value(value.clone()),
166            None => Resolution::delete(),
167        }
168    }
169
170    /// Delete the key when either side deleted it; otherwise leave unresolved.
171    pub fn delete_wins(conflict: &Conflict) -> Resolution {
172        if conflict.left.is_none() || conflict.right.is_none() {
173            Resolution::delete()
174        } else {
175            Resolution::unresolved()
176        }
177    }
178
179    /// Keep the updated side for delete/update conflicts; otherwise leave unresolved.
180    pub fn update_wins(conflict: &Conflict) -> Resolution {
181        match (&conflict.left, &conflict.right) {
182            (Some(value), None) | (None, Some(value)) => Resolution::value(value.clone()),
183            _ => Resolution::unresolved(),
184        }
185    }
186}
187
188use super::secondary_index::IndexProjection;
189use super::transaction::TransactionConflict;
190use super::versioned_map::MapVersionId;
191
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum IndexErrorCode {
194    FormatUnsupported,
195    Conflict,
196    Corruption,
197    DefinitionInvalid,
198    RuntimeDefinitionMissing,
199    ManagedWriteRequired,
200    OperationUnsupported,
201    ExtractionFailed,
202    ProjectionInvalid,
203    ResourceLimit,
204    CursorInvalid,
205    BundleInvalid,
206    SnapshotUnavailable,
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub enum RetryAdvice {
211    Never,
212    RetryFreshState,
213    RetryAfter,
214}
215
216impl IndexErrorCode {
217    pub const fn as_str(self) -> &'static str {
218        match self {
219            Self::FormatUnsupported => "format_unsupported",
220            Self::Conflict => "conflict",
221            Self::Corruption => "corruption",
222            Self::DefinitionInvalid => "definition_invalid",
223            Self::RuntimeDefinitionMissing => "runtime_definition_missing",
224            Self::ManagedWriteRequired => "managed_write_required",
225            Self::OperationUnsupported => "operation_unsupported",
226            Self::ExtractionFailed => "extraction_failed",
227            Self::ProjectionInvalid => "projection_invalid",
228            Self::ResourceLimit => "resource_limit",
229            Self::CursorInvalid => "cursor_invalid",
230            Self::BundleInvalid => "bundle_invalid",
231            Self::SnapshotUnavailable => "snapshot_unavailable",
232        }
233    }
234}
235
236impl RetryAdvice {
237    pub const fn as_str(self) -> &'static str {
238        match self {
239            Self::Never => "never",
240            Self::RetryFreshState => "retry_fresh_state",
241            Self::RetryAfter => "retry_after",
242        }
243    }
244}
245
246/// Prolly tree errors
247pub enum Error {
248    /// Node not found in store
249    NotFound(Cid),
250    /// Invalid node structure
251    InvalidNode,
252    /// Persisted tree format parameters are invalid.
253    InvalidFormat(String),
254    /// Runtime execution limits must be finite and nonzero.
255    InvalidExecutionConfig { field: &'static str, value: usize },
256    /// A node was decoded under a different persisted tree format.
257    FormatMismatch { expected: Cid, actual: Cid },
258    /// One entry cannot fit below the persisted hard byte limit.
259    EntryTooLarge { encoded_bytes: u64, limit: u64 },
260    /// Deserialization failed
261    Deserialize(String),
262    /// Serialization failed
263    Serialize(String),
264    /// Storage error
265    Store(Box<dyn std::error::Error + Send + Sync>),
266    /// Stored bytes did not hash to the CID they were stored under.
267    CidMismatch { expected: Cid, actual: Cid },
268    /// Merge conflict - occurs when both trees modify the same key differently
269    /// and no resolver is provided or the resolver returns `Resolution::Unresolved`
270    ///
271    Conflict(Conflict),
272    /// Mutation buffer is full - adding a mutation would exceed the buffer size limit
273    BufferFull,
274    /// A savepoint belongs to another write-session generation.
275    InvalidSavepoint,
276    /// A patch does not describe the selected immutable base.
277    PatchBaseMismatch,
278    /// A structural patch is malformed or cannot be safely applied.
279    InvalidStructuralPatch(String),
280    /// Sorted bulk loading received keys out of order.
281    UnsortedInput { previous: Vec<u8>, next: Vec<u8> },
282    /// Splice received more than one mutation for a logical key.
283    DuplicateMutation { key: Vec<u8> },
284    /// Splice manager and immutable tree use different shape settings.
285    SpliceConfigMismatch,
286    /// A GC retention policy referenced named roots that were not present.
287    MissingNamedRoots { names: Vec<Vec<u8>> },
288    /// A portable snapshot bundle is malformed or not self-contained.
289    InvalidSnapshotBundle(String),
290    /// The configured store does not support strict atomic transactions.
291    UnsupportedTransactions { store: &'static str },
292    /// A named root from an obsolete secondary-index architecture was found.
293    IndexFormatUnsupported,
294    /// A transaction could not commit because a validated named root changed.
295    TransactionConflict(Box<TransactionConflict>),
296    /// A built-in versioned-map catalog is missing or internally inconsistent.
297    InvalidVersionedMap(String),
298    /// A runtime secondary-index definition is invalid.
299    InvalidIndexDefinition { reason: String },
300    /// Persisted active index semantics have no matching runtime extractor.
301    IndexRuntimeDefinitionMissing { name: Vec<u8>, generation: u64 },
302    /// Runtime and persisted descriptor fingerprints disagree.
303    IndexDefinitionMismatch {
304        name: Vec<u8>,
305        persisted: Cid,
306        runtime: Cid,
307    },
308    /// A managed source map must be mutated through `IndexedMap`.
309    IndexesRequireIndexedMap {
310        map_id: Vec<u8>,
311        active_indexes: Vec<Vec<u8>>,
312    },
313    /// The requested operation has no safe canonical indexed implementation.
314    IndexOperationUnsupported { operation: &'static str },
315    /// An application extractor rejected one source record.
316    IndexExtractionFailed {
317        name: Vec<u8>,
318        primary_key: Vec<u8>,
319        reason: String,
320    },
321    /// An extractor emission is incompatible with its projection mode.
322    IndexProjectionMismatch {
323        name: Vec<u8>,
324        mode: IndexProjection,
325        primary_key: Vec<u8>,
326    },
327    /// One source record emitted different projections for the same term.
328    ConflictingIndexProjection {
329        name: Vec<u8>,
330        primary_key: Vec<u8>,
331        term: Vec<u8>,
332    },
333    /// Repeated source movement prevented index activation.
334    IndexBuildConflictLimitExceeded { name: Vec<u8>, attempts: usize },
335    /// No exact snapshot exists for an index at the selected source version.
336    IndexUnavailableAtVersion {
337        name: Vec<u8>,
338        source_version: MapVersionId,
339    },
340    /// A persisted snapshot disagrees with the selected source or index root.
341    IndexSnapshotMismatch {
342        name: Vec<u8>,
343        source_version: MapVersionId,
344        reason: String,
345    },
346    /// A cursor belongs to a different immutable indexed snapshot.
347    IndexCursorVersionMismatch { expected: String, actual: String },
348    /// A destructive index GC sweep lacks a quiescence/lease safety proof.
349    IndexGcUnsafe,
350    /// Index work exceeded a configured resource bound.
351    IndexResourceLimitExceeded {
352        resource: &'static str,
353        limit: usize,
354        actual: usize,
355    },
356    /// A current indexed-snapshot bundle is malformed or inconsistent.
357    InvalidIndexedSnapshotBundle { reason: String },
358    /// A proximity-map shape configuration is invalid.
359    InvalidProximityConfig { reason: String },
360    /// Persisted proximity bytes use a format version this build does not read.
361    UnsupportedProximityVersion { found: u8, required: u8 },
362    /// A vector is incompatible with the proximity-map configuration.
363    InvalidProximityVector { reason: String },
364    /// Cosine distance cannot prepare a vector with zero Euclidean norm.
365    ZeroCosineVector,
366    /// A proximity build or mutation contains the same logical key twice.
367    DuplicateProximityKey { key: Vec<u8> },
368    /// Proximity search options are invalid.
369    InvalidProximitySearch { reason: String },
370    /// Proximity accelerator construction exceeded an explicit resource bound.
371    ProximityResourceLimitExceeded {
372        resource: &'static str,
373        limit: usize,
374        actual: usize,
375    },
376    /// A persisted proximity record, node, or descriptor is malformed.
377    InvalidProximityObject { kind: &'static str, reason: String },
378    /// One canonical proximity node exceeds the configured hard byte limit.
379    ProximityNodeTooLarge {
380        level: u8,
381        entries: usize,
382        encoded_bytes: usize,
383        limit: usize,
384    },
385    /// A typed content-graph traversal exceeded a configured resource limit.
386    ContentGraphResourceLimitExceeded {
387        resource: &'static str,
388        limit: usize,
389        actual: usize,
390    },
391}
392
393impl std::fmt::Debug for Error {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        // Error values routinely reach generic `Debug` logging through
396        // `unwrap`, tracing, and binding adapters. Keep that path on the same
397        // redacted contract as `Display`; raw keys, terms, cursor bounds, and
398        // extractor diagnostics remain available only in explicitly matched
399        // structured variants.
400        std::fmt::Display::fmt(self, f)
401    }
402}
403
404impl Error {
405    pub(crate) fn transaction_conflict(conflict: TransactionConflict) -> Self {
406        Self::TransactionConflict(Box::new(conflict))
407    }
408
409    pub fn index_code(&self) -> Option<IndexErrorCode> {
410        use IndexErrorCode as Code;
411        Some(match self {
412            Self::IndexFormatUnsupported => Code::FormatUnsupported,
413            Self::TransactionConflict(_) | Self::IndexBuildConflictLimitExceeded { .. } => {
414                Code::Conflict
415            }
416            Self::IndexSnapshotMismatch { .. } => Code::Corruption,
417            Self::InvalidIndexDefinition { .. } | Self::IndexDefinitionMismatch { .. } => {
418                Code::DefinitionInvalid
419            }
420            Self::IndexRuntimeDefinitionMissing { .. } => Code::RuntimeDefinitionMissing,
421            Self::IndexesRequireIndexedMap { .. } => Code::ManagedWriteRequired,
422            Self::IndexOperationUnsupported { .. } => Code::OperationUnsupported,
423            Self::IndexExtractionFailed { .. } => Code::ExtractionFailed,
424            Self::IndexProjectionMismatch { .. } | Self::ConflictingIndexProjection { .. } => {
425                Code::ProjectionInvalid
426            }
427            Self::IndexResourceLimitExceeded { .. } => Code::ResourceLimit,
428            Self::IndexCursorVersionMismatch { .. } => Code::CursorInvalid,
429            Self::IndexGcUnsafe => Code::OperationUnsupported,
430            Self::InvalidIndexedSnapshotBundle { .. } => Code::BundleInvalid,
431            Self::IndexUnavailableAtVersion { .. } => Code::SnapshotUnavailable,
432            _ => return None,
433        })
434    }
435
436    pub fn retry_advice(&self) -> RetryAdvice {
437        match self {
438            Self::TransactionConflict(_) | Self::IndexBuildConflictLimitExceeded { .. } => {
439                RetryAdvice::RetryFreshState
440            }
441            Self::Store(_) => RetryAdvice::RetryAfter,
442            _ => RetryAdvice::Never,
443        }
444    }
445}
446
447impl std::fmt::Display for Error {
448    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449        match self {
450            Error::NotFound(cid) => write!(f, "node not found: {:?}", cid),
451            Error::InvalidNode => write!(f, "invalid node structure"),
452            Error::InvalidFormat(message) => write!(f, "invalid tree format: {message}"),
453            Error::InvalidExecutionConfig { field, value } => {
454                write!(f, "invalid execution config: {field} must be nonzero, got {value}")
455            }
456            Error::FormatMismatch { expected, actual } => write!(
457                f,
458                "tree format mismatch: expected {:?}, got {:?}",
459                expected, actual
460            ),
461            Error::EntryTooLarge {
462                encoded_bytes,
463                limit,
464            } => write!(
465                f,
466                "entry encodes to {encoded_bytes} bytes, exceeding node limit {limit}"
467            ),
468            Error::Deserialize(e) => write!(f, "deserialize error: {}", e),
469            Error::Serialize(e) => write!(f, "serialize error: {}", e),
470            Error::Store(e) => write!(f, "storage error: {}", e),
471            Error::CidMismatch { expected, actual } => {
472                write!(
473                    f,
474                    "content CID mismatch: expected {:?}, got {:?}",
475                    expected, actual
476                )
477            }
478            Error::Conflict(c) => write!(f, "merge conflict at key: {:?}", c.key),
479            Error::BufferFull => write!(f, "mutation buffer is full"),
480            Error::InvalidSavepoint => write!(f, "invalid or stale write-session savepoint"),
481            Error::PatchBaseMismatch => write!(f, "patch base root or values do not match"),
482            Error::InvalidStructuralPatch(reason) => {
483                write!(f, "invalid structural patch: {reason}")
484            }
485            Error::UnsortedInput { previous, next } => write!(
486                f,
487                "sorted input keys are out of order: previous={:?} next={:?}",
488                previous, next
489            ),
490            Error::DuplicateMutation { key } => {
491                write!(f, "duplicate splice mutation: {key:?}")
492            }
493            Error::SpliceConfigMismatch => {
494                write!(f, "splice manager/tree configuration mismatch")
495            }
496            Error::MissingNamedRoots { names } => {
497                write!(f, "missing named roots for retention policy: {:?}", names)
498            }
499            Error::InvalidSnapshotBundle(message) => {
500                write!(f, "invalid snapshot bundle: {message}")
501            }
502            Error::UnsupportedTransactions { store } => {
503                write!(f, "store does not support strict transactions: {store}")
504            }
505            Error::TransactionConflict(conflict) => {
506                write!(
507                    f,
508                    "transaction conflict for named root: {:?}",
509                    conflict.name
510                )
511            }
512            Error::InvalidVersionedMap(message) => {
513                write!(f, "invalid versioned map: {message}")
514            }
515            Error::IndexFormatUnsupported => {
516                write!(f, "secondary index format is unsupported; hard cutover required")
517            }
518            Error::InvalidIndexDefinition { .. } => {
519                write!(f, "invalid secondary index definition")
520            }
521            Error::IndexRuntimeDefinitionMissing { generation, .. } => write!(
522                f,
523                "runtime secondary index definition missing: generation={generation}"
524            ),
525            Error::IndexDefinitionMismatch { .. } => {
526                write!(f, "secondary index definition mismatch")
527            }
528            Error::IndexesRequireIndexedMap { .. } => {
529                write!(f, "managed map requires IndexedMap coordinator")
530            }
531            Error::IndexOperationUnsupported { operation } => {
532                write!(f, "indexed map operation is unsupported: {operation}")
533            }
534            Error::IndexExtractionFailed { .. } => {
535                write!(f, "secondary index extraction failed")
536            }
537            Error::IndexProjectionMismatch { mode, .. } => {
538                write!(f, "secondary index projection mismatch: mode={mode:?}")
539            }
540            Error::ConflictingIndexProjection { .. } => {
541                write!(f, "conflicting secondary index projection")
542            }
543            Error::IndexBuildConflictLimitExceeded { attempts, .. } => write!(
544                f,
545                "secondary index conflict limit exceeded: attempts={attempts}"
546            ),
547            Error::IndexUnavailableAtVersion { .. } => {
548                write!(f, "secondary index unavailable at selected snapshot")
549            }
550            Error::IndexSnapshotMismatch { .. } => {
551                write!(f, "secondary index snapshot closure is inconsistent")
552            }
553            Error::IndexCursorVersionMismatch { .. } => {
554                write!(f, "secondary index cursor does not match the query snapshot")
555            }
556            Error::IndexGcUnsafe => {
557                write!(f, "secondary index garbage collection lacks a safety proof")
558            }
559            Error::IndexResourceLimitExceeded {
560                resource,
561                limit,
562                actual,
563            } => write!(
564                f,
565                "secondary index resource limit exceeded: resource={resource} limit={limit} actual={actual}"
566            ),
567            Error::InvalidIndexedSnapshotBundle { .. } => {
568                write!(f, "invalid indexed snapshot bundle")
569            }
570            Error::InvalidProximityConfig { reason } => {
571                write!(f, "invalid proximity configuration: {reason}")
572            }
573            Error::UnsupportedProximityVersion { found, required } => write!(
574                f,
575                "unsupported proximity format version: found={found} required={required}"
576            ),
577            Error::InvalidProximityVector { reason } => {
578                write!(f, "invalid proximity vector: {reason}")
579            }
580            Error::ZeroCosineVector => write!(f, "cosine proximity vector has zero norm"),
581            Error::DuplicateProximityKey { key } => {
582                write!(f, "duplicate proximity key: {key:?}")
583            }
584            Error::InvalidProximitySearch { reason } => {
585                write!(f, "invalid proximity search options: {reason}")
586            }
587            Error::ProximityResourceLimitExceeded {
588                resource,
589                limit,
590                actual,
591            } => write!(
592                f,
593                "proximity accelerator resource limit exceeded: resource={resource} limit={limit} actual={actual}"
594            ),
595            Error::InvalidProximityObject { kind, reason } => {
596                write!(f, "invalid proximity {kind}: {reason}")
597            }
598            Error::ProximityNodeTooLarge {
599                level,
600                entries,
601                encoded_bytes,
602                limit,
603            } => write!(
604                f,
605                "proximity node exceeds byte limit: level={level} entries={entries} bytes={encoded_bytes} limit={limit}"
606            ),
607            Error::ContentGraphResourceLimitExceeded {
608                resource,
609                limit,
610                actual,
611            } => write!(
612                f,
613                "content graph resource limit exceeded: resource={resource} limit={limit} actual={actual}"
614            ),
615        }
616    }
617}
618
619impl std::error::Error for Error {}