Skip to main content

oxgraph_db/
error.rs

1//! Db error surface.
2//!
3//! [`DbError`] is the single error type on the public API. It composes four
4//! subsystem enums — [`StorageError`], [`CatalogError`], [`TxnError`], and
5//! [`QueryError`] — so callers can match on the failing subsystem while
6//! internal code constructs the precise variant and `?`-converts via [`From`].
7
8use std::{fmt, io};
9
10use crate::{PropertyKeyId, catalog::PropertyFamily, value::PropertyType};
11
12/// Canonical id family, for errors that name one.
13///
14/// # Performance
15///
16/// Copying, comparing, and formatting are `O(1)`.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum IdFamily {
19    /// Canonical element ids.
20    Element,
21    /// Canonical relation ids.
22    Relation,
23    /// Canonical incidence ids.
24    Incidence,
25    /// Structural role ids.
26    Role,
27    /// Catalog label ids.
28    Label,
29    /// Catalog relation-type ids.
30    RelationType,
31    /// Catalog property-key ids.
32    PropertyKey,
33    /// Catalog projection ids.
34    Projection,
35    /// Catalog index ids.
36    Index,
37}
38
39impl fmt::Display for IdFamily {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        formatter.write_str(match self {
42            Self::Element => "element",
43            Self::Relation => "relation",
44            Self::Incidence => "incidence",
45            Self::Role => "role",
46            Self::Label => "label",
47            Self::RelationType => "relation type",
48            Self::PropertyKey => "property key",
49            Self::Projection => "projection",
50            Self::Index => "index",
51        })
52    }
53}
54
55/// Errors from the persistence layer: db files, the superblock, the base
56/// store format, and the delta log.
57///
58/// # Performance
59///
60/// Formatting is `O(message length)`.
61#[derive(Debug)]
62#[non_exhaustive]
63pub enum StorageError {
64    /// Db files already exist.
65    AlreadyExists,
66    /// Db files do not exist.
67    NotFound,
68    /// Wraps an IO error with operation context.
69    Io {
70        /// Operation that failed.
71        operation: &'static str,
72        /// Underlying IO error.
73        source: io::Error,
74    },
75    /// Storage bytes are invalid.
76    InvalidStore {
77        /// Deterministic validation message.
78        message: String,
79    },
80    /// The store's OXGDB format version is not supported by this build. A base
81    /// written under an older format (for example one lacking the persisted
82    /// `SECTION_INDEX_*` postings) is rejected here rather than silently rebuilt.
83    UnsupportedFormat {
84        /// Format version recorded in the store.
85        found: u32,
86        /// Format version this build requires.
87        expected: u32,
88    },
89    /// A delta-log record is corrupt beyond the recoverable torn tail.
90    LogCorrupt {
91        /// Log sequence number of the offending record.
92        lsn: u64,
93        /// Deterministic reason the record was rejected.
94        reason: &'static str,
95    },
96    /// A delta-log record names a different base generation than the superblock.
97    BaseGenerationMismatch {
98        /// Base generation named by the superblock.
99        expected: u64,
100        /// Base generation found in the record.
101        found: u64,
102    },
103}
104
105impl StorageError {
106    /// Creates an IO error with operation context.
107    ///
108    /// # Performance
109    ///
110    /// This function is `O(1)`.
111    pub(crate) const fn io(operation: &'static str, source: io::Error) -> Self {
112        Self::Io { operation, source }
113    }
114
115    /// Creates an invalid-store error.
116    ///
117    /// # Performance
118    ///
119    /// This function is `O(message.len())`.
120    pub(crate) fn invalid_store(message: impl Into<String>) -> Self {
121        Self::InvalidStore {
122            message: message.into(),
123        }
124    }
125}
126
127impl fmt::Display for StorageError {
128    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            Self::AlreadyExists => formatter.write_str("database already exists"),
131            Self::NotFound => formatter.write_str("database not found"),
132            Self::Io { operation, source } => write!(formatter, "{operation} failed: {source}"),
133            Self::InvalidStore { message } => write!(formatter, "invalid store: {message}"),
134            Self::UnsupportedFormat { found, expected } => write!(
135                formatter,
136                "unsupported OXGDB format version: found {found}, this build requires {expected}"
137            ),
138            Self::LogCorrupt { lsn, reason } => {
139                write!(formatter, "delta-log corrupt at lsn {lsn}: {reason}")
140            }
141            Self::BaseGenerationMismatch { expected, found } => write!(
142                formatter,
143                "base generation mismatch: superblock names {expected}, record has {found}"
144            ),
145        }
146    }
147}
148
149impl std::error::Error for StorageError {
150    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
151        match self {
152            Self::Io { source, .. } => Some(source),
153            Self::AlreadyExists
154            | Self::NotFound
155            | Self::InvalidStore { .. }
156            | Self::UnsupportedFormat { .. }
157            | Self::LogCorrupt { .. }
158            | Self::BaseGenerationMismatch { .. } => None,
159        }
160    }
161}
162
163/// Errors from catalog identity: canonical ids, catalog names, and declared
164/// schema shapes.
165///
166/// # Performance
167///
168/// Formatting is `O(message length)`.
169#[derive(Debug)]
170#[non_exhaustive]
171pub enum CatalogError {
172    /// A referenced canonical id is not present.
173    UnknownId {
174        /// Id family that was looked up.
175        family: IdFamily,
176        /// Raw canonical id value that was absent.
177        id: u64,
178    },
179    /// A catalog name was not found in a bound schema.
180    UnknownName {
181        /// The kind of catalog entry (for example `"role"` or `"property key"`).
182        kind: &'static str,
183        /// The name that was not found.
184        name: String,
185    },
186    /// Duplicate catalog name or ID.
187    DuplicateName,
188    /// Duplicate canonical ID.
189    DuplicateId,
190    /// A declared schema item conflicts with an existing catalog entry.
191    SchemaConflict {
192        /// The conflicting catalog name.
193        name: String,
194        /// Deterministic reason the declaration conflicts with the catalog.
195        reason: &'static str,
196    },
197}
198
199impl fmt::Display for CatalogError {
200    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            Self::UnknownId { family, id } => write!(formatter, "unknown {family} {id}"),
203            Self::UnknownName { kind, name } => write!(formatter, "unknown {kind} {name:?}"),
204            Self::DuplicateName => formatter.write_str("duplicate catalog name"),
205            Self::DuplicateId => formatter.write_str("duplicate ID"),
206            Self::SchemaConflict { name, reason } => {
207                write!(formatter, "schema conflict for {name:?}: {reason}")
208            }
209        }
210    }
211}
212
213impl std::error::Error for CatalogError {}
214
215/// Errors from the transaction lifecycle: the single-writer lock and id and
216/// sequence allocation.
217///
218/// # Performance
219///
220/// Formatting is `O(1)`.
221#[derive(Debug)]
222#[non_exhaustive]
223pub enum TxnError {
224    /// The single-writer lock is already held by another writer.
225    WriterLockHeld,
226    /// Canonical ID space is exhausted.
227    IdOverflow {
228        /// Id family whose space is exhausted.
229        family: IdFamily,
230    },
231    /// Transaction ID space is exhausted.
232    TransactionIdOverflow,
233    /// Commit sequence space is exhausted.
234    CommitSeqOverflow,
235}
236
237impl fmt::Display for TxnError {
238    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
239        match self {
240            Self::WriterLockHeld => formatter.write_str("database writer lock is held"),
241            Self::IdOverflow { family } => write!(formatter, "database {family} ID overflow"),
242            Self::TransactionIdOverflow => formatter.write_str("transaction ID overflow"),
243            Self::CommitSeqOverflow => formatter.write_str("commit sequence overflow"),
244        }
245    }
246}
247
248impl std::error::Error for TxnError {}
249
250/// Errors from query and read validation: query text, property schema checks,
251/// projections, and traversals.
252///
253/// # Performance
254///
255/// Formatting is `O(message length)`.
256#[derive(Debug)]
257#[non_exhaustive]
258pub enum QueryError {
259    /// Query text is empty.
260    Empty,
261    /// Query text is outside the pinned profile.
262    Unsupported {
263        /// Deterministic explanation.
264        message: String,
265    },
266    /// Property value type mismatched the catalog schema.
267    PropertyTypeMismatch {
268        /// Expected property type.
269        expected: PropertyType,
270        /// Actual property type.
271        actual: PropertyType,
272    },
273    /// Property subject family mismatched the catalog schema.
274    WrongPropertyFamily {
275        /// Expected subject family.
276        expected: PropertyFamily,
277        /// Actual subject family.
278        actual: PropertyFamily,
279    },
280    /// Projection cannot be materialized as requested.
281    InvalidProjection {
282        /// Deterministic validation message.
283        message: String,
284    },
285    /// A bounded traversal failed.
286    Traversal {
287        /// Deterministic reason the traversal failed.
288        reason: &'static str,
289    },
290    /// A required property was absent from a subject.
291    MissingProperty {
292        /// The property key that was required but absent.
293        key: PropertyKeyId,
294    },
295    /// A numeric value was outside the representable `i64` range.
296    ValueOutOfRange,
297    /// A property key has no associated equality index.
298    NoEqualityIndex {
299        /// The property key lacking an equality index.
300        key: PropertyKeyId,
301    },
302}
303
304impl fmt::Display for QueryError {
305    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
306        match self {
307            Self::Empty => formatter.write_str("empty query"),
308            Self::Unsupported { message } => write!(formatter, "unsupported query: {message}"),
309            Self::PropertyTypeMismatch { expected, actual } => {
310                write!(
311                    formatter,
312                    "property type mismatch: expected {expected:?}, got {actual:?}"
313                )
314            }
315            Self::WrongPropertyFamily { expected, actual } => {
316                write!(
317                    formatter,
318                    "property family mismatch: expected {expected:?}, got {actual:?}"
319                )
320            }
321            Self::InvalidProjection { message } => {
322                write!(formatter, "invalid projection: {message}")
323            }
324            Self::Traversal { reason } => write!(formatter, "traversal error: {reason}"),
325            Self::MissingProperty { key } => {
326                write!(formatter, "missing property {}", key.get())
327            }
328            Self::ValueOutOfRange => formatter.write_str("value out of representable i64 range"),
329            Self::NoEqualityIndex { key } => {
330                write!(
331                    formatter,
332                    "no equality index for property key {}",
333                    key.get()
334                )
335            }
336        }
337    }
338}
339
340impl std::error::Error for QueryError {}
341
342/// Errors raised by the `OxGraph` database product.
343///
344/// # Performance
345///
346/// Formatting is `O(message length)`.
347#[derive(Debug)]
348#[non_exhaustive]
349pub enum DbError {
350    /// Persistence-layer failure (files, superblock, base format, delta log).
351    Storage(StorageError),
352    /// Catalog identity failure (canonical ids, names, schema shapes).
353    Catalog(CatalogError),
354    /// Transaction lifecycle failure (writer lock, id/sequence allocation).
355    Txn(TxnError),
356    /// Query or read validation failure.
357    Query(QueryError),
358}
359
360impl DbError {
361    /// Creates an IO error with operation context.
362    ///
363    /// # Performance
364    ///
365    /// This function is `O(1)`.
366    pub(crate) const fn io(operation: &'static str, source: io::Error) -> Self {
367        Self::Storage(StorageError::Io { operation, source })
368    }
369
370    /// Creates an unsupported-query error.
371    ///
372    /// # Performance
373    ///
374    /// This function is `O(message.len())`.
375    pub(crate) fn unsupported(message: impl Into<String>) -> Self {
376        Self::Query(QueryError::Unsupported {
377            message: message.into(),
378        })
379    }
380
381    /// Creates an invalid-projection error.
382    ///
383    /// # Performance
384    ///
385    /// This function is `O(message.len())`.
386    pub(crate) fn invalid_projection(message: impl Into<String>) -> Self {
387        Self::Query(QueryError::InvalidProjection {
388            message: message.into(),
389        })
390    }
391
392    /// Creates an invalid-store error.
393    ///
394    /// # Performance
395    ///
396    /// This function is `O(message.len())`.
397    pub(crate) fn invalid_store(message: impl Into<String>) -> Self {
398        Self::Storage(StorageError::InvalidStore {
399            message: message.into(),
400        })
401    }
402
403    /// Builds a traversal error from a deterministic reason.
404    ///
405    /// # Performance
406    ///
407    /// This function is `O(1)`.
408    pub(crate) const fn traversal(reason: &'static str) -> Self {
409        Self::Query(QueryError::Traversal { reason })
410    }
411
412    /// Builds an unknown-id error from any canonical id newtype.
413    ///
414    /// # Performance
415    ///
416    /// This function is `O(1)`.
417    pub(crate) fn unknown(id: impl Into<(IdFamily, u64)>) -> Self {
418        let (family, id) = id.into();
419        Self::Catalog(CatalogError::UnknownId { family, id })
420    }
421
422    /// Builds the id-exhaustion error for `family`'s allocator.
423    ///
424    /// # Performance
425    ///
426    /// This function is `O(1)`.
427    #[must_use]
428    pub(crate) const fn id_overflow(family: IdFamily) -> Self {
429        Self::Txn(TxnError::IdOverflow { family })
430    }
431}
432
433impl fmt::Display for DbError {
434    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
435        match self {
436            Self::Storage(error) => fmt::Display::fmt(error, formatter),
437            Self::Catalog(error) => fmt::Display::fmt(error, formatter),
438            Self::Txn(error) => fmt::Display::fmt(error, formatter),
439            Self::Query(error) => fmt::Display::fmt(error, formatter),
440        }
441    }
442}
443
444impl std::error::Error for DbError {
445    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
446        // Skip-level delegation: the chain stays `DbError -> io::Error` with no
447        // intermediate subsystem hop, matching the pre-split surface.
448        match self {
449            Self::Storage(error) => std::error::Error::source(error),
450            Self::Catalog(error) => std::error::Error::source(error),
451            Self::Txn(error) => std::error::Error::source(error),
452            Self::Query(error) => std::error::Error::source(error),
453        }
454    }
455}
456
457impl From<StorageError> for DbError {
458    fn from(error: StorageError) -> Self {
459        Self::Storage(error)
460    }
461}
462
463impl From<CatalogError> for DbError {
464    fn from(error: CatalogError) -> Self {
465        Self::Catalog(error)
466    }
467}
468
469impl From<TxnError> for DbError {
470    fn from(error: TxnError) -> Self {
471        Self::Txn(error)
472    }
473}
474
475impl From<QueryError> for DbError {
476    fn from(error: QueryError) -> Self {
477        Self::Query(error)
478    }
479}