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    /// The specified table branch does not exist
82    TableBranchNotFound = 22,
83    /// A table branch with this name already exists
84    TableBranchAlreadyExists = 23,
85}
86
87impl ErrorCode {
88    /// Returns the numeric code value.
89    pub fn as_u32(self) -> u32 {
90        self as u32
91    }
92
93    /// Creates an ErrorCode from a numeric code.
94    ///
95    /// Returns `None` if the code is not recognized.
96    pub fn from_u32(code: u32) -> Option<Self> {
97        match code {
98            0 => Some(Self::Unsupported),
99            1 => Some(Self::NamespaceNotFound),
100            2 => Some(Self::NamespaceAlreadyExists),
101            3 => Some(Self::NamespaceNotEmpty),
102            4 => Some(Self::TableNotFound),
103            5 => Some(Self::TableAlreadyExists),
104            6 => Some(Self::TableIndexNotFound),
105            7 => Some(Self::TableIndexAlreadyExists),
106            8 => Some(Self::TableTagNotFound),
107            9 => Some(Self::TableTagAlreadyExists),
108            10 => Some(Self::TransactionNotFound),
109            11 => Some(Self::TableVersionNotFound),
110            12 => Some(Self::TableColumnNotFound),
111            13 => Some(Self::InvalidInput),
112            14 => Some(Self::ConcurrentModification),
113            15 => Some(Self::PermissionDenied),
114            16 => Some(Self::Unauthenticated),
115            17 => Some(Self::ServiceUnavailable),
116            18 => Some(Self::Internal),
117            19 => Some(Self::InvalidTableState),
118            20 => Some(Self::TableSchemaValidationError),
119            21 => Some(Self::Throttling),
120            22 => Some(Self::TableBranchNotFound),
121            23 => Some(Self::TableBranchAlreadyExists),
122            _ => None,
123        }
124    }
125}
126
127impl std::fmt::Display for ErrorCode {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        let name = match self {
130            Self::Unsupported => "Unsupported",
131            Self::NamespaceNotFound => "NamespaceNotFound",
132            Self::NamespaceAlreadyExists => "NamespaceAlreadyExists",
133            Self::NamespaceNotEmpty => "NamespaceNotEmpty",
134            Self::TableNotFound => "TableNotFound",
135            Self::TableAlreadyExists => "TableAlreadyExists",
136            Self::TableIndexNotFound => "TableIndexNotFound",
137            Self::TableIndexAlreadyExists => "TableIndexAlreadyExists",
138            Self::TableTagNotFound => "TableTagNotFound",
139            Self::TableTagAlreadyExists => "TableTagAlreadyExists",
140            Self::TransactionNotFound => "TransactionNotFound",
141            Self::TableVersionNotFound => "TableVersionNotFound",
142            Self::TableColumnNotFound => "TableColumnNotFound",
143            Self::InvalidInput => "InvalidInput",
144            Self::ConcurrentModification => "ConcurrentModification",
145            Self::PermissionDenied => "PermissionDenied",
146            Self::Unauthenticated => "Unauthenticated",
147            Self::ServiceUnavailable => "ServiceUnavailable",
148            Self::Internal => "Internal",
149            Self::InvalidTableState => "InvalidTableState",
150            Self::TableSchemaValidationError => "TableSchemaValidationError",
151            Self::Throttling => "Throttling",
152            Self::TableBranchNotFound => "TableBranchNotFound",
153            Self::TableBranchAlreadyExists => "TableBranchAlreadyExists",
154        };
155        write!(f, "{}", name)
156    }
157}
158
159/// Lance Namespace error type.
160///
161/// This enum provides fine-grained error types for Lance Namespace operations.
162/// Each variant corresponds to a specific error condition and has an associated
163/// [`ErrorCode`] accessible via the [`code()`](NamespaceError::code) method.
164///
165/// # Converting to lance_core::Error
166///
167/// `NamespaceError` implements `Into<lance_core::Error>`, preserving the original
168/// error so it can be downcast later:
169///
170/// ```rust,ignore
171/// let ns_err = NamespaceError::TableNotFound { message: "...".into() };
172/// let lance_err: lance_core::Error = ns_err.into();
173///
174/// // Later, extract the original error:
175/// if let lance_core::Error::Namespace { source, .. } = &lance_err {
176///     if let Some(ns_err) = source.downcast_ref::<NamespaceError>() {
177///         println!("Error code: {:?}", ns_err.code());
178///     }
179/// }
180/// ```
181#[derive(Debug, Snafu)]
182#[snafu(visibility(pub))]
183pub enum NamespaceError {
184    /// Operation not supported by this backend.
185    #[snafu(display("Unsupported: {message}"))]
186    Unsupported { message: String },
187
188    /// The specified namespace does not exist.
189    #[snafu(display("Namespace not found: {message}"))]
190    NamespaceNotFound { message: String },
191
192    /// A namespace with this name already exists.
193    #[snafu(display("Namespace already exists: {message}"))]
194    NamespaceAlreadyExists { message: String },
195
196    /// Namespace contains tables or child namespaces.
197    #[snafu(display("Namespace not empty: {message}"))]
198    NamespaceNotEmpty { message: String },
199
200    /// The specified table does not exist.
201    #[snafu(display("Table not found: {message}"))]
202    TableNotFound { message: String },
203
204    /// A table with this name already exists.
205    #[snafu(display("Table already exists: {message}"))]
206    TableAlreadyExists { message: String },
207
208    /// The specified table index does not exist.
209    #[snafu(display("Table index not found: {message}"))]
210    TableIndexNotFound { message: String },
211
212    /// A table index with this name already exists.
213    #[snafu(display("Table index already exists: {message}"))]
214    TableIndexAlreadyExists { message: String },
215
216    /// The specified table tag does not exist.
217    #[snafu(display("Table tag not found: {message}"))]
218    TableTagNotFound { message: String },
219
220    /// A table tag with this name already exists.
221    #[snafu(display("Table tag already exists: {message}"))]
222    TableTagAlreadyExists { message: String },
223
224    /// The specified transaction does not exist.
225    #[snafu(display("Transaction not found: {message}"))]
226    TransactionNotFound { message: String },
227
228    /// The specified table version does not exist.
229    #[snafu(display("Table version not found: {message}"))]
230    TableVersionNotFound { message: String },
231
232    /// The specified table column does not exist.
233    #[snafu(display("Table column not found: {message}"))]
234    TableColumnNotFound { message: String },
235
236    /// Malformed request or invalid parameters.
237    #[snafu(display("Invalid input: {message}"))]
238    InvalidInput { message: String },
239
240    /// Optimistic concurrency conflict.
241    #[snafu(display("Concurrent modification: {message}"))]
242    ConcurrentModification { message: String },
243
244    /// User lacks permission for this operation.
245    #[snafu(display("Permission denied: {message}"))]
246    PermissionDenied { message: String },
247
248    /// Authentication credentials are missing or invalid.
249    #[snafu(display("Unauthenticated: {message}"))]
250    Unauthenticated { message: String },
251
252    /// Service is temporarily unavailable.
253    #[snafu(display("Service unavailable: {message}"))]
254    ServiceUnavailable { message: String },
255
256    /// Unexpected internal error.
257    #[snafu(display("Internal error: {message}"))]
258    Internal { message: String },
259
260    /// Table is in an invalid state for the operation.
261    #[snafu(display("Invalid table state: {message}"))]
262    InvalidTableState { message: String },
263
264    /// Table schema validation failed.
265    #[snafu(display("Table schema validation error: {message}"))]
266    TableSchemaValidationError { message: String },
267
268    /// Request was throttled due to rate limiting or too many concurrent operations.
269    #[snafu(display("Throttling: {message}"))]
270    Throttling { message: String },
271
272    /// The specified table branch does not exist.
273    #[snafu(display("Table branch not found: {message}"))]
274    TableBranchNotFound { message: String },
275
276    /// A table branch with this name already exists.
277    #[snafu(display("Table branch already exists: {message}"))]
278    TableBranchAlreadyExists { message: String },
279}
280
281impl NamespaceError {
282    /// Returns the inner message without the Display prefix.
283    ///
284    /// Useful when serializing across boundaries (e.g. REST) where
285    /// the receiver will reconstruct the variant from the error code
286    /// and re-apply its own Display formatting.
287    pub fn message(&self) -> &str {
288        match self {
289            Self::Unsupported { message }
290            | Self::NamespaceNotFound { message }
291            | Self::NamespaceAlreadyExists { message }
292            | Self::NamespaceNotEmpty { message }
293            | Self::TableNotFound { message }
294            | Self::TableAlreadyExists { message }
295            | Self::TableIndexNotFound { message }
296            | Self::TableIndexAlreadyExists { message }
297            | Self::TableTagNotFound { message }
298            | Self::TableTagAlreadyExists { message }
299            | Self::TransactionNotFound { message }
300            | Self::TableVersionNotFound { message }
301            | Self::TableColumnNotFound { message }
302            | Self::InvalidInput { message }
303            | Self::ConcurrentModification { message }
304            | Self::PermissionDenied { message }
305            | Self::Unauthenticated { message }
306            | Self::ServiceUnavailable { message }
307            | Self::Internal { message }
308            | Self::InvalidTableState { message }
309            | Self::TableSchemaValidationError { message }
310            | Self::Throttling { message }
311            | Self::TableBranchNotFound { message }
312            | Self::TableBranchAlreadyExists { message } => message,
313        }
314    }
315
316    /// Returns the error code for this error.
317    ///
318    /// Use this for programmatic error handling across language boundaries.
319    pub fn code(&self) -> ErrorCode {
320        match self {
321            Self::Unsupported { .. } => ErrorCode::Unsupported,
322            Self::NamespaceNotFound { .. } => ErrorCode::NamespaceNotFound,
323            Self::NamespaceAlreadyExists { .. } => ErrorCode::NamespaceAlreadyExists,
324            Self::NamespaceNotEmpty { .. } => ErrorCode::NamespaceNotEmpty,
325            Self::TableNotFound { .. } => ErrorCode::TableNotFound,
326            Self::TableAlreadyExists { .. } => ErrorCode::TableAlreadyExists,
327            Self::TableIndexNotFound { .. } => ErrorCode::TableIndexNotFound,
328            Self::TableIndexAlreadyExists { .. } => ErrorCode::TableIndexAlreadyExists,
329            Self::TableTagNotFound { .. } => ErrorCode::TableTagNotFound,
330            Self::TableTagAlreadyExists { .. } => ErrorCode::TableTagAlreadyExists,
331            Self::TransactionNotFound { .. } => ErrorCode::TransactionNotFound,
332            Self::TableVersionNotFound { .. } => ErrorCode::TableVersionNotFound,
333            Self::TableColumnNotFound { .. } => ErrorCode::TableColumnNotFound,
334            Self::InvalidInput { .. } => ErrorCode::InvalidInput,
335            Self::ConcurrentModification { .. } => ErrorCode::ConcurrentModification,
336            Self::PermissionDenied { .. } => ErrorCode::PermissionDenied,
337            Self::Unauthenticated { .. } => ErrorCode::Unauthenticated,
338            Self::ServiceUnavailable { .. } => ErrorCode::ServiceUnavailable,
339            Self::Internal { .. } => ErrorCode::Internal,
340            Self::InvalidTableState { .. } => ErrorCode::InvalidTableState,
341            Self::TableSchemaValidationError { .. } => ErrorCode::TableSchemaValidationError,
342            Self::Throttling { .. } => ErrorCode::Throttling,
343            Self::TableBranchNotFound { .. } => ErrorCode::TableBranchNotFound,
344            Self::TableBranchAlreadyExists { .. } => ErrorCode::TableBranchAlreadyExists,
345        }
346    }
347
348    /// Creates a NamespaceError from an error code and message.
349    ///
350    /// This is useful when receiving errors from REST API or other language bindings.
351    pub fn from_code(code: u32, message: impl Into<String>) -> Self {
352        let message = message.into();
353        match ErrorCode::from_u32(code) {
354            Some(ErrorCode::Unsupported) => Self::Unsupported { message },
355            Some(ErrorCode::NamespaceNotFound) => Self::NamespaceNotFound { message },
356            Some(ErrorCode::NamespaceAlreadyExists) => Self::NamespaceAlreadyExists { message },
357            Some(ErrorCode::NamespaceNotEmpty) => Self::NamespaceNotEmpty { message },
358            Some(ErrorCode::TableNotFound) => Self::TableNotFound { message },
359            Some(ErrorCode::TableAlreadyExists) => Self::TableAlreadyExists { message },
360            Some(ErrorCode::TableIndexNotFound) => Self::TableIndexNotFound { message },
361            Some(ErrorCode::TableIndexAlreadyExists) => Self::TableIndexAlreadyExists { message },
362            Some(ErrorCode::TableTagNotFound) => Self::TableTagNotFound { message },
363            Some(ErrorCode::TableTagAlreadyExists) => Self::TableTagAlreadyExists { message },
364            Some(ErrorCode::TransactionNotFound) => Self::TransactionNotFound { message },
365            Some(ErrorCode::TableVersionNotFound) => Self::TableVersionNotFound { message },
366            Some(ErrorCode::TableColumnNotFound) => Self::TableColumnNotFound { message },
367            Some(ErrorCode::InvalidInput) => Self::InvalidInput { message },
368            Some(ErrorCode::ConcurrentModification) => Self::ConcurrentModification { message },
369            Some(ErrorCode::PermissionDenied) => Self::PermissionDenied { message },
370            Some(ErrorCode::Unauthenticated) => Self::Unauthenticated { message },
371            Some(ErrorCode::ServiceUnavailable) => Self::ServiceUnavailable { message },
372            Some(ErrorCode::Internal) => Self::Internal { message },
373            Some(ErrorCode::InvalidTableState) => Self::InvalidTableState { message },
374            Some(ErrorCode::TableSchemaValidationError) => {
375                Self::TableSchemaValidationError { message }
376            }
377            Some(ErrorCode::Throttling) => Self::Throttling { message },
378            Some(ErrorCode::TableBranchNotFound) => Self::TableBranchNotFound { message },
379            Some(ErrorCode::TableBranchAlreadyExists) => Self::TableBranchAlreadyExists { message },
380            None => Self::Internal { message },
381        }
382    }
383}
384
385/// Converts a NamespaceError into a lance_core::Error.
386///
387/// The original `NamespaceError` is preserved in the `source` field and can be
388/// extracted via downcasting for programmatic error handling.
389impl From<NamespaceError> for lance_core::Error {
390    #[track_caller]
391    fn from(err: NamespaceError) -> Self {
392        Self::namespace_source(Box::new(err))
393    }
394}
395
396/// Result type for namespace operations.
397pub type Result<T> = std::result::Result<T, NamespaceError>;
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_error_code_roundtrip() {
405        for code in 0..=23 {
406            let error_code = ErrorCode::from_u32(code).unwrap();
407            assert_eq!(error_code.as_u32(), code);
408        }
409    }
410
411    #[test]
412    fn test_unknown_error_code() {
413        assert!(ErrorCode::from_u32(999).is_none());
414    }
415
416    #[test]
417    fn test_namespace_error_code() {
418        let err = NamespaceError::TableNotFound {
419            message: "test table".to_string(),
420        };
421        assert_eq!(err.code(), ErrorCode::TableNotFound);
422        assert_eq!(err.code().as_u32(), 4);
423    }
424
425    #[test]
426    fn test_from_code() {
427        let err = NamespaceError::from_code(4, "table not found");
428        assert_eq!(err.code(), ErrorCode::TableNotFound);
429        assert!(err.to_string().contains("table not found"));
430    }
431
432    #[test]
433    fn test_from_unknown_code() {
434        let err = NamespaceError::from_code(999, "unknown error");
435        assert_eq!(err.code(), ErrorCode::Internal);
436    }
437
438    #[test]
439    fn test_convert_to_lance_error() {
440        let ns_err = NamespaceError::TableNotFound {
441            message: "users".to_string(),
442        };
443        let lance_err: lance_core::Error = ns_err.into();
444
445        // Verify it's a Namespace error
446        match &lance_err {
447            lance_core::Error::Namespace { source, .. } => {
448                // Downcast to get the original error
449                let downcast = source.downcast_ref::<NamespaceError>();
450                assert!(downcast.is_some());
451                assert_eq!(downcast.unwrap().code(), ErrorCode::TableNotFound);
452            }
453            _ => panic!("Expected Namespace error"),
454        }
455    }
456
457    #[test]
458    fn test_error_display() {
459        let err = NamespaceError::TableNotFound {
460            message: "users".to_string(),
461        };
462        assert_eq!(err.to_string(), "Table not found: users");
463    }
464}