1use thiserror::Error;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ExtendedErrorCode {
10 None = 0,
11
12 SyntaxError = 1000,
14 SemanticError = 1001,
15 UnexpectedToken = 1002,
16 UnterminatedLiteral = 1003,
17
18 TypeMismatch = 1100,
20 DivisionByZero = 1101,
21 OutOfRange = 1102,
22
23 DuplicateKey = 1200,
25 ForeignKeyConstraint = 1201,
26 NotNullConstraint = 1202,
27 UniqueConstraint = 1203,
28 CheckConstraint = 1204,
29
30 ConnectionLost = 1300,
32 Deadlock = 1301,
33 LockTimeout = 1302,
34
35 InvalidVertex = 1400,
37 InvalidEdge = 1401,
38 PathNotFound = 1402,
39
40 Internal = 1500,
42}
43
44impl ExtendedErrorCode {
45 pub fn as_i32(&self) -> i32 {
46 *self as i32
47 }
48}
49
50#[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
139fn 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
169pub 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}