1use crate::db_string::DbString;
4
5pub type CoreResult<T> = Result<T, CoreError>;
7
8#[derive(Debug, thiserror::Error, miette::Diagnostic)]
13#[non_exhaustive]
14pub enum CoreError {
15 #[error("string too long: {got} bytes (max {max})")]
17 #[diagnostic(code(SLENE_C_002))]
18 StringTooLong {
19 got: usize,
21 max: u32,
23 },
24
25 #[error("constructed value too large: {got} elements (max {max})")]
27 #[diagnostic(code(SLENE_C_003))]
28 ConstructedValueTooLarge {
29 got: usize,
31 max: u32,
33 },
34
35 #[error("decimal precision exceeded: {got} significant digits (max {max})")]
37 #[diagnostic(code(SLENE_C_004))]
38 DecimalPrecisionExceeded {
39 got: u32,
41 max: u32,
43 },
44
45 #[error("vector must contain at least one component")]
47 #[diagnostic(code(SLENE_C_010))]
48 VectorEmpty,
49
50 #[error("vector dimension too large: {got} components (max {max})")]
52 #[diagnostic(code(SLENE_C_011))]
53 VectorTooLarge {
54 got: usize,
56 max: usize,
58 },
59
60 #[error("vector component {index} is not finite: {value}")]
62 #[diagnostic(code(SLENE_C_012))]
63 VectorComponentNotFinite {
64 index: usize,
66 value: f32,
68 },
69
70 #[error("vector dimensions do not match: lhs has {lhs} components, rhs has {rhs}")]
72 #[diagnostic(code(SLENE_C_013))]
73 VectorDimensionMismatch {
74 lhs: usize,
76 rhs: usize,
78 },
79
80 #[error("cosine distance is undefined for zero-norm vector on {side}")]
82 #[diagnostic(code(SLENE_C_014))]
83 VectorZeroNorm {
84 side: &'static str,
86 },
87
88 #[error("invalid identifier: zero is reserved as tombstone sentinel")]
90 #[diagnostic(code(SLENE_C_007))]
91 ZeroIdentifier,
92
93 #[error("compact property map key/value length mismatch: {keys} keys, {values} values")]
95 #[diagnostic(code(SLENE_C_008))]
96 CompactKeyValueLengthMismatch {
97 keys: usize,
99 values: usize,
101 },
102
103 #[error("overlapping {kind} diff: key {key} appears in both add/set and remove")]
105 #[diagnostic(code(SLENE_C_009))]
106 OverlappingDiff {
107 kind: &'static str,
109 key: DbString,
111 },
112
113 #[error("invalid JSON text: {message}")]
115 #[diagnostic(code(SLENE_C_015))]
116 JsonParse {
117 message: String,
119 },
120
121 #[error("invalid JSON Patch: {message}")]
123 #[diagnostic(code(SLENE_C_016))]
124 JsonPatch {
125 message: String,
127 },
128}
129
130impl CoreError {
131 #[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}