Skip to main content

selene_core/
error.rs

1//! Core error types and ISO GQLSTATUS mappings.
2
3use crate::db_string::DbString;
4
5/// Result alias for `selene-core` operations.
6pub type CoreResult<T> = Result<T, CoreError>;
7
8/// Error type for foundation data-model operations.
9///
10/// Codes in the `0Gxxx` range are selene-db implementation-defined conditions
11/// reserved for engine-internal validation and registry failures.
12#[derive(Debug, thiserror::Error, miette::Diagnostic)]
13#[non_exhaustive]
14pub enum CoreError {
15    /// A string or byte-string exceeded the implementation-defined length.
16    #[error("string too long: {got} bytes (max {max})")]
17    #[diagnostic(code(SLENE_C_002))]
18    StringTooLong {
19        /// Observed byte length.
20        got: usize,
21        /// Maximum byte length.
22        max: u32,
23    },
24
25    /// A list or record exceeded the implementation-defined cardinality.
26    #[error("constructed value too large: {got} elements (max {max})")]
27    #[diagnostic(code(SLENE_C_003))]
28    ConstructedValueTooLarge {
29        /// Observed element count.
30        got: usize,
31        /// Maximum element count.
32        max: u32,
33    },
34
35    /// A decimal exceeded the implementation-defined significant-digit precision.
36    #[error("decimal precision exceeded: {got} significant digits (max {max})")]
37    #[diagnostic(code(SLENE_C_004))]
38    DecimalPrecisionExceeded {
39        /// Observed significant-digit count.
40        got: u32,
41        /// Maximum significant-digit count.
42        max: u32,
43    },
44
45    /// A native dense vector was constructed without components.
46    #[error("vector must contain at least one component")]
47    #[diagnostic(code(SLENE_C_010))]
48    VectorEmpty,
49
50    /// A native dense vector exceeded the implementation-defined dimension cap.
51    #[error("vector dimension too large: {got} components (max {max})")]
52    #[diagnostic(code(SLENE_C_011))]
53    VectorTooLarge {
54        /// Observed component count.
55        got: usize,
56        /// Maximum component count.
57        max: usize,
58    },
59
60    /// A native dense vector component was NaN or infinite.
61    #[error("vector component {index} is not finite: {value}")]
62    #[diagnostic(code(SLENE_C_012))]
63    VectorComponentNotFinite {
64        /// Zero-based component index.
65        index: usize,
66        /// Rejected component value.
67        value: f32,
68    },
69
70    /// Two native dense vectors had incompatible dimensions for metric work.
71    #[error("vector dimensions do not match: lhs has {lhs} components, rhs has {rhs}")]
72    #[diagnostic(code(SLENE_C_013))]
73    VectorDimensionMismatch {
74        /// Left-hand vector dimension.
75        lhs: usize,
76        /// Right-hand vector dimension.
77        rhs: usize,
78    },
79
80    /// A cosine-distance vector had zero magnitude.
81    #[error("cosine distance is undefined for zero-norm vector on {side}")]
82    #[diagnostic(code(SLENE_C_014))]
83    VectorZeroNorm {
84        /// Which side of the comparison was zero magnitude.
85        side: &'static str,
86    },
87
88    /// Identifier value zero is reserved as the tombstone sentinel.
89    #[error("invalid identifier: zero is reserved as tombstone sentinel")]
90    #[diagnostic(code(SLENE_C_007))]
91    ZeroIdentifier,
92
93    /// Compact `PropertyMap` was constructed with mismatched key and value counts.
94    #[error("compact property map key/value length mismatch: {keys} keys, {values} values")]
95    #[diagnostic(code(SLENE_C_008))]
96    CompactKeyValueLengthMismatch {
97        /// Number of keys supplied.
98        keys: usize,
99        /// Number of value slots supplied.
100        values: usize,
101    },
102
103    /// A label diff or property diff named the same key in both add/set and remove.
104    #[error("overlapping {kind} diff: key {key} appears in both add/set and remove")]
105    #[diagnostic(code(SLENE_C_009))]
106    OverlappingDiff {
107        /// `"label"` or `"property"`.
108        kind: &'static str,
109        /// The contradicting key.
110        key: DbString,
111    },
112
113    /// JSON text could not be parsed.
114    #[error("invalid JSON text: {message}")]
115    #[diagnostic(code(SLENE_C_015))]
116    JsonParse {
117        /// Parser diagnostic.
118        message: String,
119    },
120
121    /// A JSON Patch document or operation is invalid.
122    #[error("invalid JSON Patch: {message}")]
123    #[diagnostic(code(SLENE_C_016))]
124    JsonPatch {
125        /// Patch diagnostic.
126        message: String,
127    },
128}
129
130impl CoreError {
131    /// Map this error to its 5-character ISO GQLSTATUS code.
132    ///
133    /// ISO/IEC 39075:2024 clause 23 defines the status-code shape. Spec 02
134    /// section 3.1 binds the value-limit and numeric-limit choices used here.
135    #[must_use]
136    pub const fn gqlstatus(&self) -> &'static str {
137        match self {
138            Self::StringTooLong { .. } | Self::ConstructedValueTooLarge { .. } => "22G03",
139            Self::DecimalPrecisionExceeded { .. } | Self::VectorComponentNotFinite { .. } => {
140                "22003"
141            }
142            Self::VectorEmpty | Self::VectorTooLarge { .. } => "22G03",
143            Self::VectorDimensionMismatch { .. } => "22G04",
144            Self::VectorZeroNorm { .. } => "22012",
145            Self::ZeroIdentifier => "0G003",
146            Self::CompactKeyValueLengthMismatch { .. } => "0G008",
147            Self::OverlappingDiff { .. } => "0G009",
148            Self::JsonParse { .. } => "22018",
149            Self::JsonPatch { .. } => "22G03",
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use miette::Diagnostic;
157    use rstest::rstest;
158
159    use super::*;
160
161    #[rstest]
162    #[case(CoreError::StringTooLong { got: 2, max: 1 }, "22G03", "SLENE_C_002")]
163    #[case(
164        CoreError::ConstructedValueTooLarge { got: 2, max: 1 },
165        "22G03",
166        "SLENE_C_003"
167    )]
168    #[case(
169        CoreError::DecimalPrecisionExceeded { got: 29, max: 28 },
170        "22003",
171        "SLENE_C_004"
172    )]
173    #[case(CoreError::VectorEmpty, "22G03", "SLENE_C_010")]
174    #[case(
175        CoreError::VectorTooLarge { got: 65_536, max: 65_535 },
176        "22G03",
177        "SLENE_C_011"
178    )]
179    #[case(
180        CoreError::VectorComponentNotFinite { index: 1, value: f32::INFINITY },
181        "22003",
182        "SLENE_C_012"
183    )]
184    #[case(
185        CoreError::VectorDimensionMismatch { lhs: 2, rhs: 3 },
186        "22G04",
187        "SLENE_C_013"
188    )]
189    #[case(
190        CoreError::VectorZeroNorm { side: "lhs" },
191        "22012",
192        "SLENE_C_014"
193    )]
194    #[case(CoreError::ZeroIdentifier, "0G003", "SLENE_C_007")]
195    #[case(
196        CoreError::CompactKeyValueLengthMismatch { keys: 2, values: 1 },
197        "0G008",
198        "SLENE_C_008"
199    )]
200    #[case(
201        CoreError::OverlappingDiff { kind: "label", key: crate::db_string("err.test.overlap").unwrap() },
202        "0G009",
203        "SLENE_C_009"
204    )]
205    #[case(
206        CoreError::JsonParse { message: "expected value".to_owned() },
207        "22018",
208        "SLENE_C_015"
209    )]
210    #[case(
211        CoreError::JsonPatch { message: "missing op".to_owned() },
212        "22G03",
213        "SLENE_C_016"
214    )]
215    fn gqlstatus_and_diagnostic_code_match(
216        #[case] error: CoreError,
217        #[case] gqlstatus: &str,
218        #[case] diagnostic_code: &str,
219    ) {
220        assert_eq!(error.gqlstatus(), gqlstatus);
221        assert!(
222            crate::gqlstatus_name(gqlstatus).is_some(),
223            "GQLSTATUS code {gqlstatus} emitted by CoreError but not in ALL_GQLSTATUS_NAMES"
224        );
225        assert_eq!(
226            error.code().map(|code| code.to_string()).as_deref(),
227            Some(diagnostic_code)
228        );
229    }
230
231    #[test]
232    fn display_includes_structured_field_values() {
233        let error = CoreError::StringTooLong { got: 7, max: 3 };
234        let rendered = error.to_string();
235        assert!(rendered.contains('7'));
236        assert!(rendered.contains('3'));
237    }
238}