Skip to main content

oxgraph_postgres/
error.rs

1//! Shared error surface for the Postgres graph engine.
2
3use core::fmt;
4
5use oxgraph_csc::CscSnapshotError;
6use oxgraph_csr::CsrSnapshotError;
7use oxgraph_snapshot::{PlanError, SnapshotError};
8
9use crate::{catalog::CatalogError, role::GraphRole};
10
11/// Relational build or snapshot export failures.
12#[derive(Debug)]
13pub enum BuildError {
14    /// Snapshot planning failed.
15    Plan(PlanError),
16    /// CSR construction failed.
17    Graph(oxgraph_csr::build::GraphBuildError<u32, u32>),
18    /// Engine build was invoked without snapshot bytes.
19    MissingSnapshotBytes,
20    /// Build received no edge rows.
21    EmptyEdges,
22    /// Distinct node count does not fit in `u32` metadata.
23    NodeCountOverflow,
24    /// Edge count does not fit in `u32` metadata.
25    EdgeCountOverflow,
26    /// Forward and inbound edge counts diverged after transpose.
27    EdgeCountMismatch,
28    /// An edge row referenced a node key that was not assigned.
29    MissingNodeKey,
30    /// Required CSR section was absent during snapshot assembly.
31    MissingCsrSection {
32        /// Section kind that was expected.
33        kind: u32,
34    },
35    /// Postgres metadata section was missing at engine open.
36    MissingMetadataSection,
37    /// Section payload could not be interpreted.
38    MalformedMetadata(alloc::string::String),
39    /// Artifact metadata lacks the inbound CSC flag.
40    MissingReverseIndex,
41    /// Forward and inbound node counts disagree at engine open.
42    TopologyNodeCountMismatch,
43    /// Forward and inbound edge counts disagree at engine open.
44    TopologyEdgeCountMismatch,
45    /// Forward node count disagrees with metadata.
46    MetadataNodeCountMismatch,
47    /// Forward edge count disagrees with metadata.
48    MetadataEdgeCountMismatch,
49    /// SPI or extension boundary failure.
50    Spi(alloc::string::String),
51}
52
53impl fmt::Display for BuildError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::Plan(error) => write!(f, "plan error: {error}"),
57            Self::Graph(error) => write!(f, "graph build error: {error}"),
58            Self::MissingSnapshotBytes => f.write_str("snapshot bytes required"),
59            Self::EmptyEdges => f.write_str("build requires at least one edge row"),
60            Self::NodeCountOverflow => f.write_str("node count does not fit in u32 metadata"),
61            Self::EdgeCountOverflow => f.write_str("edge count does not fit in u32 metadata"),
62            Self::EdgeCountMismatch | Self::TopologyEdgeCountMismatch => {
63                f.write_str("forward and inbound edge counts diverged")
64            }
65            Self::MissingNodeKey => f.write_str("edge row references missing node key"),
66            Self::MissingCsrSection { kind } => {
67                write!(f, "missing CSR section {kind:#06x}")
68            }
69            Self::MissingMetadataSection => f.write_str("postgres metadata section missing"),
70            Self::MalformedMetadata(message) => write!(f, "postgres metadata layout: {message}"),
71            Self::MissingReverseIndex => {
72                f.write_str("artifact missing HAS_REVERSE_INDEX metadata flag")
73            }
74            Self::TopologyNodeCountMismatch => {
75                f.write_str("forward and inbound node counts diverged")
76            }
77            Self::MetadataNodeCountMismatch => {
78                f.write_str("forward node count disagrees with metadata")
79            }
80            Self::MetadataEdgeCountMismatch => {
81                f.write_str("forward edge count disagrees with metadata")
82            }
83            Self::Spi(message) => write!(f, "spi: {message}"),
84        }
85    }
86}
87
88impl core::error::Error for BuildError {}
89
90/// Query input or limit failures.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum QueryError {
93    /// BFS seed is outside the canonical node bound.
94    SeedOutOfBounds {
95        /// Requested seed id.
96        seed: u32,
97        /// Canonical node count.
98        node_count: u32,
99    },
100    /// A required limit was zero.
101    LimitZero,
102    /// Dense node iteration overflowed `u32`.
103    NodeIndexOverflow,
104    /// Internal kernel invariant violated.
105    InternalInvariant(&'static str),
106}
107
108impl fmt::Display for QueryError {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            Self::SeedOutOfBounds { seed, node_count } => {
112                write!(f, "seed {seed} out of bounds (node_count {node_count})")
113            }
114            Self::LimitZero => f.write_str("limit must be > 0"),
115            Self::NodeIndexOverflow => f.write_str("node index does not fit in u32"),
116            Self::InternalInvariant(message) => write!(f, "internal query invariant: {message}"),
117        }
118    }
119}
120
121impl core::error::Error for QueryError {}
122
123/// Sync replay ordering failures.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum SyncError {
126    /// Sequence numbers were not strictly increasing.
127    NonMonotonicSequence {
128        /// Offending sequence.
129        sequence: u64,
130        /// Previous sequence in the batch.
131        previous: u64,
132    },
133    /// Persisted action type id is not recognized.
134    InvalidActionType {
135        /// Raw action type from the sync log.
136        action_type: i16,
137    },
138    /// Action arguments could not be decoded for a known action type.
139    InvalidActionArgs {
140        /// Raw action type from the sync log.
141        action_type: i16,
142    },
143    /// A keyed sync action referenced a node key absent from the current scan.
144    UnknownNodeKey {
145        /// Unassigned registered node key.
146        key: crate::catalog::NodeKey,
147    },
148}
149
150impl fmt::Display for SyncError {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        match self {
153            Self::NonMonotonicSequence { sequence, previous } => {
154                write!(f, "non-monotonic sync sequence {sequence} after {previous}")
155            }
156            Self::InvalidActionType { action_type } => {
157                write!(f, "invalid sync action type {action_type}")
158            }
159            Self::InvalidActionArgs { action_type } => {
160                write!(f, "invalid sync action arguments for type {action_type}")
161            }
162            Self::UnknownNodeKey { key } => write!(f, "unknown sync node key {key}"),
163        }
164    }
165}
166
167impl core::error::Error for SyncError {}
168
169/// Operational configuration failures.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum ConfigError {
172    /// Traverse limit GUC was zero.
173    ZeroTraverseLimit,
174    /// Search limit GUC was zero.
175    ZeroSearchLimit,
176    /// Maintenance rebuild is disabled by policy.
177    MaintenanceDisabled,
178}
179
180impl fmt::Display for ConfigError {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        match self {
183            Self::ZeroTraverseLimit => f.write_str("traverse_limit must be > 0"),
184            Self::ZeroSearchLimit => f.write_str("search_limit must be > 0"),
185            Self::MaintenanceDisabled => f.write_str("maintenance rebuild disabled by config"),
186        }
187    }
188}
189
190impl core::error::Error for ConfigError {}
191
192/// Errors returned by the Postgres graph engine library.
193///
194/// Concrete variants only — no boxed or dynamic error types on the public surface.
195#[derive(Debug)]
196pub enum PostgresGraphError {
197    /// Snapshot bytes failed container validation.
198    Snapshot(SnapshotError),
199    /// Forward CSR topology sections could not be opened.
200    ForwardSnapshot(CsrSnapshotError<u32, u32>),
201    /// Inbound CSC topology sections could not be opened.
202    InboundSnapshot(CscSnapshotError),
203    /// No graph engine is loaded in this session.
204    NotLoaded,
205    /// Relational build or export failed.
206    Build(BuildError),
207    /// Catalog registration is inconsistent or incomplete.
208    Catalog(CatalogError),
209    /// Query limits or inputs were invalid.
210    Query(QueryError),
211    /// Sync replay rejected a row or batch.
212    Sync(SyncError),
213    /// Operational configuration was invalid.
214    Config(ConfigError),
215    /// Access control policy denied an operation.
216    AccessDenied {
217        /// Role required for the operation.
218        required: GraphRole,
219        /// Role observed in the session.
220        actual: GraphRole,
221    },
222}
223
224impl fmt::Display for PostgresGraphError {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            Self::Snapshot(error) => write!(f, "snapshot error: {error}"),
228            Self::ForwardSnapshot(error) => write!(f, "forward snapshot error: {error}"),
229            Self::InboundSnapshot(error) => write!(f, "inbound snapshot error: {error}"),
230            Self::NotLoaded => f.write_str("graph engine not loaded"),
231            Self::Build(error) => write!(f, "build error: {error}"),
232            Self::Catalog(error) => write!(f, "catalog error: {error}"),
233            Self::Query(error) => write!(f, "query error: {error}"),
234            Self::Sync(error) => write!(f, "sync error: {error}"),
235            Self::Config(error) => write!(f, "config error: {error}"),
236            Self::AccessDenied { required, actual } => {
237                write!(f, "access denied: {actual:?} cannot satisfy {required:?}")
238            }
239        }
240    }
241}
242
243impl core::error::Error for PostgresGraphError {}
244
245impl From<SnapshotError> for PostgresGraphError {
246    fn from(error: SnapshotError) -> Self {
247        Self::Snapshot(error)
248    }
249}
250
251impl From<CsrSnapshotError<u32, u32>> for PostgresGraphError {
252    fn from(error: CsrSnapshotError<u32, u32>) -> Self {
253        Self::ForwardSnapshot(error)
254    }
255}
256
257impl From<CscSnapshotError> for PostgresGraphError {
258    fn from(error: CscSnapshotError) -> Self {
259        Self::InboundSnapshot(error)
260    }
261}
262
263impl From<PlanError> for PostgresGraphError {
264    fn from(error: PlanError) -> Self {
265        Self::Build(BuildError::Plan(error))
266    }
267}
268
269impl From<oxgraph_csr::build::GraphBuildError<u32, u32>> for PostgresGraphError {
270    fn from(error: oxgraph_csr::build::GraphBuildError<u32, u32>) -> Self {
271        Self::Build(BuildError::Graph(error))
272    }
273}
274
275impl From<CatalogError> for PostgresGraphError {
276    fn from(error: CatalogError) -> Self {
277        Self::Catalog(error)
278    }
279}
280
281impl From<BuildError> for PostgresGraphError {
282    fn from(error: BuildError) -> Self {
283        Self::Build(error)
284    }
285}
286
287impl From<QueryError> for PostgresGraphError {
288    fn from(error: QueryError) -> Self {
289        Self::Query(error)
290    }
291}
292
293impl From<SyncError> for PostgresGraphError {
294    fn from(error: SyncError) -> Self {
295        Self::Sync(error)
296    }
297}
298
299impl From<ConfigError> for PostgresGraphError {
300    fn from(error: ConfigError) -> Self {
301        Self::Config(error)
302    }
303}