Skip to main content

omnigraph/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, OmniError>;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ManifestErrorKind {
7    BadRequest,
8    NotFound,
9    Conflict,
10    Internal,
11}
12
13/// Structured details for a manifest-level conflict. Set on the `details`
14/// field of `ManifestError` when callers need to match on the specific
15/// concurrency-control failure rather than parse a string.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ManifestConflictDetails {
18    /// A caller-supplied per-table expected version did not match the
19    /// manifest's current latest non-tombstoned version for that table.
20    ExpectedVersionMismatch {
21        table_key: String,
22        expected: u64,
23        actual: u64,
24    },
25    /// Lance's row-level CAS rejected the publish because a concurrent writer
26    /// landed a row with the same `object_id`. Distinct from
27    /// `ExpectedVersionMismatch`: the caller's expectations (if any) still
28    /// hold against the new manifest state, so the publisher will retry.
29    RowLevelCasContention,
30}
31
32#[derive(Debug, Clone, Error)]
33#[error("{message}")]
34pub struct ManifestError {
35    pub kind: ManifestErrorKind,
36    pub message: String,
37    pub details: Option<ManifestConflictDetails>,
38}
39
40impl ManifestError {
41    pub fn new(kind: ManifestErrorKind, message: impl Into<String>) -> Self {
42        Self {
43            kind,
44            message: message.into(),
45            details: None,
46        }
47    }
48
49    pub fn with_details(mut self, details: ManifestConflictDetails) -> Self {
50        self.details = Some(details);
51        self
52    }
53}
54
55#[derive(Debug, Clone)]
56pub struct MergeConflict {
57    pub table_key: String,
58    pub row_id: Option<String>,
59    pub kind: MergeConflictKind,
60    pub message: String,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum MergeConflictKind {
65    DivergentInsert,
66    DivergentUpdate,
67    DeleteVsUpdate,
68    OrphanEdge,
69    UniqueViolation,
70    CardinalityViolation,
71    ValueConstraintViolation,
72}
73
74#[derive(Debug, Error)]
75pub enum OmniError {
76    #[error("{0}")]
77    Compiler(#[from] omnigraph_compiler::error::NanoError),
78    #[error("storage: {0}")]
79    Lance(String),
80    #[error("query: {0}")]
81    DataFusion(String),
82    #[error("io: {0}")]
83    Io(#[from] std::io::Error),
84    #[error("{0}")]
85    Manifest(ManifestError),
86    #[error("merge conflicts: {0:?}")]
87    MergeConflicts(Vec<MergeConflict>),
88    /// Engine-layer policy enforcement (MR-722). Wraps either a policy
89    /// denial ("you can't do that") or a policy-evaluation failure
90    /// ("the policy engine itself blew up"). The HTTP layer maps
91    /// denials to 403 and evaluation failures to 500; CLI and embedded
92    /// callers can match on this variant directly.
93    #[error("policy: {0}")]
94    Policy(String),
95}
96
97impl OmniError {
98    pub fn manifest(message: impl Into<String>) -> Self {
99        Self::Manifest(ManifestError::new(ManifestErrorKind::BadRequest, message))
100    }
101
102    pub fn manifest_not_found(message: impl Into<String>) -> Self {
103        Self::Manifest(ManifestError::new(ManifestErrorKind::NotFound, message))
104    }
105
106    pub fn manifest_conflict(message: impl Into<String>) -> Self {
107        Self::Manifest(ManifestError::new(ManifestErrorKind::Conflict, message))
108    }
109
110    pub fn manifest_internal(message: impl Into<String>) -> Self {
111        Self::Manifest(ManifestError::new(ManifestErrorKind::Internal, message))
112    }
113
114    pub fn manifest_expected_version_mismatch(
115        table_key: impl Into<String>,
116        expected: u64,
117        actual: u64,
118    ) -> Self {
119        let table_key = table_key.into();
120        let message = format!(
121            "stale view of '{}': expected manifest table version {} but current is {} — refresh and retry",
122            table_key, expected, actual
123        );
124        Self::Manifest(
125            ManifestError::new(ManifestErrorKind::Conflict, message).with_details(
126                ManifestConflictDetails::ExpectedVersionMismatch {
127                    table_key,
128                    expected,
129                    actual,
130                },
131            ),
132        )
133    }
134
135    pub fn manifest_row_level_cas_contention(message: impl Into<String>) -> Self {
136        Self::Manifest(
137            ManifestError::new(ManifestErrorKind::Conflict, message)
138                .with_details(ManifestConflictDetails::RowLevelCasContention),
139        )
140    }
141}