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