Skip to main content

lance_namespace/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Lance Namespace error types.
5//!
6//! This module defines fine-grained error types for Lance Namespace operations.
7//! Each error type has a unique numeric code that is consistent across all
8//! Lance Namespace implementations (Python, Java, Rust, REST).
9//!
10//! # Error Handling
11//!
12//! Namespace operations return [`NamespaceError`] which can be converted to
13//! [`lance_core::Error`] for integration with the Lance ecosystem.
14//!
15//! ```rust,ignore
16//! use lance_namespace::{NamespaceError, ErrorCode};
17//!
18//! // Create and use namespace errors
19//! let err = NamespaceError::TableNotFound {
20//!     message: "Table 'users' not found".into(),
21//! };
22//! assert_eq!(err.code(), ErrorCode::TableNotFound);
23//!
24//! // Convert to lance_core::Error
25//! let lance_err: lance_core::Error = err.into();
26//! ```
27
28use snafu::Snafu;
29
30/// Lance Namespace error codes.
31///
32/// These codes are globally unique across all Lance Namespace implementations
33/// (Python, Java, Rust, REST). Use these codes for programmatic error handling.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[repr(u32)]
36pub enum ErrorCode {
37    /// Operation not supported by this backend
38    Unsupported = 0,
39    /// The specified namespace does not exist
40    NamespaceNotFound = 1,
41    /// A namespace with this name already exists
42    NamespaceAlreadyExists = 2,
43    /// Namespace contains tables or child namespaces
44    NamespaceNotEmpty = 3,
45    /// The specified table does not exist
46    TableNotFound = 4,
47    /// A table with this name already exists
48    TableAlreadyExists = 5,
49    /// The specified table index does not exist
50    TableIndexNotFound = 6,
51    /// A table index with this name already exists
52    TableIndexAlreadyExists = 7,
53    /// The specified table tag does not exist
54    TableTagNotFound = 8,
55    /// A table tag with this name already exists
56    TableTagAlreadyExists = 9,
57    /// The specified transaction does not exist
58    TransactionNotFound = 10,
59    /// The specified table version does not exist
60    TableVersionNotFound = 11,
61    /// The specified table column does not exist
62    TableColumnNotFound = 12,
63    /// Malformed request or invalid parameters
64    InvalidInput = 13,
65    /// Optimistic concurrency conflict
66    ConcurrentModification = 14,
67    /// User lacks permission for this operation
68    PermissionDenied = 15,
69    /// Authentication credentials are missing or invalid
70    Unauthenticated = 16,
71    /// Service is temporarily unavailable
72    ServiceUnavailable = 17,
73    /// Unexpected server/implementation error
74    Internal = 18,
75    /// Table is in an invalid state for the operation
76    InvalidTableState = 19,
77    /// Table schema validation failed
78    TableSchemaValidationError = 20,
79    /// Request was throttled due to rate limiting or too many concurrent operations
80    Throttled = 21,
81}
82
83impl ErrorCode {
84    /// Returns the numeric code value.
85    pub fn as_u32(self) -> u32 {
86        self as u32
87    }
88
89    /// Creates an ErrorCode from a numeric code.
90    ///
91    /// Returns `None` if the code is not recognized.
92    pub fn from_u32(code: u32) -> Option<Self> {
93        match code {
94            0 => Some(Self::Unsupported),
95            1 => Some(Self::NamespaceNotFound),
96            2 => Some(Self::NamespaceAlreadyExists),
97            3 => Some(Self::NamespaceNotEmpty),
98            4 => Some(Self::TableNotFound),
99            5 => Some(Self::TableAlreadyExists),
100            6 => Some(Self::TableIndexNotFound),
101            7 => Some(Self::TableIndexAlreadyExists),
102            8 => Some(Self::TableTagNotFound),
103            9 => Some(Self::TableTagAlreadyExists),
104            10 => Some(Self::TransactionNotFound),
105            11 => Some(Self::TableVersionNotFound),
106            12 => Some(Self::TableColumnNotFound),
107            13 => Some(Self::InvalidInput),
108            14 => Some(Self::ConcurrentModification),
109            15 => Some(Self::PermissionDenied),
110            16 => Some(Self::Unauthenticated),
111            17 => Some(Self::ServiceUnavailable),
112            18 => Some(Self::Internal),
113            19 => Some(Self::InvalidTableState),
114            20 => Some(Self::TableSchemaValidationError),
115            21 => Some(Self::Throttled),
116            _ => None,
117        }
118    }
119}
120
121impl std::fmt::Display for ErrorCode {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        let name = match self {
124            Self::Unsupported => "Unsupported",
125            Self::NamespaceNotFound => "NamespaceNotFound",
126            Self::NamespaceAlreadyExists => "NamespaceAlreadyExists",
127            Self::NamespaceNotEmpty => "NamespaceNotEmpty",
128            Self::TableNotFound => "TableNotFound",
129            Self::TableAlreadyExists => "TableAlreadyExists",
130            Self::TableIndexNotFound => "TableIndexNotFound",
131            Self::TableIndexAlreadyExists => "TableIndexAlreadyExists",
132            Self::TableTagNotFound => "TableTagNotFound",
133            Self::TableTagAlreadyExists => "TableTagAlreadyExists",
134            Self::TransactionNotFound => "TransactionNotFound",
135            Self::TableVersionNotFound => "TableVersionNotFound",
136            Self::TableColumnNotFound => "TableColumnNotFound",
137            Self::InvalidInput => "InvalidInput",
138            Self::ConcurrentModification => "ConcurrentModification",
139            Self::PermissionDenied => "PermissionDenied",
140            Self::Unauthenticated => "Unauthenticated",
141            Self::ServiceUnavailable => "ServiceUnavailable",
142            Self::Internal => "Internal",
143            Self::InvalidTableState => "InvalidTableState",
144            Self::TableSchemaValidationError => "TableSchemaValidationError",
145            Self::Throttled => "Throttled",
146        };
147        write!(f, "{}", name)
148    }
149}
150
151/// Lance Namespace error type.
152///
153/// This enum provides fine-grained error types for Lance Namespace operations.
154/// Each variant corresponds to a specific error condition and has an associated
155/// [`ErrorCode`] accessible via the [`code()`](NamespaceError::code) method.
156///
157/// # Converting to lance_core::Error
158///
159/// `NamespaceError` implements `Into<lance_core::Error>`, preserving the original
160/// error so it can be downcast later:
161///
162/// ```rust,ignore
163/// let ns_err = NamespaceError::TableNotFound { message: "...".into() };
164/// let lance_err: lance_core::Error = ns_err.into();
165///
166/// // Later, extract the original error:
167/// if let lance_core::Error::Namespace { source, .. } = &lance_err {
168///     if let Some(ns_err) = source.downcast_ref::<NamespaceError>() {
169///         println!("Error code: {:?}", ns_err.code());
170///     }
171/// }
172/// ```
173#[derive(Debug, Snafu)]
174#[snafu(visibility(pub))]
175pub enum NamespaceError {
176    /// Operation not supported by this backend.
177    #[snafu(display("Unsupported: {message}"))]
178    Unsupported { message: String },
179
180    /// The specified namespace does not exist.
181    #[snafu(display("Namespace not found: {message}"))]
182    NamespaceNotFound { message: String },
183
184    /// A namespace with this name already exists.
185    #[snafu(display("Namespace already exists: {message}"))]
186    NamespaceAlreadyExists { message: String },
187
188    /// Namespace contains tables or child namespaces.
189    #[snafu(display("Namespace not empty: {message}"))]
190    NamespaceNotEmpty { message: String },
191
192    /// The specified table does not exist.
193    #[snafu(display("Table not found: {message}"))]
194    TableNotFound { message: String },
195
196    /// A table with this name already exists.
197    #[snafu(display("Table already exists: {message}"))]
198    TableAlreadyExists { message: String },
199
200    /// The specified table index does not exist.
201    #[snafu(display("Table index not found: {message}"))]
202    TableIndexNotFound { message: String },
203
204    /// A table index with this name already exists.
205    #[snafu(display("Table index already exists: {message}"))]
206    TableIndexAlreadyExists { message: String },
207
208    /// The specified table tag does not exist.
209    #[snafu(display("Table tag not found: {message}"))]
210    TableTagNotFound { message: String },
211
212    /// A table tag with this name already exists.
213    #[snafu(display("Table tag already exists: {message}"))]
214    TableTagAlreadyExists { message: String },
215
216    /// The specified transaction does not exist.
217    #[snafu(display("Transaction not found: {message}"))]
218    TransactionNotFound { message: String },
219
220    /// The specified table version does not exist.
221    #[snafu(display("Table version not found: {message}"))]
222    TableVersionNotFound { message: String },
223
224    /// The specified table column does not exist.
225    #[snafu(display("Table column not found: {message}"))]
226    TableColumnNotFound { message: String },
227
228    /// Malformed request or invalid parameters.
229    #[snafu(display("Invalid input: {message}"))]
230    InvalidInput { message: String },
231
232    /// Optimistic concurrency conflict.
233    #[snafu(display("Concurrent modification: {message}"))]
234    ConcurrentModification { message: String },
235
236    /// User lacks permission for this operation.
237    #[snafu(display("Permission denied: {message}"))]
238    PermissionDenied { message: String },
239
240    /// Authentication credentials are missing or invalid.
241    #[snafu(display("Unauthenticated: {message}"))]
242    Unauthenticated { message: String },
243
244    /// Service is temporarily unavailable.
245    #[snafu(display("Service unavailable: {message}"))]
246    ServiceUnavailable { message: String },
247
248    /// Unexpected internal error.
249    #[snafu(display("Internal error: {message}"))]
250    Internal { message: String },
251
252    /// Table is in an invalid state for the operation.
253    #[snafu(display("Invalid table state: {message}"))]
254    InvalidTableState { message: String },
255
256    /// Table schema validation failed.
257    #[snafu(display("Table schema validation error: {message}"))]
258    TableSchemaValidationError { message: String },
259
260    /// Request was throttled due to rate limiting or too many concurrent operations.
261    #[snafu(display("Throttled: {message}"))]
262    Throttled { message: String },
263}
264
265impl NamespaceError {
266    /// Returns the error code for this error.
267    ///
268    /// Use this for programmatic error handling across language boundaries.
269    pub fn code(&self) -> ErrorCode {
270        match self {
271            Self::Unsupported { .. } => ErrorCode::Unsupported,
272            Self::NamespaceNotFound { .. } => ErrorCode::NamespaceNotFound,
273            Self::NamespaceAlreadyExists { .. } => ErrorCode::NamespaceAlreadyExists,
274            Self::NamespaceNotEmpty { .. } => ErrorCode::NamespaceNotEmpty,
275            Self::TableNotFound { .. } => ErrorCode::TableNotFound,
276            Self::TableAlreadyExists { .. } => ErrorCode::TableAlreadyExists,
277            Self::TableIndexNotFound { .. } => ErrorCode::TableIndexNotFound,
278            Self::TableIndexAlreadyExists { .. } => ErrorCode::TableIndexAlreadyExists,
279            Self::TableTagNotFound { .. } => ErrorCode::TableTagNotFound,
280            Self::TableTagAlreadyExists { .. } => ErrorCode::TableTagAlreadyExists,
281            Self::TransactionNotFound { .. } => ErrorCode::TransactionNotFound,
282            Self::TableVersionNotFound { .. } => ErrorCode::TableVersionNotFound,
283            Self::TableColumnNotFound { .. } => ErrorCode::TableColumnNotFound,
284            Self::InvalidInput { .. } => ErrorCode::InvalidInput,
285            Self::ConcurrentModification { .. } => ErrorCode::ConcurrentModification,
286            Self::PermissionDenied { .. } => ErrorCode::PermissionDenied,
287            Self::Unauthenticated { .. } => ErrorCode::Unauthenticated,
288            Self::ServiceUnavailable { .. } => ErrorCode::ServiceUnavailable,
289            Self::Internal { .. } => ErrorCode::Internal,
290            Self::InvalidTableState { .. } => ErrorCode::InvalidTableState,
291            Self::TableSchemaValidationError { .. } => ErrorCode::TableSchemaValidationError,
292            Self::Throttled { .. } => ErrorCode::Throttled,
293        }
294    }
295
296    /// Creates a NamespaceError from an error code and message.
297    ///
298    /// This is useful when receiving errors from REST API or other language bindings.
299    pub fn from_code(code: u32, message: impl Into<String>) -> Self {
300        let message = message.into();
301        match ErrorCode::from_u32(code) {
302            Some(ErrorCode::Unsupported) => Self::Unsupported { message },
303            Some(ErrorCode::NamespaceNotFound) => Self::NamespaceNotFound { message },
304            Some(ErrorCode::NamespaceAlreadyExists) => Self::NamespaceAlreadyExists { message },
305            Some(ErrorCode::NamespaceNotEmpty) => Self::NamespaceNotEmpty { message },
306            Some(ErrorCode::TableNotFound) => Self::TableNotFound { message },
307            Some(ErrorCode::TableAlreadyExists) => Self::TableAlreadyExists { message },
308            Some(ErrorCode::TableIndexNotFound) => Self::TableIndexNotFound { message },
309            Some(ErrorCode::TableIndexAlreadyExists) => Self::TableIndexAlreadyExists { message },
310            Some(ErrorCode::TableTagNotFound) => Self::TableTagNotFound { message },
311            Some(ErrorCode::TableTagAlreadyExists) => Self::TableTagAlreadyExists { message },
312            Some(ErrorCode::TransactionNotFound) => Self::TransactionNotFound { message },
313            Some(ErrorCode::TableVersionNotFound) => Self::TableVersionNotFound { message },
314            Some(ErrorCode::TableColumnNotFound) => Self::TableColumnNotFound { message },
315            Some(ErrorCode::InvalidInput) => Self::InvalidInput { message },
316            Some(ErrorCode::ConcurrentModification) => Self::ConcurrentModification { message },
317            Some(ErrorCode::PermissionDenied) => Self::PermissionDenied { message },
318            Some(ErrorCode::Unauthenticated) => Self::Unauthenticated { message },
319            Some(ErrorCode::ServiceUnavailable) => Self::ServiceUnavailable { message },
320            Some(ErrorCode::Internal) => Self::Internal { message },
321            Some(ErrorCode::InvalidTableState) => Self::InvalidTableState { message },
322            Some(ErrorCode::TableSchemaValidationError) => {
323                Self::TableSchemaValidationError { message }
324            }
325            Some(ErrorCode::Throttled) => Self::Throttled { message },
326            None => Self::Internal { message },
327        }
328    }
329}
330
331/// Converts a NamespaceError into a lance_core::Error.
332///
333/// The original `NamespaceError` is preserved in the `source` field and can be
334/// extracted via downcasting for programmatic error handling.
335impl From<NamespaceError> for lance_core::Error {
336    #[track_caller]
337    fn from(err: NamespaceError) -> Self {
338        Self::namespace_source(Box::new(err))
339    }
340}
341
342/// Result type for namespace operations.
343pub type Result<T> = std::result::Result<T, NamespaceError>;
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_error_code_roundtrip() {
351        for code in 0..=21 {
352            let error_code = ErrorCode::from_u32(code).unwrap();
353            assert_eq!(error_code.as_u32(), code);
354        }
355    }
356
357    #[test]
358    fn test_unknown_error_code() {
359        assert!(ErrorCode::from_u32(999).is_none());
360    }
361
362    #[test]
363    fn test_namespace_error_code() {
364        let err = NamespaceError::TableNotFound {
365            message: "test table".to_string(),
366        };
367        assert_eq!(err.code(), ErrorCode::TableNotFound);
368        assert_eq!(err.code().as_u32(), 4);
369    }
370
371    #[test]
372    fn test_from_code() {
373        let err = NamespaceError::from_code(4, "table not found");
374        assert_eq!(err.code(), ErrorCode::TableNotFound);
375        assert!(err.to_string().contains("table not found"));
376    }
377
378    #[test]
379    fn test_from_unknown_code() {
380        let err = NamespaceError::from_code(999, "unknown error");
381        assert_eq!(err.code(), ErrorCode::Internal);
382    }
383
384    #[test]
385    fn test_convert_to_lance_error() {
386        let ns_err = NamespaceError::TableNotFound {
387            message: "users".to_string(),
388        };
389        let lance_err: lance_core::Error = ns_err.into();
390
391        // Verify it's a Namespace error
392        match &lance_err {
393            lance_core::Error::Namespace { source, .. } => {
394                // Downcast to get the original error
395                let downcast = source.downcast_ref::<NamespaceError>();
396                assert!(downcast.is_some());
397                assert_eq!(downcast.unwrap().code(), ErrorCode::TableNotFound);
398            }
399            _ => panic!("Expected Namespace error"),
400        }
401    }
402
403    #[test]
404    fn test_error_display() {
405        let err = NamespaceError::TableNotFound {
406            message: "users".to_string(),
407        };
408        assert_eq!(err.to_string(), "Table not found: users");
409    }
410}