Skip to main content

nautilus_protocol/
error.rs

1//! Nautilus protocol errors with stable error codes.
2//!
3//! Error code ranges:
4//! - `1000..1999`: Schema / Validation errors
5//! - `2000..2999`: Query planning / rendering errors
6//! - `3000..3999`: Database execution errors
7//! - `9000..9999`: Internal engine errors
8//!
9//! Standard JSON-RPC errors (negative codes) are reserved for protocol-level issues.
10
11use serde::{Deserialize, Serialize};
12use serde_json::{json, Value};
13use thiserror::Error;
14
15use crate::wire::RpcError;
16
17pub const ERR_SCHEMA_VALIDATION: i32 = 1000;
18pub const ERR_INVALID_MODEL: i32 = 1001;
19pub const ERR_INVALID_FIELD: i32 = 1002;
20pub const ERR_TYPE_MISMATCH: i32 = 1003;
21
22pub const ERR_QUERY_PLANNING: i32 = 2000;
23pub const ERR_INVALID_FILTER: i32 = 2001;
24pub const ERR_INVALID_ORDERBY: i32 = 2002;
25pub const ERR_UNSUPPORTED_OPERATION: i32 = 2003;
26
27pub const ERR_DATABASE_EXECUTION: i32 = 3000;
28pub const ERR_CONNECTION_FAILED: i32 = 3001;
29pub const ERR_CONSTRAINT_VIOLATION: i32 = 3002;
30pub const ERR_QUERY_TIMEOUT: i32 = 3003;
31pub const ERR_RECORD_NOT_FOUND: i32 = 3004;
32pub const ERR_UNIQUE_CONSTRAINT: i32 = 3005;
33pub const ERR_FOREIGN_KEY_CONSTRAINT: i32 = 3006;
34pub const ERR_CHECK_CONSTRAINT: i32 = 3007;
35pub const ERR_NULL_CONSTRAINT: i32 = 3008;
36pub const ERR_DEADLOCK: i32 = 3009;
37pub const ERR_SERIALIZATION_FAILURE: i32 = 3010;
38
39pub const ERR_TRANSACTION_NOT_FOUND: i32 = 4001;
40pub const ERR_TRANSACTION_TIMEOUT: i32 = 4002;
41pub const ERR_TRANSACTION_ALREADY_CLOSED: i32 = 4003;
42pub const ERR_TRANSACTION_FAILED: i32 = 4004;
43
44pub const ERR_INTERNAL: i32 = 9000;
45pub const ERR_UNSUPPORTED_PROTOCOL_VERSION: i32 = 9001;
46pub const ERR_INVALID_METHOD: i32 = 9002;
47pub const ERR_INVALID_REQUEST_PARAMS: i32 = 9003;
48
49/// The original error returned by a failed protocol operation.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct ProtocolErrorCause {
53    pub code: i32,
54    pub message: String,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub data: Option<Value>,
57}
58
59/// Structured `error.data` payload emitted when `transaction.batch` fails.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct BatchOperationErrorData {
63    pub batch_operation_index: usize,
64    pub batch_operation_method: String,
65    pub cause: ProtocolErrorCause,
66}
67
68/// Nautilus protocol error.
69#[derive(Debug, Error)]
70pub enum ProtocolError {
71    #[error("Schema validation error: {0}")]
72    SchemaValidation(String),
73
74    #[error("Invalid model: {0}")]
75    InvalidModel(String),
76
77    #[error("Invalid field: {0}")]
78    InvalidField(String),
79
80    #[error("Type mismatch: {0}")]
81    TypeMismatch(String),
82
83    #[error("Query planning error: {0}")]
84    QueryPlanning(String),
85
86    #[error("Invalid filter: {0}")]
87    InvalidFilter(String),
88
89    #[error("Invalid order by: {0}")]
90    InvalidOrderBy(String),
91
92    #[error("Unsupported operation: {0}")]
93    UnsupportedOperation(String),
94
95    #[error("Database execution error: {0}")]
96    DatabaseExecution(String),
97
98    #[error("Connection failed: {0}")]
99    ConnectionFailed(String),
100
101    #[error("Constraint violation: {0}")]
102    ConstraintViolation(String),
103
104    #[error("Unique constraint violation: {0}")]
105    UniqueConstraintViolation(String),
106
107    #[error("Foreign key constraint violation: {0}")]
108    ForeignKeyConstraintViolation(String),
109
110    #[error("Check constraint violation: {0}")]
111    CheckConstraintViolation(String),
112
113    #[error("NOT NULL constraint violation: {0}")]
114    NullConstraintViolation(String),
115
116    #[error("Deadlock detected: {0}")]
117    Deadlock(String),
118
119    #[error("Serialization failure: {0}")]
120    SerializationFailure(String),
121
122    #[error("Query timeout: {0}")]
123    QueryTimeout(String),
124
125    #[error("Record not found: {0}")]
126    RecordNotFound(String),
127
128    #[error("Transaction not found: {0}")]
129    TransactionNotFound(String),
130
131    #[error("Transaction timed out: {0}")]
132    TransactionTimeout(String),
133
134    #[error("Transaction already closed: {0}")]
135    TransactionAlreadyClosed(String),
136
137    #[error("Transaction failed: {0}")]
138    TransactionFailed(String),
139
140    #[error("{source}")]
141    BatchOperationFailed {
142        index: usize,
143        method: String,
144        #[source]
145        source: Box<ProtocolError>,
146    },
147
148    #[error("Internal engine error: {0}")]
149    Internal(String),
150
151    #[error("Unsupported protocol version: {actual}, expected {expected}")]
152    UnsupportedProtocolVersion { actual: u32, expected: u32 },
153
154    #[error("Invalid method: {0}")]
155    InvalidMethod(String),
156
157    #[error("Invalid request params: {0}")]
158    InvalidParams(String),
159
160    #[error("Serialization error: {0}")]
161    Serialization(String),
162}
163
164impl ProtocolError {
165    /// Get the error code for this error.
166    pub fn code(&self) -> i32 {
167        match self {
168            ProtocolError::SchemaValidation(_) => ERR_SCHEMA_VALIDATION,
169            ProtocolError::InvalidModel(_) => ERR_INVALID_MODEL,
170            ProtocolError::InvalidField(_) => ERR_INVALID_FIELD,
171            ProtocolError::TypeMismatch(_) => ERR_TYPE_MISMATCH,
172            ProtocolError::QueryPlanning(_) => ERR_QUERY_PLANNING,
173            ProtocolError::InvalidFilter(_) => ERR_INVALID_FILTER,
174            ProtocolError::InvalidOrderBy(_) => ERR_INVALID_ORDERBY,
175            ProtocolError::UnsupportedOperation(_) => ERR_UNSUPPORTED_OPERATION,
176            ProtocolError::DatabaseExecution(_) => ERR_DATABASE_EXECUTION,
177            ProtocolError::ConnectionFailed(_) => ERR_CONNECTION_FAILED,
178            ProtocolError::ConstraintViolation(_) => ERR_CONSTRAINT_VIOLATION,
179            ProtocolError::UniqueConstraintViolation(_) => ERR_UNIQUE_CONSTRAINT,
180            ProtocolError::ForeignKeyConstraintViolation(_) => ERR_FOREIGN_KEY_CONSTRAINT,
181            ProtocolError::CheckConstraintViolation(_) => ERR_CHECK_CONSTRAINT,
182            ProtocolError::NullConstraintViolation(_) => ERR_NULL_CONSTRAINT,
183            ProtocolError::Deadlock(_) => ERR_DEADLOCK,
184            ProtocolError::SerializationFailure(_) => ERR_SERIALIZATION_FAILURE,
185            ProtocolError::QueryTimeout(_) => ERR_QUERY_TIMEOUT,
186            ProtocolError::RecordNotFound(_) => ERR_RECORD_NOT_FOUND,
187            ProtocolError::TransactionNotFound(_) => ERR_TRANSACTION_NOT_FOUND,
188            ProtocolError::TransactionTimeout(_) => ERR_TRANSACTION_TIMEOUT,
189            ProtocolError::TransactionAlreadyClosed(_) => ERR_TRANSACTION_ALREADY_CLOSED,
190            ProtocolError::TransactionFailed(_) => ERR_TRANSACTION_FAILED,
191            ProtocolError::BatchOperationFailed { source, .. } => source.code(),
192            ProtocolError::Internal(_) => ERR_INTERNAL,
193            ProtocolError::UnsupportedProtocolVersion { .. } => ERR_UNSUPPORTED_PROTOCOL_VERSION,
194            ProtocolError::InvalidMethod(_) => ERR_INVALID_METHOD,
195            ProtocolError::InvalidParams(_) => ERR_INVALID_REQUEST_PARAMS,
196            ProtocolError::Serialization(_) => ERR_INTERNAL,
197        }
198    }
199
200    fn rpc_data(&self) -> Option<Value> {
201        match self {
202            ProtocolError::UnsupportedProtocolVersion { actual, expected } => Some(json!({
203                "actual": actual,
204                "expected": expected,
205            })),
206            ProtocolError::BatchOperationFailed {
207                index,
208                method,
209                source,
210            } => Some(
211                serde_json::to_value(BatchOperationErrorData {
212                    batch_operation_index: *index,
213                    batch_operation_method: method.clone(),
214                    cause: ProtocolErrorCause {
215                        code: source.code(),
216                        message: source.to_string(),
217                        data: source.rpc_data(),
218                    },
219                })
220                .expect("serializing batch-operation error data should succeed"),
221            ),
222            _ => None,
223        }
224    }
225}
226
227impl From<ProtocolError> for RpcError {
228    fn from(err: ProtocolError) -> Self {
229        let code = err.code();
230        let message = err.to_string();
231        let data = err.rpc_data();
232        RpcError {
233            code,
234            message,
235            data,
236        }
237    }
238}
239
240impl From<serde_json::Error> for ProtocolError {
241    fn from(err: serde_json::Error) -> Self {
242        ProtocolError::Serialization(err.to_string())
243    }
244}
245
246/// Result type alias for protocol operations.
247pub type Result<T> = std::result::Result<T, ProtocolError>;
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use serde_json::Value;
253
254    #[test]
255    fn test_error_codes() {
256        assert_eq!(
257            ProtocolError::SchemaValidation("test".into()).code(),
258            ERR_SCHEMA_VALIDATION
259        );
260        assert_eq!(
261            ProtocolError::InvalidModel("User".into()).code(),
262            ERR_INVALID_MODEL
263        );
264        assert_eq!(
265            ProtocolError::QueryPlanning("bad query".into()).code(),
266            ERR_QUERY_PLANNING
267        );
268        assert_eq!(
269            ProtocolError::DatabaseExecution("timeout".into()).code(),
270            ERR_DATABASE_EXECUTION
271        );
272        assert_eq!(
273            ProtocolError::UnsupportedProtocolVersion {
274                actual: 2,
275                expected: 1
276            }
277            .code(),
278            ERR_UNSUPPORTED_PROTOCOL_VERSION
279        );
280    }
281
282    #[test]
283    fn test_error_to_rpc_error() {
284        let err = ProtocolError::InvalidModel("Post".to_string());
285        let rpc_err: RpcError = err.into();
286
287        assert_eq!(rpc_err.code, ERR_INVALID_MODEL);
288        assert_eq!(rpc_err.message, "Invalid model: Post");
289        assert!(rpc_err.data.is_none());
290    }
291
292    #[test]
293    fn test_batch_operation_error_to_rpc_error_preserves_source_code_and_context() {
294        let err = ProtocolError::BatchOperationFailed {
295            index: 1,
296            method: "query.create".to_string(),
297            source: Box::new(ProtocolError::UniqueConstraintViolation(
298                "duplicate email".to_string(),
299            )),
300        };
301
302        let rpc_err: RpcError = err.into();
303
304        assert_eq!(rpc_err.code, ERR_UNIQUE_CONSTRAINT);
305        assert_eq!(
306            rpc_err.message,
307            "Unique constraint violation: duplicate email"
308        );
309
310        let data = rpc_err
311            .data
312            .expect("batch failures should include error.data");
313        assert_eq!(data["batchOperationIndex"], 1);
314        assert_eq!(data["batchOperationMethod"], "query.create");
315        assert_eq!(data["cause"]["code"], ERR_UNIQUE_CONSTRAINT);
316        assert_eq!(
317            data["cause"]["message"],
318            "Unique constraint violation: duplicate email"
319        );
320    }
321
322    #[test]
323    fn test_unsupported_protocol_version_rpc_error_includes_structured_data() {
324        let rpc_err: RpcError = ProtocolError::UnsupportedProtocolVersion {
325            actual: 2,
326            expected: 1,
327        }
328        .into();
329
330        assert_eq!(rpc_err.code, ERR_UNSUPPORTED_PROTOCOL_VERSION);
331        let data = rpc_err
332            .data
333            .expect("version mismatch should include error.data");
334        assert_eq!(data["actual"], 2);
335        assert_eq!(data["expected"], 1);
336    }
337
338    #[test]
339    fn test_error_display() {
340        let err = ProtocolError::UnsupportedProtocolVersion {
341            actual: 3,
342            expected: 1,
343        };
344        assert_eq!(
345            err.to_string(),
346            "Unsupported protocol version: 3, expected 1"
347        );
348    }
349
350    #[test]
351    fn test_serde_error_conversion() {
352        let bad_json = "{invalid json}";
353        let err: serde_json::Error = serde_json::from_str::<Value>(bad_json).unwrap_err();
354        let protocol_err: ProtocolError = err.into();
355
356        match protocol_err {
357            ProtocolError::Serialization(msg) => {
358                assert!(!msg.is_empty());
359            }
360            _ => panic!("Expected Serialization error"),
361        }
362    }
363}