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::CompilerError),
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    /// `Omnigraph::init` was called against a URI that already holds
96    /// schema artifacts from a previous init. Strict mode (the default)
97    /// fails fast with this error before touching disk so an existing
98    /// graph's metadata cannot be overwritten or destroyed. Operators
99    /// who actually want to overwrite pass `InitOptions { force: true }`
100    /// (CLI: `omnigraph init --force`).
101    #[error("graph already initialized at '{uri}'; pass --force to overwrite")]
102    AlreadyInitialized { uri: String },
103}
104
105impl OmniError {
106    pub fn manifest(message: impl Into<String>) -> Self {
107        Self::Manifest(ManifestError::new(ManifestErrorKind::BadRequest, message))
108    }
109
110    pub fn manifest_not_found(message: impl Into<String>) -> Self {
111        Self::Manifest(ManifestError::new(ManifestErrorKind::NotFound, message))
112    }
113
114    pub fn manifest_conflict(message: impl Into<String>) -> Self {
115        Self::Manifest(ManifestError::new(ManifestErrorKind::Conflict, message))
116    }
117
118    pub fn manifest_internal(message: impl Into<String>) -> Self {
119        Self::Manifest(ManifestError::new(ManifestErrorKind::Internal, message))
120    }
121
122    pub fn manifest_expected_version_mismatch(
123        table_key: impl Into<String>,
124        expected: u64,
125        actual: u64,
126    ) -> Self {
127        let table_key = table_key.into();
128        let message = format!(
129            "stale view of '{}': expected manifest table version {} but current is {} — refresh and retry",
130            table_key, expected, actual
131        );
132        Self::Manifest(
133            ManifestError::new(ManifestErrorKind::Conflict, message).with_details(
134                ManifestConflictDetails::ExpectedVersionMismatch {
135                    table_key,
136                    expected,
137                    actual,
138                },
139            ),
140        )
141    }
142
143    pub fn manifest_row_level_cas_contention(message: impl Into<String>) -> Self {
144        Self::Manifest(
145            ManifestError::new(ManifestErrorKind::Conflict, message)
146                .with_details(ManifestConflictDetails::RowLevelCasContention),
147        )
148    }
149}