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    Throttling = 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::Throttling),
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::Throttling => "Throttling",
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("Throttling: {message}"))]
262    Throttling { message: String },
263}
264
265impl NamespaceError {
266    /// Returns the inner message without the Display prefix.
267    ///
268    /// Useful when serializing across boundaries (e.g. REST) where
269    /// the receiver will reconstruct the variant from the error code
270    /// and re-apply its own Display formatting.
271    pub fn message(&self) -> &str {
272        match self {
273            Self::Unsupported { message }
274            | Self::NamespaceNotFound { message }
275            | Self::NamespaceAlreadyExists { message }
276            | Self::NamespaceNotEmpty { message }
277            | Self::TableNotFound { message }
278            | Self::TableAlreadyExists { message }
279            | Self::TableIndexNotFound { message }
280            | Self::TableIndexAlreadyExists { message }
281            | Self::TableTagNotFound { message }
282            | Self::TableTagAlreadyExists { message }
283            | Self::TransactionNotFound { message }
284            | Self::TableVersionNotFound { message }
285            | Self::TableColumnNotFound { message }
286            | Self::InvalidInput { message }
287            | Self::ConcurrentModification { message }
288            | Self::PermissionDenied { message }
289            | Self::Unauthenticated { message }
290            | Self::ServiceUnavailable { message }
291            | Self::Internal { message }
292            | Self::InvalidTableState { message }
293            | Self::TableSchemaValidationError { message }
294            | Self::Throttling { message } => message,
295        }
296    }
297
298    /// Returns the error code for this error.
299    ///
300    /// Use this for programmatic error handling across language boundaries.
301    pub fn code(&self) -> ErrorCode {
302        match self {
303            Self::Unsupported { .. } => ErrorCode::Unsupported,
304            Self::NamespaceNotFound { .. } => ErrorCode::NamespaceNotFound,
305            Self::NamespaceAlreadyExists { .. } => ErrorCode::NamespaceAlreadyExists,
306            Self::NamespaceNotEmpty { .. } => ErrorCode::NamespaceNotEmpty,
307            Self::TableNotFound { .. } => ErrorCode::TableNotFound,
308            Self::TableAlreadyExists { .. } => ErrorCode::TableAlreadyExists,
309            Self::TableIndexNotFound { .. } => ErrorCode::TableIndexNotFound,
310            Self::TableIndexAlreadyExists { .. } => ErrorCode::TableIndexAlreadyExists,
311            Self::TableTagNotFound { .. } => ErrorCode::TableTagNotFound,
312            Self::TableTagAlreadyExists { .. } => ErrorCode::TableTagAlreadyExists,
313            Self::TransactionNotFound { .. } => ErrorCode::TransactionNotFound,
314            Self::TableVersionNotFound { .. } => ErrorCode::TableVersionNotFound,
315            Self::TableColumnNotFound { .. } => ErrorCode::TableColumnNotFound,
316            Self::InvalidInput { .. } => ErrorCode::InvalidInput,
317            Self::ConcurrentModification { .. } => ErrorCode::ConcurrentModification,
318            Self::PermissionDenied { .. } => ErrorCode::PermissionDenied,
319            Self::Unauthenticated { .. } => ErrorCode::Unauthenticated,
320            Self::ServiceUnavailable { .. } => ErrorCode::ServiceUnavailable,
321            Self::Internal { .. } => ErrorCode::Internal,
322            Self::InvalidTableState { .. } => ErrorCode::InvalidTableState,
323            Self::TableSchemaValidationError { .. } => ErrorCode::TableSchemaValidationError,
324            Self::Throttling { .. } => ErrorCode::Throttling,
325        }
326    }
327
328    /// Creates a NamespaceError from an error code and message.
329    ///
330    /// This is useful when receiving errors from REST API or other language bindings.
331    pub fn from_code(code: u32, message: impl Into<String>) -> Self {
332        let message = message.into();
333        match ErrorCode::from_u32(code) {
334            Some(ErrorCode::Unsupported) => Self::Unsupported { message },
335            Some(ErrorCode::NamespaceNotFound) => Self::NamespaceNotFound { message },
336            Some(ErrorCode::NamespaceAlreadyExists) => Self::NamespaceAlreadyExists { message },
337            Some(ErrorCode::NamespaceNotEmpty) => Self::NamespaceNotEmpty { message },
338            Some(ErrorCode::TableNotFound) => Self::TableNotFound { message },
339            Some(ErrorCode::TableAlreadyExists) => Self::TableAlreadyExists { message },
340            Some(ErrorCode::TableIndexNotFound) => Self::TableIndexNotFound { message },
341            Some(ErrorCode::TableIndexAlreadyExists) => Self::TableIndexAlreadyExists { message },
342            Some(ErrorCode::TableTagNotFound) => Self::TableTagNotFound { message },
343            Some(ErrorCode::TableTagAlreadyExists) => Self::TableTagAlreadyExists { message },
344            Some(ErrorCode::TransactionNotFound) => Self::TransactionNotFound { message },
345            Some(ErrorCode::TableVersionNotFound) => Self::TableVersionNotFound { message },
346            Some(ErrorCode::TableColumnNotFound) => Self::TableColumnNotFound { message },
347            Some(ErrorCode::InvalidInput) => Self::InvalidInput { message },
348            Some(ErrorCode::ConcurrentModification) => Self::ConcurrentModification { message },
349            Some(ErrorCode::PermissionDenied) => Self::PermissionDenied { message },
350            Some(ErrorCode::Unauthenticated) => Self::Unauthenticated { message },
351            Some(ErrorCode::ServiceUnavailable) => Self::ServiceUnavailable { message },
352            Some(ErrorCode::Internal) => Self::Internal { message },
353            Some(ErrorCode::InvalidTableState) => Self::InvalidTableState { message },
354            Some(ErrorCode::TableSchemaValidationError) => {
355                Self::TableSchemaValidationError { message }
356            }
357            Some(ErrorCode::Throttling) => Self::Throttling { message },
358            None => Self::Internal { message },
359        }
360    }
361}
362
363/// Converts a NamespaceError into a lance_core::Error.
364///
365/// The original `NamespaceError` is preserved in the `source` field and can be
366/// extracted via downcasting for programmatic error handling.
367impl From<NamespaceError> for lance_core::Error {
368    #[track_caller]
369    fn from(err: NamespaceError) -> Self {
370        Self::namespace_source(Box::new(err))
371    }
372}
373
374/// Result type for namespace operations.
375pub type Result<T> = std::result::Result<T, NamespaceError>;
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_error_code_roundtrip() {
383        for code in 0..=21 {
384            let error_code = ErrorCode::from_u32(code).unwrap();
385            assert_eq!(error_code.as_u32(), code);
386        }
387    }
388
389    #[test]
390    fn test_unknown_error_code() {
391        assert!(ErrorCode::from_u32(999).is_none());
392    }
393
394    #[test]
395    fn test_namespace_error_code() {
396        let err = NamespaceError::TableNotFound {
397            message: "test table".to_string(),
398        };
399        assert_eq!(err.code(), ErrorCode::TableNotFound);
400        assert_eq!(err.code().as_u32(), 4);
401    }
402
403    #[test]
404    fn test_from_code() {
405        let err = NamespaceError::from_code(4, "table not found");
406        assert_eq!(err.code(), ErrorCode::TableNotFound);
407        assert!(err.to_string().contains("table not found"));
408    }
409
410    #[test]
411    fn test_from_unknown_code() {
412        let err = NamespaceError::from_code(999, "unknown error");
413        assert_eq!(err.code(), ErrorCode::Internal);
414    }
415
416    #[test]
417    fn test_convert_to_lance_error() {
418        let ns_err = NamespaceError::TableNotFound {
419            message: "users".to_string(),
420        };
421        let lance_err: lance_core::Error = ns_err.into();
422
423        // Verify it's a Namespace error
424        match &lance_err {
425            lance_core::Error::Namespace { source, .. } => {
426                // Downcast to get the original error
427                let downcast = source.downcast_ref::<NamespaceError>();
428                assert!(downcast.is_some());
429                assert_eq!(downcast.unwrap().code(), ErrorCode::TableNotFound);
430            }
431            _ => panic!("Expected Namespace error"),
432        }
433    }
434
435    #[test]
436    fn test_error_display() {
437        let err = NamespaceError::TableNotFound {
438            message: "users".to_string(),
439        };
440        assert_eq!(err.to_string(), "Table not found: users");
441    }
442}