Skip to main content

lance_core/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::fmt;
5
6use arrow_schema::ArrowError;
7use snafu::{IntoError as _, Location, Snafu};
8
9type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
10
11#[cfg(feature = "backtrace")]
12mod backtrace_support {
13    use std::backtrace::Backtrace;
14
15    use snafu::{AsBacktrace, GenerateImplicitData};
16
17    #[derive(Debug)]
18    pub struct MaybeBacktrace(pub Option<Backtrace>);
19
20    impl GenerateImplicitData for MaybeBacktrace {
21        fn generate() -> Self {
22            Self(<Option<Backtrace>>::generate())
23        }
24    }
25
26    impl AsBacktrace for MaybeBacktrace {
27        fn as_backtrace(&self) -> Option<&Backtrace> {
28            self.0.as_ref()
29        }
30    }
31}
32
33#[cfg(not(feature = "backtrace"))]
34mod backtrace_support {
35    use std::backtrace::Backtrace;
36
37    use snafu::{AsBacktrace, GenerateImplicitData};
38
39    #[derive(Debug)]
40    pub struct MaybeBacktrace;
41
42    impl GenerateImplicitData for MaybeBacktrace {
43        fn generate() -> Self {
44            Self
45        }
46    }
47
48    impl AsBacktrace for MaybeBacktrace {
49        fn as_backtrace(&self) -> Option<&Backtrace> {
50            None
51        }
52    }
53}
54
55use backtrace_support::MaybeBacktrace;
56
57/// Error for when a requested field is not found in a schema.
58///
59/// This error computes suggestions lazily (only when displayed) to avoid
60/// computing Levenshtein distance when the error is created but never shown.
61#[derive(Debug)]
62pub struct FieldNotFoundError {
63    pub field_name: String,
64    pub candidates: Vec<String>,
65}
66
67impl fmt::Display for FieldNotFoundError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "Field '{}' not found.", self.field_name)?;
70        let suggestion =
71            crate::levenshtein::find_best_suggestion(&self.field_name, &self.candidates);
72        if let Some(suggestion) = suggestion {
73            write!(f, " Did you mean '{}'?", suggestion)?;
74        }
75        write!(f, "\nAvailable fields: [")?;
76        for (i, candidate) in self.candidates.iter().take(10).enumerate() {
77            if i > 0 {
78                write!(f, ", ")?;
79            }
80            write!(f, "'{}'", candidate)?;
81        }
82        if self.candidates.len() > 10 {
83            let remaining = self.candidates.len() - 10;
84            write!(f, ", ... and {} more]", remaining)?;
85        } else {
86            write!(f, "]")?;
87        }
88        Ok(())
89    }
90}
91
92impl std::error::Error for FieldNotFoundError {}
93
94/// Allocates error on the heap and then places `e` into it.
95#[inline]
96pub fn box_error(e: impl std::error::Error + Send + Sync + 'static) -> BoxedError {
97    Box::new(e)
98}
99
100/// Why a writer is fenced. Both reasons are terminal, but callers must tell them
101/// apart (a peer takeover vs. our own failure) rather than parse the message.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum FenceReason {
104    /// A successor writer claimed a higher epoch; this writer lost ownership.
105    PeerClaimedEpoch,
106    /// Our own WAL persistence failed, so in-memory state may have diverged from
107    /// the durable WAL. The writer must be reopened to replay.
108    PersistenceFailure,
109}
110
111impl std::fmt::Display for FenceReason {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        // Stable strings — surfaced in error messages.
114        let s = match self {
115            Self::PeerClaimedEpoch => "peer claimed epoch",
116            Self::PersistenceFailure => "persistence failure",
117        };
118        f.write_str(s)
119    }
120}
121
122#[derive(Debug, Snafu)]
123#[snafu(visibility(pub))]
124pub enum Error {
125    #[snafu(display("Invalid user input: {source}, {location}"))]
126    InvalidInput {
127        source: BoxedError,
128        #[snafu(implicit)]
129        location: Location,
130        #[snafu(implicit)]
131        backtrace: MaybeBacktrace,
132    },
133    #[snafu(display("Dataset already exists: {uri}, {location}"))]
134    DatasetAlreadyExists {
135        uri: String,
136        #[snafu(implicit)]
137        location: Location,
138        #[snafu(implicit)]
139        backtrace: MaybeBacktrace,
140    },
141    #[snafu(display("Append with different schema: {difference}, location: {location}"))]
142    SchemaMismatch {
143        difference: String,
144        #[snafu(implicit)]
145        location: Location,
146        #[snafu(implicit)]
147        backtrace: MaybeBacktrace,
148    },
149    #[snafu(display("Dataset at path {path} was not found: {source}, {location}"))]
150    DatasetNotFound {
151        path: String,
152        source: BoxedError,
153        #[snafu(implicit)]
154        location: Location,
155        #[snafu(implicit)]
156        backtrace: MaybeBacktrace,
157    },
158    #[snafu(display("Encountered corrupt file {path}: {source}, {location}"))]
159    CorruptFile {
160        path: object_store::path::Path,
161        source: BoxedError,
162        #[snafu(implicit)]
163        location: Location,
164        #[snafu(implicit)]
165        backtrace: MaybeBacktrace,
166    },
167    #[snafu(display("Not supported: {source}, {location}"))]
168    NotSupported {
169        source: BoxedError,
170        #[snafu(implicit)]
171        location: Location,
172        #[snafu(implicit)]
173        backtrace: MaybeBacktrace,
174    },
175    #[snafu(display("Commit conflict for version {version}: {source}, {location}"))]
176    CommitConflict {
177        version: u64,
178        source: BoxedError,
179        #[snafu(implicit)]
180        location: Location,
181        #[snafu(implicit)]
182        backtrace: MaybeBacktrace,
183    },
184    #[snafu(display("Incompatible transaction: {source}, {location}"))]
185    IncompatibleTransaction {
186        source: BoxedError,
187        #[snafu(implicit)]
188        location: Location,
189        #[snafu(implicit)]
190        backtrace: MaybeBacktrace,
191    },
192    #[snafu(display("Retryable commit conflict for version {version}: {source}, {location}"))]
193    RetryableCommitConflict {
194        version: u64,
195        source: BoxedError,
196        #[snafu(implicit)]
197        location: Location,
198        #[snafu(implicit)]
199        backtrace: MaybeBacktrace,
200    },
201    #[snafu(display("Too many concurrent writers. {message}, {location}"))]
202    TooMuchWriteContention {
203        message: String,
204        #[snafu(implicit)]
205        location: Location,
206        #[snafu(implicit)]
207        backtrace: MaybeBacktrace,
208    },
209    #[snafu(display("Operation timed out: {message}, {location}"))]
210    Timeout {
211        message: String,
212        #[snafu(implicit)]
213        location: Location,
214    },
215    #[snafu(display(
216        "Encountered internal error. Please file a bug report at https://github.com/lance-format/lance/issues. {message}, {location}"
217    ))]
218    Internal {
219        message: String,
220        #[snafu(implicit)]
221        location: Location,
222        #[snafu(implicit)]
223        backtrace: MaybeBacktrace,
224    },
225    #[snafu(display("A prerequisite task failed: {message}, {location}"))]
226    PrerequisiteFailed {
227        message: String,
228        #[snafu(implicit)]
229        location: Location,
230        #[snafu(implicit)]
231        backtrace: MaybeBacktrace,
232    },
233    #[snafu(display("Unprocessable: {message}, {location}"))]
234    Unprocessable {
235        message: String,
236        #[snafu(implicit)]
237        location: Location,
238        #[snafu(implicit)]
239        backtrace: MaybeBacktrace,
240    },
241    #[snafu(display("LanceError(Arrow): {message}, {location}"))]
242    Arrow {
243        message: String,
244        #[snafu(implicit)]
245        location: Location,
246        #[snafu(implicit)]
247        backtrace: MaybeBacktrace,
248    },
249    #[snafu(display("LanceError(Schema): {message}, {location}"))]
250    Schema {
251        message: String,
252        #[snafu(implicit)]
253        location: Location,
254        #[snafu(implicit)]
255        backtrace: MaybeBacktrace,
256    },
257    #[snafu(display("Not found: {uri}, {location}"))]
258    NotFound {
259        uri: String,
260        #[snafu(implicit)]
261        location: Location,
262        #[snafu(implicit)]
263        backtrace: MaybeBacktrace,
264    },
265    #[snafu(display("LanceError(IO): {source}, {location}"))]
266    IO {
267        source: BoxedError,
268        #[snafu(implicit)]
269        location: Location,
270        #[snafu(implicit)]
271        backtrace: MaybeBacktrace,
272    },
273    #[snafu(display("LanceError(Index): {message}, {location}"))]
274    Index {
275        message: String,
276        #[snafu(implicit)]
277        location: Location,
278        #[snafu(implicit)]
279        backtrace: MaybeBacktrace,
280    },
281    #[snafu(display("Lance index not found: {identity}, {location}"))]
282    IndexNotFound {
283        identity: String,
284        #[snafu(implicit)]
285        location: Location,
286        #[snafu(implicit)]
287        backtrace: MaybeBacktrace,
288    },
289    #[snafu(display("Cannot infer storage location from: {message}"))]
290    InvalidTableLocation { message: String },
291    /// Stream early stop
292    Stop,
293    #[snafu(display("Wrapped error: {error}, {location}"))]
294    Wrapped {
295        #[snafu(source)]
296        error: BoxedError,
297        #[snafu(implicit)]
298        location: Location,
299        #[snafu(implicit)]
300        backtrace: MaybeBacktrace,
301    },
302    #[snafu(display("Cloned error: {message}, {location}"))]
303    Cloned {
304        message: String,
305        #[snafu(implicit)]
306        location: Location,
307        #[snafu(implicit)]
308        backtrace: MaybeBacktrace,
309    },
310    #[snafu(display("Query Execution error: {message}, {location}"))]
311    Execution {
312        message: String,
313        #[snafu(implicit)]
314        location: Location,
315        #[snafu(implicit)]
316        backtrace: MaybeBacktrace,
317    },
318    #[snafu(display("Ref is invalid: {message}"))]
319    InvalidRef { message: String },
320    #[snafu(display("Ref conflict error: {message}"))]
321    RefConflict { message: String },
322    #[snafu(display("Ref not found error: {message}"))]
323    RefNotFound { message: String },
324    #[snafu(display("Cleanup error: {message}"))]
325    Cleanup { message: String },
326    #[snafu(display("Version not found error: {message}"))]
327    VersionNotFound { message: String },
328    #[snafu(display("Version conflict error: {message}"))]
329    VersionConflict {
330        message: String,
331        major_version: u16,
332        minor_version: u16,
333        #[snafu(implicit)]
334        location: Location,
335        #[snafu(implicit)]
336        backtrace: MaybeBacktrace,
337    },
338    #[snafu(display("Namespace error: {source}, {location}"))]
339    Namespace {
340        source: BoxedError,
341        #[snafu(implicit)]
342        location: Location,
343        #[snafu(implicit)]
344        backtrace: MaybeBacktrace,
345    },
346    /// External error passed through from user code.
347    ///
348    /// This variant preserves errors that users pass into Lance APIs (e.g., via streams
349    /// with custom error types). The original error can be recovered using [`Error::into_external`]
350    /// or inspected using [`Error::external_source`].
351    #[snafu(transparent)]
352    External { source: BoxedError },
353
354    /// A requested field was not found in a schema.
355    #[snafu(transparent)]
356    FieldNotFound { source: FieldNotFoundError },
357
358    #[snafu(display(
359        "Spill disk cap of {cap_bytes} bytes exceeded; currently using {used_bytes} bytes, {location}"
360    ))]
361    DiskCapExceeded {
362        cap_bytes: u64,
363        used_bytes: u64,
364        #[snafu(implicit)]
365        location: Location,
366    },
367    /// A writer has been fenced and must stop (see [`FenceReason`]). The message
368    /// keeps the `Writer fenced` prefix for legacy string consumers; new code
369    /// should match on [`Error::fence_reason`].
370    #[snafu(display("Writer fenced ({reason}): {message}, {location}"))]
371    Fenced {
372        reason: FenceReason,
373        message: String,
374        #[snafu(implicit)]
375        location: Location,
376    },
377}
378
379impl Error {
380    /// Returns the captured Rust backtrace, if available.
381    ///
382    /// Requires the `backtrace` feature to be enabled at compile time
383    /// and `RUST_BACKTRACE=1` at runtime.
384    #[cfg(feature = "backtrace")]
385    pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
386        match self {
387            Self::InvalidInput { backtrace, .. }
388            | Self::DatasetAlreadyExists { backtrace, .. }
389            | Self::SchemaMismatch { backtrace, .. }
390            | Self::DatasetNotFound { backtrace, .. }
391            | Self::CorruptFile { backtrace, .. }
392            | Self::NotSupported { backtrace, .. }
393            | Self::CommitConflict { backtrace, .. }
394            | Self::IncompatibleTransaction { backtrace, .. }
395            | Self::RetryableCommitConflict { backtrace, .. }
396            | Self::TooMuchWriteContention { backtrace, .. }
397            | Self::Internal { backtrace, .. }
398            | Self::PrerequisiteFailed { backtrace, .. }
399            | Self::Unprocessable { backtrace, .. }
400            | Self::Arrow { backtrace, .. }
401            | Self::Schema { backtrace, .. }
402            | Self::NotFound { backtrace, .. }
403            | Self::IO { backtrace, .. }
404            | Self::Index { backtrace, .. }
405            | Self::IndexNotFound { backtrace, .. }
406            | Self::Wrapped { backtrace, .. }
407            | Self::Cloned { backtrace, .. }
408            | Self::Execution { backtrace, .. }
409            | Self::VersionConflict { backtrace, .. }
410            | Self::Namespace { backtrace, .. } => {
411                use snafu::AsBacktrace;
412                backtrace.as_backtrace()
413            }
414            // Variants without a backtrace field — listed explicitly so that
415            // adding a new variant with a backtrace field triggers a compiler error.
416            Self::InvalidTableLocation { .. }
417            | Self::Stop
418            | Self::InvalidRef { .. }
419            | Self::RefConflict { .. }
420            | Self::RefNotFound { .. }
421            | Self::Cleanup { .. }
422            | Self::VersionNotFound { .. }
423            | Self::External { .. }
424            | Self::FieldNotFound { .. }
425            | Self::Timeout { .. }
426            | Self::DiskCapExceeded { .. }
427            | Self::Fenced { .. } => None,
428        }
429    }
430
431    /// Returns the captured Rust backtrace, if available.
432    ///
433    /// Always returns `None` when the `backtrace` feature is not enabled.
434    #[cfg(not(feature = "backtrace"))]
435    pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
436        None
437    }
438
439    #[track_caller]
440    pub fn corrupt_file(path: object_store::path::Path, message: impl Into<String>) -> Self {
441        CorruptFileSnafu { path }.into_error(message.into().into())
442    }
443
444    /// Reports a corrupt file when the caller only has a logical/section name
445    /// rather than the real file path (for example, a decoder that validates an
446    /// in-memory buffer and does not know where it came from).
447    ///
448    /// `name` is carried in the `path` field of the resulting [`Error::CorruptFile`]
449    /// variant and is NOT a filesystem path; callers that have the real path should
450    /// use [`Self::corrupt_file`] instead.
451    #[track_caller]
452    pub fn corrupt_file_named(name: &str, message: impl Into<String>) -> Self {
453        Self::corrupt_file(object_store::path::Path::from(name), message)
454    }
455
456    #[track_caller]
457    pub fn invalid_input(message: impl Into<String>) -> Self {
458        InvalidInputSnafu.into_error(message.into().into())
459    }
460
461    #[track_caller]
462    pub fn invalid_input_source(source: BoxedError) -> Self {
463        InvalidInputSnafu.into_error(source)
464    }
465
466    #[track_caller]
467    pub fn io(message: impl Into<String>) -> Self {
468        IOSnafu.into_error(message.into().into())
469    }
470
471    /// A successor writer claimed a higher epoch; this writer lost ownership.
472    #[track_caller]
473    pub fn fenced_by_peer(message: impl Into<String>) -> Self {
474        FencedSnafu {
475            reason: FenceReason::PeerClaimedEpoch,
476            message: message.into(),
477        }
478        .build()
479    }
480
481    /// Our WAL persistence failed; in-memory state may have diverged from the
482    /// durable WAL, so the writer must be reopened to replay.
483    #[track_caller]
484    pub fn writer_poisoned(message: impl Into<String>) -> Self {
485        FencedSnafu {
486            reason: FenceReason::PersistenceFailure,
487            message: message.into(),
488        }
489        .build()
490    }
491
492    /// The [`FenceReason`] if this is [`Error::Fenced`], else `None`. Prefer this
493    /// over matching the error message to decide how to react to a fence.
494    pub fn fence_reason(&self) -> Option<FenceReason> {
495        match self {
496            Self::Fenced { reason, .. } => Some(*reason),
497            _ => None,
498        }
499    }
500
501    #[track_caller]
502    pub fn io_source(source: BoxedError) -> Self {
503        IOSnafu.into_error(source)
504    }
505
506    #[track_caller]
507    pub fn dataset_already_exists(uri: impl Into<String>) -> Self {
508        DatasetAlreadyExistsSnafu { uri: uri.into() }.build()
509    }
510
511    #[track_caller]
512    pub fn dataset_not_found(path: impl Into<String>, source: BoxedError) -> Self {
513        DatasetNotFoundSnafu { path: path.into() }.into_error(source)
514    }
515
516    #[track_caller]
517    pub fn version_conflict(
518        message: impl Into<String>,
519        major_version: u16,
520        minor_version: u16,
521    ) -> Self {
522        VersionConflictSnafu {
523            message: message.into(),
524            major_version,
525            minor_version,
526        }
527        .build()
528    }
529
530    #[track_caller]
531    pub fn not_found(uri: impl Into<String>) -> Self {
532        NotFoundSnafu { uri: uri.into() }.build()
533    }
534
535    /// Return whether this error or one of its typed sources is a missing object.
536    pub fn is_not_found(&self) -> bool {
537        match self {
538            Self::NotFound { .. } => true,
539            Self::IO { source, .. } | Self::Wrapped { error: source, .. } => {
540                error_source_is_not_found(source.as_ref())
541            }
542            _ => false,
543        }
544    }
545
546    #[track_caller]
547    pub fn wrapped(error: BoxedError) -> Self {
548        WrappedSnafu.into_error(error)
549    }
550
551    #[track_caller]
552    pub fn schema(message: impl Into<String>) -> Self {
553        SchemaSnafu {
554            message: message.into(),
555        }
556        .build()
557    }
558
559    #[track_caller]
560    pub fn not_supported(message: impl Into<String>) -> Self {
561        NotSupportedSnafu.into_error(message.into().into())
562    }
563
564    #[track_caller]
565    pub fn not_supported_source(source: BoxedError) -> Self {
566        NotSupportedSnafu.into_error(source)
567    }
568
569    #[track_caller]
570    pub fn internal(message: impl Into<String>) -> Self {
571        InternalSnafu {
572            message: message.into(),
573        }
574        .build()
575    }
576
577    #[track_caller]
578    pub fn timeout(message: impl Into<String>) -> Self {
579        TimeoutSnafu {
580            message: message.into(),
581        }
582        .build()
583    }
584
585    #[track_caller]
586    pub fn namespace(message: impl Into<String>) -> Self {
587        NamespaceSnafu.into_error(message.into().into())
588    }
589
590    #[track_caller]
591    pub fn namespace_source(source: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
592        NamespaceSnafu.into_error(source)
593    }
594
595    #[track_caller]
596    pub fn arrow(message: impl Into<String>) -> Self {
597        ArrowSnafu {
598            message: message.into(),
599        }
600        .build()
601    }
602
603    #[track_caller]
604    pub fn execution(message: impl Into<String>) -> Self {
605        ExecutionSnafu {
606            message: message.into(),
607        }
608        .build()
609    }
610
611    #[track_caller]
612    pub fn cloned(message: impl Into<String>) -> Self {
613        ClonedSnafu {
614            message: message.into(),
615        }
616        .build()
617    }
618
619    #[track_caller]
620    pub fn schema_mismatch(difference: impl Into<String>) -> Self {
621        SchemaMismatchSnafu {
622            difference: difference.into(),
623        }
624        .build()
625    }
626
627    #[track_caller]
628    pub fn unprocessable(message: impl Into<String>) -> Self {
629        UnprocessableSnafu {
630            message: message.into(),
631        }
632        .build()
633    }
634
635    #[track_caller]
636    pub fn too_much_write_contention(message: impl Into<String>) -> Self {
637        TooMuchWriteContentionSnafu {
638            message: message.into(),
639        }
640        .build()
641    }
642
643    #[track_caller]
644    pub fn prerequisite_failed(message: impl Into<String>) -> Self {
645        PrerequisiteFailedSnafu {
646            message: message.into(),
647        }
648        .build()
649    }
650
651    #[track_caller]
652    pub fn index(message: impl Into<String>) -> Self {
653        IndexSnafu {
654            message: message.into(),
655        }
656        .build()
657    }
658
659    #[track_caller]
660    pub fn index_not_found(identity: impl Into<String>) -> Self {
661        IndexNotFoundSnafu {
662            identity: identity.into(),
663        }
664        .build()
665    }
666
667    #[track_caller]
668    pub fn commit_conflict_source(version: u64, source: BoxedError) -> Self {
669        CommitConflictSnafu { version }.into_error(source)
670    }
671
672    #[track_caller]
673    pub fn retryable_commit_conflict_source(version: u64, source: BoxedError) -> Self {
674        RetryableCommitConflictSnafu { version }.into_error(source)
675    }
676
677    #[track_caller]
678    pub fn incompatible_transaction_source(source: BoxedError) -> Self {
679        IncompatibleTransactionSnafu.into_error(source)
680    }
681
682    #[track_caller]
683    pub fn disk_cap_exceeded(cap_bytes: u64, used_bytes: u64) -> Self {
684        DiskCapExceededSnafu {
685            cap_bytes,
686            used_bytes,
687        }
688        .build()
689    }
690
691    /// Create an External error from a boxed error source.
692    pub fn external(source: BoxedError) -> Self {
693        Self::External { source }
694    }
695
696    /// Create a FieldNotFound error with the given field name and available candidates.
697    pub fn field_not_found(field_name: impl Into<String>, candidates: Vec<String>) -> Self {
698        Self::FieldNotFound {
699            source: FieldNotFoundError {
700                field_name: field_name.into(),
701                candidates,
702            },
703        }
704    }
705
706    /// Returns a reference to the external error source if this is an `External` variant.
707    ///
708    /// This allows downcasting to recover the original error type.
709    pub fn external_source(&self) -> Option<&BoxedError> {
710        match self {
711            Self::External { source } => Some(source),
712            _ => None,
713        }
714    }
715
716    /// Consumes the error and returns the external source if this is an `External` variant.
717    ///
718    /// Returns `Err(self)` if this is not an `External` variant, allowing for chained handling.
719    pub fn into_external(self) -> std::result::Result<BoxedError, Self> {
720        match self {
721            Self::External { source } => Ok(source),
722            other => Err(other),
723        }
724    }
725}
726
727fn error_source_is_not_found(source: &(dyn std::error::Error + 'static)) -> bool {
728    if let Some(error) = source.downcast_ref::<Error>() {
729        return error.is_not_found();
730    }
731    if let Some(error) = source.downcast_ref::<object_store::Error>() {
732        return matches!(error, object_store::Error::NotFound { .. })
733            || std::error::Error::source(error).is_some_and(error_source_is_not_found);
734    }
735    source.source().is_some_and(error_source_is_not_found)
736}
737
738pub trait LanceOptionExt<T> {
739    /// Unwraps an option, returning an internal error if the option is None.
740    ///
741    /// Can be used when an option is expected to have a value.
742    fn expect_ok(self) -> Result<T>;
743}
744
745impl<T> LanceOptionExt<T> for Option<T> {
746    #[track_caller]
747    fn expect_ok(self) -> Result<T> {
748        self.ok_or_else(|| Error::internal("Expected option to have value"))
749    }
750}
751
752pub type Result<T> = std::result::Result<T, Error>;
753pub type ArrowResult<T> = std::result::Result<T, ArrowError>;
754#[cfg(feature = "datafusion")]
755pub type DataFusionResult<T> = std::result::Result<T, datafusion_common::DataFusionError>;
756
757impl From<ArrowError> for Error {
758    #[track_caller]
759    fn from(e: ArrowError) -> Self {
760        match e {
761            ArrowError::ExternalError(source) => {
762                // Try to downcast to lance_core::Error first to recover the original
763                match source.downcast::<Self>() {
764                    Ok(lance_err) => *lance_err,
765                    Err(source) => Self::External { source },
766                }
767            }
768            other => Self::arrow(other.to_string()),
769        }
770    }
771}
772
773impl From<&ArrowError> for Error {
774    #[track_caller]
775    fn from(e: &ArrowError) -> Self {
776        Self::arrow(e.to_string())
777    }
778}
779
780impl From<std::io::Error> for Error {
781    #[track_caller]
782    fn from(e: std::io::Error) -> Self {
783        // A lance `Error` may have been wrapped in an `io::Error` (e.g. via
784        // `io::Error::other(Error::...)`) to cross an `AsyncWrite`/`AsyncRead`
785        // boundary. Recover it so typed errors such as `DiskCapExceeded`
786        // survive the round-trip instead of collapsing into an opaque `IO`.
787        if e.get_ref().is_some_and(|inner| inner.is::<Self>()) {
788            return *e
789                .into_inner()
790                .expect("checked Some above")
791                .downcast::<Self>()
792                .expect("checked type above");
793        }
794        Self::io_source(box_error(e))
795    }
796}
797
798impl From<object_store::Error> for Error {
799    #[track_caller]
800    fn from(e: object_store::Error) -> Self {
801        match e {
802            // source intentionally dropped; Error::NotFound carries only the path
803            object_store::Error::NotFound { path, .. } => Self::not_found(path),
804            other => Self::io_source(box_error(other)),
805        }
806    }
807}
808
809impl From<prost::DecodeError> for Error {
810    #[track_caller]
811    fn from(e: prost::DecodeError) -> Self {
812        Self::io_source(box_error(e))
813    }
814}
815
816impl From<prost::EncodeError> for Error {
817    #[track_caller]
818    fn from(e: prost::EncodeError) -> Self {
819        Self::io_source(box_error(e))
820    }
821}
822
823impl From<prost::UnknownEnumValue> for Error {
824    #[track_caller]
825    fn from(e: prost::UnknownEnumValue) -> Self {
826        Self::io_source(box_error(e))
827    }
828}
829
830impl From<tokio::task::JoinError> for Error {
831    #[track_caller]
832    fn from(e: tokio::task::JoinError) -> Self {
833        Self::io_source(box_error(e))
834    }
835}
836
837impl From<object_store::path::Error> for Error {
838    #[track_caller]
839    fn from(e: object_store::path::Error) -> Self {
840        Self::io_source(box_error(e))
841    }
842}
843
844impl From<url::ParseError> for Error {
845    #[track_caller]
846    fn from(e: url::ParseError) -> Self {
847        Self::io_source(box_error(e))
848    }
849}
850
851impl From<serde_json::Error> for Error {
852    #[track_caller]
853    fn from(e: serde_json::Error) -> Self {
854        Self::arrow(e.to_string())
855    }
856}
857
858impl From<Error> for ArrowError {
859    fn from(value: Error) -> Self {
860        match value {
861            // Pass through external errors directly
862            Error::External { source } => Self::ExternalError(source),
863            // Preserve schema errors with their specific type
864            Error::Schema { message, .. } => Self::SchemaError(message),
865            // Wrap all other lance errors so they can be recovered
866            e => Self::ExternalError(Box::new(e)),
867        }
868    }
869}
870
871#[cfg(feature = "datafusion")]
872impl From<datafusion_sql::sqlparser::parser::ParserError> for Error {
873    #[track_caller]
874    fn from(e: datafusion_sql::sqlparser::parser::ParserError) -> Self {
875        Self::io_source(box_error(e))
876    }
877}
878
879#[cfg(feature = "datafusion")]
880impl From<datafusion_sql::sqlparser::tokenizer::TokenizerError> for Error {
881    #[track_caller]
882    fn from(e: datafusion_sql::sqlparser::tokenizer::TokenizerError) -> Self {
883        Self::io_source(box_error(e))
884    }
885}
886
887#[cfg(feature = "datafusion")]
888impl From<Error> for datafusion_common::DataFusionError {
889    #[track_caller]
890    fn from(e: Error) -> Self {
891        Self::External(Box::new(e))
892    }
893}
894
895#[cfg(feature = "datafusion")]
896impl From<datafusion_common::DataFusionError> for Error {
897    #[track_caller]
898    fn from(e: datafusion_common::DataFusionError) -> Self {
899        match e {
900            datafusion_common::DataFusionError::SQL(..)
901            | datafusion_common::DataFusionError::Plan(..)
902            | datafusion_common::DataFusionError::Configuration(..)
903            | datafusion_common::DataFusionError::SchemaError(..) => {
904                Self::invalid_input_source(box_error(e))
905            }
906            datafusion_common::DataFusionError::ArrowError(arrow_err, _) => Self::from(*arrow_err),
907            datafusion_common::DataFusionError::NotImplemented(..) => {
908                Self::not_supported_source(box_error(e))
909            }
910            datafusion_common::DataFusionError::Execution(..) => Self::execution(e.to_string()),
911            datafusion_common::DataFusionError::Shared(shared) => {
912                // DataFusion shares an error across consumers (e.g. a join's
913                // build-side error fanned out to every probe partition) behind an
914                // `Arc`. If we are the sole owner we can recurse for full fidelity;
915                // otherwise the inner error can't be moved out, so we preserve its
916                // message under the execution category (its concrete type is lost).
917                match std::sync::Arc::try_unwrap(shared) {
918                    Ok(inner) => Self::from(inner),
919                    Err(shared) => Self::execution(shared.to_string()),
920                }
921            }
922            datafusion_common::DataFusionError::External(source) => {
923                // Try to downcast to lance_core::Error first
924                match source.downcast::<Self>() {
925                    Ok(lance_err) => *lance_err,
926                    Err(source) => Self::External { source },
927                }
928            }
929            _ => Self::io_source(box_error(e)),
930        }
931    }
932}
933
934// This is a bit odd but some object_store functions only accept
935// Stream<Result<T, ObjectStoreError>> and so we need to convert
936// to ObjectStoreError to call the methods.
937impl From<Error> for object_store::Error {
938    fn from(err: Error) -> Self {
939        Self::Generic {
940            store: "N/A",
941            source: Box::new(err),
942        }
943    }
944}
945
946#[track_caller]
947pub fn get_caller_location() -> &'static std::panic::Location<'static> {
948    std::panic::Location::caller()
949}
950
951/// Wrap an error in a new error type that implements Clone
952///
953/// This is useful when two threads/streams share a common fallible source
954/// Definite not-found errors preserve typed source-chain detection and their
955/// human-readable representation. Timeout and I/O errors preserve their error
956/// categories. Other cloned results use Error::Cloned with the string
957/// representation of the base error.
958pub struct CloneableError(pub Error);
959
960struct DisplayError(Error);
961
962impl fmt::Debug for DisplayError {
963    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
964        fmt::Display::fmt(self, f)
965    }
966}
967
968impl fmt::Display for DisplayError {
969    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
970        fmt::Display::fmt(&self.0, f)
971    }
972}
973
974impl std::error::Error for DisplayError {
975    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
976        Some(&self.0)
977    }
978}
979
980impl Clone for CloneableError {
981    #[track_caller]
982    fn clone(&self) -> Self {
983        match &self.0 {
984            Error::NotFound { uri, .. } => Self(Error::wrapped(Box::new(DisplayError(
985                Error::not_found(uri.clone()),
986            )))),
987            error if error.is_not_found() => Self(Error::wrapped(Box::new(DisplayError(
988                Error::not_found(error.to_string()),
989            )))),
990            Error::Timeout { message, .. } => Self(Error::timeout(message.clone())),
991            Error::IO { source, .. } => Self(Error::io(source.to_string())),
992            error => Self(Error::cloned(error.to_string())),
993        }
994    }
995}
996
997#[derive(Clone)]
998pub struct CloneableResult<T: Clone>(pub std::result::Result<T, CloneableError>);
999
1000impl<T: Clone> From<Result<T>> for CloneableResult<T> {
1001    fn from(result: Result<T>) -> Self {
1002        Self(result.map_err(CloneableError))
1003    }
1004}
1005
1006#[cfg(test)]
1007mod test {
1008    use super::*;
1009    use std::error::Error as _;
1010    use std::fmt;
1011
1012    #[test]
1013    fn cloneable_error_preserves_not_found_contract() {
1014        let original = CloneableError(Error::not_found("metadata.lance"));
1015        let cloned = original.clone();
1016        let cloned_again = cloned.clone();
1017        assert!(matches!(original.0, Error::NotFound { .. }));
1018        assert!(cloned.0.is_not_found());
1019        assert!(cloned_again.0.is_not_found());
1020        assert!(cloned.0.to_string().to_lowercase().contains("not found"));
1021        assert!(
1022            cloned_again
1023                .0
1024                .to_string()
1025                .to_lowercase()
1026                .contains("not found")
1027        );
1028        assert!(
1029            format!("{:?}", cloned.0)
1030                .to_lowercase()
1031                .contains("not found")
1032        );
1033        assert!(cloned.0.source().is_some_and(|source| source.is::<Error>()
1034            || source.source().is_some_and(|source| source.is::<Error>())));
1035        let downstream_error = Error::wrapped(Box::new(Error::io_source(Box::new(
1036            object_store::Error::Generic {
1037                store: "N/A",
1038                source: Box::new(cloned.0),
1039            },
1040        ))));
1041        assert!(downstream_error.is_not_found());
1042        assert!(
1043            format!("{downstream_error:?}")
1044                .to_lowercase()
1045                .contains("not found")
1046        );
1047
1048        let original = CloneableError(Error::timeout("metadata read timed out"));
1049        let cloned = original.clone();
1050        assert!(matches!(original.0, Error::Timeout { .. }));
1051        assert!(matches!(cloned.0, Error::Timeout { .. }));
1052
1053        let original = CloneableError(Error::io("metadata read was denied"));
1054        let cloned = original.clone();
1055        assert!(matches!(original.0, Error::IO { .. }));
1056        assert!(matches!(cloned.0, Error::IO { .. }));
1057    }
1058
1059    #[test]
1060    fn test_caller_location_capture() {
1061        let current_fn = get_caller_location();
1062        // make sure ? captures the correct location
1063        // .into() WILL NOT capture the correct location
1064        let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1065            Err(object_store::Error::Generic {
1066                store: "",
1067                source: "".into(),
1068            })?;
1069            Ok(())
1070        });
1071        match f().unwrap_err() {
1072            Error::IO { location, .. } => {
1073                // +4 is the beginning of object_store::Error::Generic...
1074                assert_eq!(location.line(), current_fn.line() + 4, "{}", location)
1075            }
1076            #[allow(unreachable_patterns)]
1077            _ => panic!("expected ObjectStore error"),
1078        }
1079    }
1080
1081    #[test]
1082    fn test_caller_location_capture_not_found() {
1083        let current_fn = get_caller_location();
1084        let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1085            Err(object_store::Error::NotFound {
1086                path: "some/path".to_string(),
1087                source: "not found".into(),
1088            })?;
1089            Ok(())
1090        });
1091        match f().unwrap_err() {
1092            Error::NotFound { location, .. } => {
1093                // +2 is the beginning of object_store::Error::NotFound...
1094                assert_eq!(location.line(), current_fn.line() + 2, "{}", location)
1095            }
1096            #[allow(unreachable_patterns)]
1097            other => panic!("expected NotFound, got {:?}", other),
1098        }
1099    }
1100
1101    #[test]
1102    fn test_object_store_not_found_converts_to_not_found() {
1103        let os_err = object_store::Error::NotFound {
1104            path: "test/path".to_string(),
1105            source: "no such file".into(),
1106        };
1107        let lance_err: Error = os_err.into();
1108        match lance_err {
1109            Error::NotFound { uri, .. } => {
1110                assert_eq!(uri, "test/path");
1111            }
1112            other => panic!("Expected NotFound, got {:?}", other),
1113        }
1114    }
1115
1116    #[derive(Debug)]
1117    struct MyCustomError {
1118        code: i32,
1119        message: String,
1120    }
1121
1122    impl fmt::Display for MyCustomError {
1123        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1124            write!(f, "MyCustomError({}): {}", self.code, self.message)
1125        }
1126    }
1127
1128    impl std::error::Error for MyCustomError {}
1129
1130    #[test]
1131    fn test_io_error_recovers_wrapped_lance_error() {
1132        // A lance Error wrapped in io::Error::other should round-trip back to
1133        // the original variant rather than collapsing into Error::IO.
1134        let io_err = std::io::Error::other(Error::disk_cap_exceeded(100, 50));
1135        let recovered: Error = io_err.into();
1136        match recovered {
1137            Error::DiskCapExceeded {
1138                cap_bytes,
1139                used_bytes,
1140                ..
1141            } => {
1142                assert_eq!(cap_bytes, 100);
1143                assert_eq!(used_bytes, 50);
1144            }
1145            other => panic!("expected DiskCapExceeded, got {other:?}"),
1146        }
1147    }
1148
1149    #[test]
1150    fn test_io_error_without_lance_error_stays_io() {
1151        // A plain io::Error (no wrapped lance Error) should become Error::IO.
1152        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
1153        let converted: Error = io_err.into();
1154        assert!(matches!(converted, Error::IO { .. }));
1155    }
1156
1157    #[test]
1158    fn test_external_error_creation() {
1159        let custom_err = MyCustomError {
1160            code: 42,
1161            message: "test error".to_string(),
1162        };
1163        let err = Error::external(Box::new(custom_err));
1164
1165        match &err {
1166            Error::External { source } => {
1167                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1168                assert_eq!(recovered.code, 42);
1169                assert_eq!(recovered.message, "test error");
1170            }
1171            _ => panic!("Expected External variant"),
1172        }
1173    }
1174
1175    #[test]
1176    fn test_external_source_method() {
1177        let custom_err = MyCustomError {
1178            code: 123,
1179            message: "source test".to_string(),
1180        };
1181        let err = Error::external(Box::new(custom_err));
1182
1183        let source = err.external_source().expect("should have external source");
1184        let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1185        assert_eq!(recovered.code, 123);
1186
1187        // Test that non-External variants return None
1188        let io_err = Error::io("test");
1189        assert!(io_err.external_source().is_none());
1190    }
1191
1192    #[test]
1193    fn test_into_external_method() {
1194        let custom_err = MyCustomError {
1195            code: 456,
1196            message: "into test".to_string(),
1197        };
1198        let err = Error::external(Box::new(custom_err));
1199
1200        match err.into_external() {
1201            Ok(source) => {
1202                let recovered = source.downcast::<MyCustomError>().unwrap();
1203                assert_eq!(recovered.code, 456);
1204            }
1205            Err(_) => panic!("Expected Ok"),
1206        }
1207
1208        // Test that non-External variants return Err(self)
1209        let io_err = Error::io("test");
1210        match io_err.into_external() {
1211            Err(Error::IO { .. }) => {}
1212            _ => panic!("Expected Err with IO variant"),
1213        }
1214    }
1215
1216    #[test]
1217    fn test_arrow_external_error_conversion() {
1218        let custom_err = MyCustomError {
1219            code: 789,
1220            message: "arrow test".to_string(),
1221        };
1222        let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1223        let lance_err: Error = arrow_err.into();
1224
1225        match lance_err {
1226            Error::External { source } => {
1227                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1228                assert_eq!(recovered.code, 789);
1229            }
1230            _ => panic!("Expected External variant, got {:?}", lance_err),
1231        }
1232    }
1233
1234    #[test]
1235    fn test_external_to_arrow_roundtrip() {
1236        let custom_err = MyCustomError {
1237            code: 999,
1238            message: "roundtrip".to_string(),
1239        };
1240        let lance_err = Error::external(Box::new(custom_err));
1241        let arrow_err: ArrowError = lance_err.into();
1242
1243        match arrow_err {
1244            ArrowError::ExternalError(source) => {
1245                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1246                assert_eq!(recovered.code, 999);
1247            }
1248            _ => panic!("Expected ExternalError variant"),
1249        }
1250    }
1251
1252    #[cfg(feature = "datafusion")]
1253    #[test]
1254    fn test_datafusion_schema_error_is_invalid_input() {
1255        // Schema errors from DataFusion (e.g., a filter referencing an unknown
1256        // column) are user-input failures, not internal lance failures. They
1257        // must surface as `Error::InvalidInput` so downstream FFI/Python
1258        // bindings can map them to the right user-facing error code.
1259        use datafusion_common::Column;
1260
1261        let schema_err = datafusion_common::SchemaError::FieldNotFound {
1262            field: Box::new(Column::from_name("missing_col")),
1263            valid_fields: vec![],
1264        };
1265        let df_err =
1266            datafusion_common::DataFusionError::SchemaError(Box::new(schema_err), Box::new(None));
1267        let lance_err: Error = df_err.into();
1268
1269        match lance_err {
1270            Error::InvalidInput { .. } => {
1271                assert!(
1272                    lance_err.to_string().contains("missing_col"),
1273                    "expected the column name to survive in the error message, got: {lance_err}"
1274                );
1275            }
1276            _ => panic!("Expected InvalidInput variant, got {:?}", lance_err),
1277        }
1278    }
1279
1280    #[cfg(feature = "datafusion")]
1281    #[test]
1282    fn test_datafusion_external_error_conversion() {
1283        let custom_err = MyCustomError {
1284            code: 111,
1285            message: "datafusion test".to_string(),
1286        };
1287        let df_err = datafusion_common::DataFusionError::External(Box::new(custom_err));
1288        let lance_err: Error = df_err.into();
1289
1290        match lance_err {
1291            Error::External { source } => {
1292                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1293                assert_eq!(recovered.code, 111);
1294            }
1295            _ => panic!("Expected External variant"),
1296        }
1297    }
1298
1299    #[cfg(feature = "datafusion")]
1300    #[test]
1301    fn test_datafusion_arrow_external_error_conversion() {
1302        // Test the nested case: ArrowError::ExternalError inside DataFusionError::ArrowError
1303        let custom_err = MyCustomError {
1304            code: 222,
1305            message: "nested test".to_string(),
1306        };
1307        let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1308        let df_err = datafusion_common::DataFusionError::ArrowError(Box::new(arrow_err), None);
1309        let lance_err: Error = df_err.into();
1310
1311        match lance_err {
1312            Error::External { source } => {
1313                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1314                assert_eq!(recovered.code, 222);
1315            }
1316            _ => panic!("Expected External variant, got {:?}", lance_err),
1317        }
1318    }
1319
1320    /// Test that lance_core::Error round-trips through ArrowError.
1321    ///
1322    /// This simulates the case where a user defines an iterator in terms of
1323    /// lance_core::Error, and the error goes through Arrow's error type
1324    /// (e.g., via RecordBatchIterator) before being converted back.
1325    #[test]
1326    fn test_lance_error_roundtrip_through_arrow() {
1327        let original = Error::invalid_input("test validation error");
1328
1329        // Simulate what happens when using ? in an Arrow context
1330        let arrow_err: ArrowError = original.into();
1331
1332        // Convert back to lance error (as happens when Lance consumes the stream)
1333        let recovered: Error = arrow_err.into();
1334
1335        // Should get back the original lance error directly (not wrapped in External)
1336        match recovered {
1337            Error::InvalidInput { .. } => {
1338                assert!(recovered.to_string().contains("test validation error"));
1339            }
1340            _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1341        }
1342    }
1343
1344    /// Test that lance_core::Error round-trips through DataFusionError.
1345    ///
1346    /// This simulates the case where a user defines a stream in terms of
1347    /// lance_core::Error, and the error goes through DataFusion's error type
1348    /// (e.g., via SendableRecordBatchStream) before being converted back.
1349    #[cfg(feature = "datafusion")]
1350    #[test]
1351    fn test_lance_error_roundtrip_through_datafusion() {
1352        let original = Error::invalid_input("test validation error");
1353
1354        // Simulate what happens when using ? in a DataFusion context
1355        let df_err: datafusion_common::DataFusionError = original.into();
1356
1357        // Convert back to lance error (as happens when Lance consumes the stream)
1358        let recovered: Error = df_err.into();
1359
1360        // Should get back the original lance error directly (not wrapped in External)
1361        match recovered {
1362            Error::InvalidInput { .. } => {
1363                assert!(recovered.to_string().contains("test validation error"));
1364            }
1365            _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1366        }
1367    }
1368
1369    #[test]
1370    fn test_backtrace_accessor() {
1371        // Verify that backtrace() returns the expected result based on feature state
1372        let err = Error::io("test backtrace");
1373        let bt = err.backtrace();
1374        #[cfg(feature = "backtrace")]
1375        {
1376            // With the backtrace feature enabled, whether a backtrace is captured
1377            // depends on the RUST_BACKTRACE env var at runtime. We just verify
1378            // the accessor doesn't panic and returns a valid Option.
1379            let _ = bt;
1380        }
1381        #[cfg(not(feature = "backtrace"))]
1382        {
1383            // Without the backtrace feature, this must always be None.
1384            assert!(bt.is_none());
1385        }
1386    }
1387
1388    #[test]
1389    fn test_backtrace_captured_when_feature_enabled() {
1390        // Test that backtrace is actually captured when the feature is on and
1391        // RUST_BACKTRACE=1 is set in the environment before the process starts.
1392        //
1393        // NOTE: std::backtrace::Backtrace caches the RUST_BACKTRACE env check,
1394        // so set_var at runtime does not reliably enable capture. This test
1395        // verifies the accessor works correctly in both cases:
1396        // - If RUST_BACKTRACE=1 was set before the test binary started, we get Some.
1397        // - If not, we get None (even with the feature on), which is expected.
1398        #[cfg(feature = "backtrace")]
1399        {
1400            let err = Error::io("backtrace capture test");
1401            if std::env::var("RUST_BACKTRACE").is_ok() {
1402                assert!(
1403                    err.backtrace().is_some(),
1404                    "Expected a backtrace when RUST_BACKTRACE=1 and backtrace feature is enabled"
1405                );
1406            }
1407            // When RUST_BACKTRACE is not set, backtrace() may return None even
1408            // with the feature enabled — this is correct runtime gating behavior.
1409        }
1410        #[cfg(not(feature = "backtrace"))]
1411        {
1412            let err = Error::io("backtrace capture test");
1413            assert!(err.backtrace().is_none());
1414        }
1415    }
1416
1417    #[test]
1418    fn test_backtrace_returns_none_for_variants_without_location() {
1419        let err = Error::InvalidTableLocation {
1420            message: "test".to_string(),
1421        };
1422        assert!(err.backtrace().is_none());
1423
1424        let err = Error::InvalidRef {
1425            message: "test".to_string(),
1426        };
1427        assert!(err.backtrace().is_none());
1428
1429        let err = Error::Stop;
1430        assert!(err.backtrace().is_none());
1431    }
1432}