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    StoreProfileUnsupported,
196    Conflict,
197    Corruption,
198    DefinitionInvalid,
199    RuntimeDefinitionMissing,
200    ManagedWriteRequired,
201    OperationUnsupported,
202    ExtractionFailed,
203    ProjectionInvalid,
204    ResourceLimit,
205    CursorInvalid,
206    BundleInvalid,
207    SnapshotUnavailable,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq)]
211pub enum RetryAdvice {
212    Never,
213    RetryFreshState,
214    RetryAfter,
215}
216
217impl IndexErrorCode {
218    pub const fn as_str(self) -> &'static str {
219        match self {
220            Self::FormatUnsupported => "format_unsupported",
221            Self::StoreProfileUnsupported => "store_profile_unsupported",
222            Self::Conflict => "conflict",
223            Self::Corruption => "corruption",
224            Self::DefinitionInvalid => "definition_invalid",
225            Self::RuntimeDefinitionMissing => "runtime_definition_missing",
226            Self::ManagedWriteRequired => "managed_write_required",
227            Self::OperationUnsupported => "operation_unsupported",
228            Self::ExtractionFailed => "extraction_failed",
229            Self::ProjectionInvalid => "projection_invalid",
230            Self::ResourceLimit => "resource_limit",
231            Self::CursorInvalid => "cursor_invalid",
232            Self::BundleInvalid => "bundle_invalid",
233            Self::SnapshotUnavailable => "snapshot_unavailable",
234        }
235    }
236}
237
238impl RetryAdvice {
239    pub const fn as_str(self) -> &'static str {
240        match self {
241            Self::Never => "never",
242            Self::RetryFreshState => "retry_fresh_state",
243            Self::RetryAfter => "retry_after",
244        }
245    }
246}
247
248/// Prolly tree errors
249pub enum Error {
250    /// Node not found in store
251    NotFound(Cid),
252    /// Invalid node structure
253    InvalidNode,
254    /// Persisted tree format parameters are invalid.
255    InvalidFormat(String),
256    /// Runtime execution limits must be finite and nonzero.
257    InvalidExecutionConfig { field: &'static str, value: usize },
258    /// A node was decoded under a different persisted tree format.
259    FormatMismatch { expected: Cid, actual: Cid },
260    /// One entry cannot fit below the persisted hard byte limit.
261    EntryTooLarge { encoded_bytes: u64, limit: u64 },
262    /// Deserialization failed
263    Deserialize(String),
264    /// Serialization failed
265    Serialize(String),
266    /// Storage error
267    Store(Box<dyn std::error::Error + Send + Sync>),
268    /// Stored bytes did not hash to the CID they were stored under.
269    CidMismatch { expected: Cid, actual: Cid },
270    /// Merge conflict - occurs when both trees modify the same key differently
271    /// and no resolver is provided or the resolver returns `Resolution::Unresolved`
272    ///
273    Conflict(Conflict),
274    /// Mutation buffer is full - adding a mutation would exceed the buffer size limit
275    BufferFull,
276    /// A savepoint belongs to another write-session generation.
277    InvalidSavepoint,
278    /// A patch does not describe the selected immutable base.
279    PatchBaseMismatch,
280    /// A structural patch is malformed or cannot be safely applied.
281    InvalidStructuralPatch(String),
282    /// Sorted bulk loading received keys out of order.
283    UnsortedInput { previous: Vec<u8>, next: Vec<u8> },
284    /// Splice received more than one mutation for a logical key.
285    DuplicateMutation { key: Vec<u8> },
286    /// Splice manager and immutable tree use different shape settings.
287    SpliceConfigMismatch,
288    /// A GC retention policy referenced named roots that were not present.
289    MissingNamedRoots { names: Vec<Vec<u8>> },
290    /// A portable snapshot bundle is malformed or not self-contained.
291    InvalidSnapshotBundle(String),
292    /// The configured store does not support strict atomic transactions.
293    UnsupportedTransactions { store: &'static str },
294    /// The configured store has not proved the required indexed-store profile.
295    UnsupportedIndexedStoreProfile {
296        store: &'static str,
297        required: &'static str,
298        actual: &'static str,
299    },
300    /// A named root from an obsolete secondary-index architecture was found.
301    IndexFormatUnsupported,
302    /// A transaction could not commit because a validated named root changed.
303    TransactionConflict(Box<TransactionConflict>),
304    /// A built-in versioned-map catalog is missing or internally inconsistent.
305    InvalidVersionedMap(String),
306    /// A runtime secondary-index definition is invalid.
307    InvalidIndexDefinition { reason: String },
308    /// Persisted active index semantics have no matching runtime extractor.
309    IndexRuntimeDefinitionMissing { name: Vec<u8>, generation: u64 },
310    /// Runtime and persisted descriptor fingerprints disagree.
311    IndexDefinitionMismatch {
312        name: Vec<u8>,
313        persisted: Cid,
314        runtime: Cid,
315    },
316    /// A managed source map must be mutated through `IndexedMap`.
317    IndexesRequireIndexedMap {
318        map_id: Vec<u8>,
319        active_indexes: Vec<Vec<u8>>,
320    },
321    /// The requested operation has no safe canonical indexed implementation.
322    IndexOperationUnsupported { operation: &'static str },
323    /// An application extractor rejected one source record.
324    IndexExtractionFailed {
325        name: Vec<u8>,
326        primary_key: Vec<u8>,
327        reason: String,
328    },
329    /// An extractor emission is incompatible with its projection mode.
330    IndexProjectionMismatch {
331        name: Vec<u8>,
332        mode: IndexProjection,
333        primary_key: Vec<u8>,
334    },
335    /// One source record emitted different projections for the same term.
336    ConflictingIndexProjection {
337        name: Vec<u8>,
338        primary_key: Vec<u8>,
339        term: Vec<u8>,
340    },
341    /// Repeated source movement prevented index activation.
342    IndexBuildConflictLimitExceeded { name: Vec<u8>, attempts: usize },
343    /// No exact snapshot exists for an index at the selected source version.
344    IndexUnavailableAtVersion {
345        name: Vec<u8>,
346        source_version: MapVersionId,
347    },
348    /// A persisted snapshot disagrees with the selected source or index root.
349    IndexSnapshotMismatch {
350        name: Vec<u8>,
351        source_version: MapVersionId,
352        reason: String,
353    },
354    /// A cursor belongs to a different immutable indexed snapshot.
355    IndexCursorVersionMismatch { expected: String, actual: String },
356    /// A destructive index GC sweep lacks a quiescence/lease safety proof.
357    IndexGcUnsafe,
358    /// Index work exceeded a configured resource bound.
359    IndexResourceLimitExceeded {
360        resource: &'static str,
361        limit: usize,
362        actual: usize,
363    },
364    /// A current indexed-snapshot bundle is malformed or inconsistent.
365    InvalidIndexedSnapshotBundle { reason: String },
366    /// A proximity-map shape configuration is invalid.
367    InvalidProximityConfig { reason: String },
368    /// Persisted proximity bytes use a format version this build does not read.
369    UnsupportedProximityVersion { found: u8, required: u8 },
370    /// A vector is incompatible with the proximity-map configuration.
371    InvalidProximityVector { reason: String },
372    /// Cosine distance cannot prepare a vector with zero Euclidean norm.
373    ZeroCosineVector,
374    /// A proximity build or mutation contains the same logical key twice.
375    DuplicateProximityKey { key: Vec<u8> },
376    /// Proximity search options are invalid.
377    InvalidProximitySearch { reason: String },
378    /// Proximity accelerator construction exceeded an explicit resource bound.
379    ProximityResourceLimitExceeded {
380        resource: &'static str,
381        limit: usize,
382        actual: usize,
383    },
384    /// A persisted proximity record, node, or descriptor is malformed.
385    InvalidProximityObject { kind: &'static str, reason: String },
386    /// One canonical proximity node exceeds the configured hard byte limit.
387    ProximityNodeTooLarge {
388        level: u8,
389        entries: usize,
390        encoded_bytes: usize,
391        limit: usize,
392    },
393    /// A typed content-graph traversal exceeded a configured resource limit.
394    ContentGraphResourceLimitExceeded {
395        resource: &'static str,
396        limit: usize,
397        actual: usize,
398    },
399}
400
401impl std::fmt::Debug for Error {
402    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403        // Error values routinely reach generic `Debug` logging through
404        // `unwrap`, tracing, and binding adapters. Keep that path on the same
405        // redacted contract as `Display`; raw keys, terms, cursor bounds, and
406        // extractor diagnostics remain available only in explicitly matched
407        // structured variants.
408        std::fmt::Display::fmt(self, f)
409    }
410}
411
412impl Error {
413    pub(crate) fn transaction_conflict(conflict: TransactionConflict) -> Self {
414        Self::TransactionConflict(Box::new(conflict))
415    }
416
417    pub fn index_code(&self) -> Option<IndexErrorCode> {
418        use IndexErrorCode as Code;
419        Some(match self {
420            Self::IndexFormatUnsupported => Code::FormatUnsupported,
421            Self::UnsupportedIndexedStoreProfile { .. } => Code::StoreProfileUnsupported,
422            Self::TransactionConflict(_) | Self::IndexBuildConflictLimitExceeded { .. } => {
423                Code::Conflict
424            }
425            Self::IndexSnapshotMismatch { .. } => Code::Corruption,
426            Self::InvalidIndexDefinition { .. } | Self::IndexDefinitionMismatch { .. } => {
427                Code::DefinitionInvalid
428            }
429            Self::IndexRuntimeDefinitionMissing { .. } => Code::RuntimeDefinitionMissing,
430            Self::IndexesRequireIndexedMap { .. } => Code::ManagedWriteRequired,
431            Self::IndexOperationUnsupported { .. } => Code::OperationUnsupported,
432            Self::IndexExtractionFailed { .. } => Code::ExtractionFailed,
433            Self::IndexProjectionMismatch { .. } | Self::ConflictingIndexProjection { .. } => {
434                Code::ProjectionInvalid
435            }
436            Self::IndexResourceLimitExceeded { .. } => Code::ResourceLimit,
437            Self::IndexCursorVersionMismatch { .. } => Code::CursorInvalid,
438            Self::IndexGcUnsafe => Code::OperationUnsupported,
439            Self::InvalidIndexedSnapshotBundle { .. } => Code::BundleInvalid,
440            Self::IndexUnavailableAtVersion { .. } => Code::SnapshotUnavailable,
441            _ => return None,
442        })
443    }
444
445    pub fn retry_advice(&self) -> RetryAdvice {
446        match self {
447            Self::TransactionConflict(_) | Self::IndexBuildConflictLimitExceeded { .. } => {
448                RetryAdvice::RetryFreshState
449            }
450            Self::Store(_) => RetryAdvice::RetryAfter,
451            _ => RetryAdvice::Never,
452        }
453    }
454}
455
456impl std::fmt::Display for Error {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        match self {
459            Error::NotFound(cid) => write!(f, "node not found: {:?}", cid),
460            Error::InvalidNode => write!(f, "invalid node structure"),
461            Error::InvalidFormat(message) => write!(f, "invalid tree format: {message}"),
462            Error::InvalidExecutionConfig { field, value } => {
463                write!(f, "invalid execution config: {field} must be nonzero, got {value}")
464            }
465            Error::FormatMismatch { expected, actual } => write!(
466                f,
467                "tree format mismatch: expected {:?}, got {:?}",
468                expected, actual
469            ),
470            Error::EntryTooLarge {
471                encoded_bytes,
472                limit,
473            } => write!(
474                f,
475                "entry encodes to {encoded_bytes} bytes, exceeding node limit {limit}"
476            ),
477            Error::Deserialize(e) => write!(f, "deserialize error: {}", e),
478            Error::Serialize(e) => write!(f, "serialize error: {}", e),
479            Error::Store(e) => write!(f, "storage error: {}", e),
480            Error::CidMismatch { expected, actual } => {
481                write!(
482                    f,
483                    "content CID mismatch: expected {:?}, got {:?}",
484                    expected, actual
485                )
486            }
487            Error::Conflict(c) => write!(f, "merge conflict at key: {:?}", c.key),
488            Error::BufferFull => write!(f, "mutation buffer is full"),
489            Error::InvalidSavepoint => write!(f, "invalid or stale write-session savepoint"),
490            Error::PatchBaseMismatch => write!(f, "patch base root or values do not match"),
491            Error::InvalidStructuralPatch(reason) => {
492                write!(f, "invalid structural patch: {reason}")
493            }
494            Error::UnsortedInput { previous, next } => write!(
495                f,
496                "sorted input keys are out of order: previous={:?} next={:?}",
497                previous, next
498            ),
499            Error::DuplicateMutation { key } => {
500                write!(f, "duplicate splice mutation: {key:?}")
501            }
502            Error::SpliceConfigMismatch => {
503                write!(f, "splice manager/tree configuration mismatch")
504            }
505            Error::MissingNamedRoots { names } => {
506                write!(f, "missing named roots for retention policy: {:?}", names)
507            }
508            Error::InvalidSnapshotBundle(message) => {
509                write!(f, "invalid snapshot bundle: {message}")
510            }
511            Error::UnsupportedTransactions { store } => {
512                write!(f, "store does not support strict transactions: {store}")
513            }
514            Error::UnsupportedIndexedStoreProfile {
515                store,
516                required,
517                actual,
518            } => write!(
519                f,
520                "store {store} does not satisfy indexed-store requirement {required}: {actual}"
521            ),
522            Error::TransactionConflict(conflict) => {
523                write!(
524                    f,
525                    "transaction conflict for named root: {:?}",
526                    conflict.name
527                )
528            }
529            Error::InvalidVersionedMap(message) => {
530                write!(f, "invalid versioned map: {message}")
531            }
532            Error::IndexFormatUnsupported => {
533                write!(f, "secondary index format is unsupported; hard cutover required")
534            }
535            Error::InvalidIndexDefinition { .. } => {
536                write!(f, "invalid secondary index definition")
537            }
538            Error::IndexRuntimeDefinitionMissing { generation, .. } => write!(
539                f,
540                "runtime secondary index definition missing: generation={generation}"
541            ),
542            Error::IndexDefinitionMismatch { .. } => {
543                write!(f, "secondary index definition mismatch")
544            }
545            Error::IndexesRequireIndexedMap { .. } => {
546                write!(f, "managed map requires IndexedMap coordinator")
547            }
548            Error::IndexOperationUnsupported { operation } => {
549                write!(f, "indexed map operation is unsupported: {operation}")
550            }
551            Error::IndexExtractionFailed { .. } => {
552                write!(f, "secondary index extraction failed")
553            }
554            Error::IndexProjectionMismatch { mode, .. } => {
555                write!(f, "secondary index projection mismatch: mode={mode:?}")
556            }
557            Error::ConflictingIndexProjection { .. } => {
558                write!(f, "conflicting secondary index projection")
559            }
560            Error::IndexBuildConflictLimitExceeded { attempts, .. } => write!(
561                f,
562                "secondary index conflict limit exceeded: attempts={attempts}"
563            ),
564            Error::IndexUnavailableAtVersion { .. } => {
565                write!(f, "secondary index unavailable at selected snapshot")
566            }
567            Error::IndexSnapshotMismatch { .. } => {
568                write!(f, "secondary index snapshot closure is inconsistent")
569            }
570            Error::IndexCursorVersionMismatch { .. } => {
571                write!(f, "secondary index cursor does not match the query snapshot")
572            }
573            Error::IndexGcUnsafe => {
574                write!(f, "secondary index garbage collection lacks a safety proof")
575            }
576            Error::IndexResourceLimitExceeded {
577                resource,
578                limit,
579                actual,
580            } => write!(
581                f,
582                "secondary index resource limit exceeded: resource={resource} limit={limit} actual={actual}"
583            ),
584            Error::InvalidIndexedSnapshotBundle { .. } => {
585                write!(f, "invalid indexed snapshot bundle")
586            }
587            Error::InvalidProximityConfig { reason } => {
588                write!(f, "invalid proximity configuration: {reason}")
589            }
590            Error::UnsupportedProximityVersion { found, required } => write!(
591                f,
592                "unsupported proximity format version: found={found} required={required}"
593            ),
594            Error::InvalidProximityVector { reason } => {
595                write!(f, "invalid proximity vector: {reason}")
596            }
597            Error::ZeroCosineVector => write!(f, "cosine proximity vector has zero norm"),
598            Error::DuplicateProximityKey { key } => {
599                write!(f, "duplicate proximity key: {key:?}")
600            }
601            Error::InvalidProximitySearch { reason } => {
602                write!(f, "invalid proximity search options: {reason}")
603            }
604            Error::ProximityResourceLimitExceeded {
605                resource,
606                limit,
607                actual,
608            } => write!(
609                f,
610                "proximity accelerator resource limit exceeded: resource={resource} limit={limit} actual={actual}"
611            ),
612            Error::InvalidProximityObject { kind, reason } => {
613                write!(f, "invalid proximity {kind}: {reason}")
614            }
615            Error::ProximityNodeTooLarge {
616                level,
617                entries,
618                encoded_bytes,
619                limit,
620            } => write!(
621                f,
622                "proximity node exceeds byte limit: level={level} entries={entries} bytes={encoded_bytes} limit={limit}"
623            ),
624            Error::ContentGraphResourceLimitExceeded {
625                resource,
626                limit,
627                actual,
628            } => write!(
629                f,
630                "content graph resource limit exceeded: resource={resource} limit={limit} actual={actual}"
631            ),
632        }
633    }
634}
635
636impl std::error::Error for Error {}