Skip to main content

linkrs_api/api_core/
error.rs

1//! API Core Layer Error Types
2//!
3//! Business logic errors not related to the transport layer
4
5use thiserror::Error;
6
7/// Extended Error Code Types
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ExtendedErrorCode {
10    None = 0,
11
12    // Parsing Related (1000-1099)
13    SyntaxError = 1000,
14    SemanticError = 1001,
15    UnexpectedToken = 1002,
16    UnterminatedLiteral = 1003,
17
18    // Type-related (1100-1199)
19    TypeMismatch = 1100,
20    DivisionByZero = 1101,
21    OutOfRange = 1102,
22
23    // Binding related (1200-1299)
24    DuplicateKey = 1200,
25    ForeignKeyConstraint = 1201,
26    NotNullConstraint = 1202,
27    UniqueConstraint = 1203,
28    CheckConstraint = 1204,
29
30    // Concurrency-related (1300-1399)
31    ConnectionLost = 1300,
32    Deadlock = 1301,
33    LockTimeout = 1302,
34
35    // Figure correlation (1400-1499)
36    InvalidVertex = 1400,
37    InvalidEdge = 1401,
38    PathNotFound = 1402,
39
40    // Inside (1500-1599)
41    Internal = 1500,
42}
43
44impl ExtendedErrorCode {
45    pub fn as_i32(&self) -> i32 {
46        *self as i32
47    }
48}
49
50/// Core layer error types
51#[derive(Error, Debug, Clone)]
52pub enum CoreError {
53    #[error("Query execution failed: {0}")]
54    QueryExecutionFailed(String),
55
56    #[error("Transaction operation failed: {0}")]
57    TransactionFailed(String),
58
59    #[error("Schema operation failed: {0}")]
60    SchemaOperationFailed(String),
61
62    #[error("Storage error: {0}")]
63    StorageError(String),
64
65    #[error("Invalid parameter: {0}")]
66    InvalidParameter(String),
67
68    #[error("Resource not found: {0}")]
69    NotFound(String),
70
71    #[error("Internal error: {0}")]
72    Internal(String),
73
74    #[error("Query error: {message}")]
75    DetailedQueryError {
76        message: String,
77        extended_code: ExtendedErrorCode,
78        offset: Option<usize>,
79        position: Option<linkrs_core::types::Position>,
80    },
81
82    #[error("Sync error: {0}")]
83    SyncError(String),
84
85    #[error("Vector error: {0}")]
86    VectorError(String),
87}
88
89impl CoreError {
90    pub fn extended_code(&self) -> ExtendedErrorCode {
91        match self {
92            CoreError::DetailedQueryError { extended_code, .. } => *extended_code,
93            _ => ExtendedErrorCode::None,
94        }
95    }
96
97    pub fn error_offset(&self) -> Option<usize> {
98        match self {
99            CoreError::DetailedQueryError { offset, .. } => *offset,
100            _ => None,
101        }
102    }
103
104    pub fn error_position(&self) -> Option<linkrs_core::types::Position> {
105        match self {
106            CoreError::DetailedQueryError { position, .. } => *position,
107            _ => None,
108        }
109    }
110
111    pub fn detailed_query_error(
112        message: impl Into<String>,
113        extended_code: ExtendedErrorCode,
114        offset: Option<usize>,
115    ) -> Self {
116        CoreError::DetailedQueryError {
117            message: message.into(),
118            extended_code,
119            offset,
120            position: None,
121        }
122    }
123
124    pub fn detailed_query_error_with_position(
125        message: impl Into<String>,
126        extended_code: ExtendedErrorCode,
127        offset: Option<usize>,
128        position: Option<linkrs_core::types::Position>,
129    ) -> Self {
130        CoreError::DetailedQueryError {
131            message: message.into(),
132            extended_code,
133            offset,
134            position,
135        }
136    }
137}
138
139/// Map the engine's error taxonomy onto the wire-visible extended codes.
140///
141/// Every engine error carries a kind, so the extended code is always
142/// populated instead of collapsing to `None` for non-syntax failures.
143///
144/// The mapping is intentionally coarse: the wire codes are a small fixed
145/// taxonomy, so several kinds share one code. Resist the urge to grow
146/// `ExtendedErrorCode` to make individual kinds distinct.
147fn extended_code_from_query_kind(
148    kind: linkrs_core::error::query::QueryErrorKind,
149) -> ExtendedErrorCode {
150    use linkrs_core::error::query::QueryErrorKind;
151    match kind {
152        QueryErrorKind::Parse => ExtendedErrorCode::SyntaxError,
153        QueryErrorKind::Type => ExtendedErrorCode::TypeMismatch,
154        QueryErrorKind::Transaction => ExtendedErrorCode::Deadlock,
155        QueryErrorKind::Timeout => ExtendedErrorCode::LockTimeout,
156        QueryErrorKind::InvalidQuery
157        | QueryErrorKind::Permission
158        | QueryErrorKind::Session
159        | QueryErrorKind::FeatureDisabled => ExtendedErrorCode::SemanticError,
160        QueryErrorKind::Storage
161        | QueryErrorKind::Planning
162        | QueryErrorKind::Optimization
163        | QueryErrorKind::Execution
164        | QueryErrorKind::Expression
165        | QueryErrorKind::PlanNodeVisit => ExtendedErrorCode::Internal,
166    }
167}
168
169/// Core layer result types
170pub type CoreResult<T> = Result<T, CoreError>;
171
172impl From<linkrs_core::error::QueryError> for CoreError {
173    fn from(err: linkrs_core::error::QueryError) -> Self {
174        CoreError::detailed_query_error_with_position(
175            err.to_string(),
176            extended_code_from_query_kind(err.kind()),
177            err.offset(),
178            err.parse_error_position(),
179        )
180    }
181}
182
183impl From<linkrs_storage::StorageError> for CoreError {
184    fn from(err: linkrs_storage::StorageError) -> Self {
185        CoreError::StorageError(err.to_string())
186    }
187}
188
189impl From<linkrs_core::error::DBError> for CoreError {
190    fn from(err: linkrs_core::error::DBError) -> Self {
191        use linkrs_core::error::ErrorKind;
192        match err.kind() {
193            ErrorKind::Query => {
194                if let Some(source) = err.source() {
195                    if let Some(query_err) = source.downcast_ref::<linkrs_core::error::QueryError>()
196                    {
197                        return CoreError::from(query_err.clone());
198                    }
199                }
200                CoreError::QueryExecutionFailed(err.message().to_string())
201            }
202            ErrorKind::Storage => CoreError::StorageError(err.message().to_string()),
203            ErrorKind::Transaction => CoreError::TransactionFailed(err.message().to_string()),
204            _ => CoreError::Internal(err.to_string()),
205        }
206    }
207}
208
209impl From<linkrs_transaction::TransactionError> for CoreError {
210    fn from(err: linkrs_transaction::TransactionError) -> Self {
211        CoreError::TransactionFailed(err.to_string())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn query_error_kinds_map_to_distinct_extended_codes() {
221        use linkrs_core::error::query::QueryErrorKind;
222        let cases = [
223            (QueryErrorKind::Parse, ExtendedErrorCode::SyntaxError),
224            (QueryErrorKind::Type, ExtendedErrorCode::TypeMismatch),
225            (QueryErrorKind::Transaction, ExtendedErrorCode::Deadlock),
226            (QueryErrorKind::Timeout, ExtendedErrorCode::LockTimeout),
227            (
228                QueryErrorKind::InvalidQuery,
229                ExtendedErrorCode::SemanticError,
230            ),
231            (QueryErrorKind::Execution, ExtendedErrorCode::Internal),
232        ];
233        for (kind, expected) in cases {
234            let error = CoreError::from(linkrs_core::error::QueryError::new(kind, "boom"));
235            assert_eq!(error.extended_code(), expected, "kind {kind:?}");
236            assert!(
237                error.to_string().contains("boom"),
238                "message survives the mapping: {error}"
239            );
240        }
241    }
242
243    #[test]
244    fn converted_query_errors_are_always_detailed() {
245        let error = CoreError::from(linkrs_core::error::QueryError::new(
246            linkrs_core::error::query::QueryErrorKind::Type,
247            "type mismatch",
248        ));
249        assert_eq!(error.extended_code(), ExtendedErrorCode::TypeMismatch);
250        assert!(error.error_offset().is_none());
251        assert!(error.error_position().is_none());
252    }
253}