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    #[track_caller]
445    pub fn invalid_input(message: impl Into<String>) -> Self {
446        InvalidInputSnafu.into_error(message.into().into())
447    }
448
449    #[track_caller]
450    pub fn invalid_input_source(source: BoxedError) -> Self {
451        InvalidInputSnafu.into_error(source)
452    }
453
454    #[track_caller]
455    pub fn io(message: impl Into<String>) -> Self {
456        IOSnafu.into_error(message.into().into())
457    }
458
459    /// A successor writer claimed a higher epoch; this writer lost ownership.
460    #[track_caller]
461    pub fn fenced_by_peer(message: impl Into<String>) -> Self {
462        FencedSnafu {
463            reason: FenceReason::PeerClaimedEpoch,
464            message: message.into(),
465        }
466        .build()
467    }
468
469    /// Our WAL persistence failed; in-memory state may have diverged from the
470    /// durable WAL, so the writer must be reopened to replay.
471    #[track_caller]
472    pub fn writer_poisoned(message: impl Into<String>) -> Self {
473        FencedSnafu {
474            reason: FenceReason::PersistenceFailure,
475            message: message.into(),
476        }
477        .build()
478    }
479
480    /// The [`FenceReason`] if this is [`Error::Fenced`], else `None`. Prefer this
481    /// over matching the error message to decide how to react to a fence.
482    pub fn fence_reason(&self) -> Option<FenceReason> {
483        match self {
484            Self::Fenced { reason, .. } => Some(*reason),
485            _ => None,
486        }
487    }
488
489    #[track_caller]
490    pub fn io_source(source: BoxedError) -> Self {
491        IOSnafu.into_error(source)
492    }
493
494    #[track_caller]
495    pub fn dataset_already_exists(uri: impl Into<String>) -> Self {
496        DatasetAlreadyExistsSnafu { uri: uri.into() }.build()
497    }
498
499    #[track_caller]
500    pub fn dataset_not_found(path: impl Into<String>, source: BoxedError) -> Self {
501        DatasetNotFoundSnafu { path: path.into() }.into_error(source)
502    }
503
504    #[track_caller]
505    pub fn version_conflict(
506        message: impl Into<String>,
507        major_version: u16,
508        minor_version: u16,
509    ) -> Self {
510        VersionConflictSnafu {
511            message: message.into(),
512            major_version,
513            minor_version,
514        }
515        .build()
516    }
517
518    #[track_caller]
519    pub fn not_found(uri: impl Into<String>) -> Self {
520        NotFoundSnafu { uri: uri.into() }.build()
521    }
522
523    /// Return whether this error or one of its typed sources is a missing object.
524    pub fn is_not_found(&self) -> bool {
525        match self {
526            Self::NotFound { .. } => true,
527            Self::IO { source, .. } | Self::Wrapped { error: source, .. } => {
528                error_source_is_not_found(source.as_ref())
529            }
530            _ => false,
531        }
532    }
533
534    #[track_caller]
535    pub fn wrapped(error: BoxedError) -> Self {
536        WrappedSnafu.into_error(error)
537    }
538
539    #[track_caller]
540    pub fn schema(message: impl Into<String>) -> Self {
541        SchemaSnafu {
542            message: message.into(),
543        }
544        .build()
545    }
546
547    #[track_caller]
548    pub fn not_supported(message: impl Into<String>) -> Self {
549        NotSupportedSnafu.into_error(message.into().into())
550    }
551
552    #[track_caller]
553    pub fn not_supported_source(source: BoxedError) -> Self {
554        NotSupportedSnafu.into_error(source)
555    }
556
557    #[track_caller]
558    pub fn internal(message: impl Into<String>) -> Self {
559        InternalSnafu {
560            message: message.into(),
561        }
562        .build()
563    }
564
565    #[track_caller]
566    pub fn timeout(message: impl Into<String>) -> Self {
567        TimeoutSnafu {
568            message: message.into(),
569        }
570        .build()
571    }
572
573    #[track_caller]
574    pub fn namespace(message: impl Into<String>) -> Self {
575        NamespaceSnafu.into_error(message.into().into())
576    }
577
578    #[track_caller]
579    pub fn namespace_source(source: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
580        NamespaceSnafu.into_error(source)
581    }
582
583    #[track_caller]
584    pub fn arrow(message: impl Into<String>) -> Self {
585        ArrowSnafu {
586            message: message.into(),
587        }
588        .build()
589    }
590
591    #[track_caller]
592    pub fn execution(message: impl Into<String>) -> Self {
593        ExecutionSnafu {
594            message: message.into(),
595        }
596        .build()
597    }
598
599    #[track_caller]
600    pub fn cloned(message: impl Into<String>) -> Self {
601        ClonedSnafu {
602            message: message.into(),
603        }
604        .build()
605    }
606
607    #[track_caller]
608    pub fn schema_mismatch(difference: impl Into<String>) -> Self {
609        SchemaMismatchSnafu {
610            difference: difference.into(),
611        }
612        .build()
613    }
614
615    #[track_caller]
616    pub fn unprocessable(message: impl Into<String>) -> Self {
617        UnprocessableSnafu {
618            message: message.into(),
619        }
620        .build()
621    }
622
623    #[track_caller]
624    pub fn too_much_write_contention(message: impl Into<String>) -> Self {
625        TooMuchWriteContentionSnafu {
626            message: message.into(),
627        }
628        .build()
629    }
630
631    #[track_caller]
632    pub fn prerequisite_failed(message: impl Into<String>) -> Self {
633        PrerequisiteFailedSnafu {
634            message: message.into(),
635        }
636        .build()
637    }
638
639    #[track_caller]
640    pub fn index(message: impl Into<String>) -> Self {
641        IndexSnafu {
642            message: message.into(),
643        }
644        .build()
645    }
646
647    #[track_caller]
648    pub fn index_not_found(identity: impl Into<String>) -> Self {
649        IndexNotFoundSnafu {
650            identity: identity.into(),
651        }
652        .build()
653    }
654
655    #[track_caller]
656    pub fn commit_conflict_source(version: u64, source: BoxedError) -> Self {
657        CommitConflictSnafu { version }.into_error(source)
658    }
659
660    #[track_caller]
661    pub fn retryable_commit_conflict_source(version: u64, source: BoxedError) -> Self {
662        RetryableCommitConflictSnafu { version }.into_error(source)
663    }
664
665    #[track_caller]
666    pub fn incompatible_transaction_source(source: BoxedError) -> Self {
667        IncompatibleTransactionSnafu.into_error(source)
668    }
669
670    #[track_caller]
671    pub fn disk_cap_exceeded(cap_bytes: u64, used_bytes: u64) -> Self {
672        DiskCapExceededSnafu {
673            cap_bytes,
674            used_bytes,
675        }
676        .build()
677    }
678
679    /// Create an External error from a boxed error source.
680    pub fn external(source: BoxedError) -> Self {
681        Self::External { source }
682    }
683
684    /// Create a FieldNotFound error with the given field name and available candidates.
685    pub fn field_not_found(field_name: impl Into<String>, candidates: Vec<String>) -> Self {
686        Self::FieldNotFound {
687            source: FieldNotFoundError {
688                field_name: field_name.into(),
689                candidates,
690            },
691        }
692    }
693
694    /// Returns a reference to the external error source if this is an `External` variant.
695    ///
696    /// This allows downcasting to recover the original error type.
697    pub fn external_source(&self) -> Option<&BoxedError> {
698        match self {
699            Self::External { source } => Some(source),
700            _ => None,
701        }
702    }
703
704    /// Consumes the error and returns the external source if this is an `External` variant.
705    ///
706    /// Returns `Err(self)` if this is not an `External` variant, allowing for chained handling.
707    pub fn into_external(self) -> std::result::Result<BoxedError, Self> {
708        match self {
709            Self::External { source } => Ok(source),
710            other => Err(other),
711        }
712    }
713}
714
715fn error_source_is_not_found(source: &(dyn std::error::Error + 'static)) -> bool {
716    if let Some(error) = source.downcast_ref::<Error>() {
717        return error.is_not_found();
718    }
719    if let Some(error) = source.downcast_ref::<object_store::Error>() {
720        return matches!(error, object_store::Error::NotFound { .. })
721            || std::error::Error::source(error).is_some_and(error_source_is_not_found);
722    }
723    source.source().is_some_and(error_source_is_not_found)
724}
725
726pub trait LanceOptionExt<T> {
727    /// Unwraps an option, returning an internal error if the option is None.
728    ///
729    /// Can be used when an option is expected to have a value.
730    fn expect_ok(self) -> Result<T>;
731}
732
733impl<T> LanceOptionExt<T> for Option<T> {
734    #[track_caller]
735    fn expect_ok(self) -> Result<T> {
736        self.ok_or_else(|| Error::internal("Expected option to have value"))
737    }
738}
739
740pub type Result<T> = std::result::Result<T, Error>;
741pub type ArrowResult<T> = std::result::Result<T, ArrowError>;
742#[cfg(feature = "datafusion")]
743pub type DataFusionResult<T> = std::result::Result<T, datafusion_common::DataFusionError>;
744
745impl From<ArrowError> for Error {
746    #[track_caller]
747    fn from(e: ArrowError) -> Self {
748        match e {
749            ArrowError::ExternalError(source) => {
750                // Try to downcast to lance_core::Error first to recover the original
751                match source.downcast::<Self>() {
752                    Ok(lance_err) => *lance_err,
753                    Err(source) => Self::External { source },
754                }
755            }
756            other => Self::arrow(other.to_string()),
757        }
758    }
759}
760
761impl From<&ArrowError> for Error {
762    #[track_caller]
763    fn from(e: &ArrowError) -> Self {
764        Self::arrow(e.to_string())
765    }
766}
767
768impl From<std::io::Error> for Error {
769    #[track_caller]
770    fn from(e: std::io::Error) -> Self {
771        // A lance `Error` may have been wrapped in an `io::Error` (e.g. via
772        // `io::Error::other(Error::...)`) to cross an `AsyncWrite`/`AsyncRead`
773        // boundary. Recover it so typed errors such as `DiskCapExceeded`
774        // survive the round-trip instead of collapsing into an opaque `IO`.
775        if e.get_ref().is_some_and(|inner| inner.is::<Self>()) {
776            return *e
777                .into_inner()
778                .expect("checked Some above")
779                .downcast::<Self>()
780                .expect("checked type above");
781        }
782        Self::io_source(box_error(e))
783    }
784}
785
786impl From<object_store::Error> for Error {
787    #[track_caller]
788    fn from(e: object_store::Error) -> Self {
789        match e {
790            // source intentionally dropped; Error::NotFound carries only the path
791            object_store::Error::NotFound { path, .. } => Self::not_found(path),
792            other => Self::io_source(box_error(other)),
793        }
794    }
795}
796
797impl From<prost::DecodeError> for Error {
798    #[track_caller]
799    fn from(e: prost::DecodeError) -> Self {
800        Self::io_source(box_error(e))
801    }
802}
803
804impl From<prost::EncodeError> for Error {
805    #[track_caller]
806    fn from(e: prost::EncodeError) -> Self {
807        Self::io_source(box_error(e))
808    }
809}
810
811impl From<prost::UnknownEnumValue> for Error {
812    #[track_caller]
813    fn from(e: prost::UnknownEnumValue) -> Self {
814        Self::io_source(box_error(e))
815    }
816}
817
818impl From<tokio::task::JoinError> for Error {
819    #[track_caller]
820    fn from(e: tokio::task::JoinError) -> Self {
821        Self::io_source(box_error(e))
822    }
823}
824
825impl From<object_store::path::Error> for Error {
826    #[track_caller]
827    fn from(e: object_store::path::Error) -> Self {
828        Self::io_source(box_error(e))
829    }
830}
831
832impl From<url::ParseError> for Error {
833    #[track_caller]
834    fn from(e: url::ParseError) -> Self {
835        Self::io_source(box_error(e))
836    }
837}
838
839impl From<serde_json::Error> for Error {
840    #[track_caller]
841    fn from(e: serde_json::Error) -> Self {
842        Self::arrow(e.to_string())
843    }
844}
845
846impl From<Error> for ArrowError {
847    fn from(value: Error) -> Self {
848        match value {
849            // Pass through external errors directly
850            Error::External { source } => Self::ExternalError(source),
851            // Preserve schema errors with their specific type
852            Error::Schema { message, .. } => Self::SchemaError(message),
853            // Wrap all other lance errors so they can be recovered
854            e => Self::ExternalError(Box::new(e)),
855        }
856    }
857}
858
859#[cfg(feature = "datafusion")]
860impl From<datafusion_sql::sqlparser::parser::ParserError> for Error {
861    #[track_caller]
862    fn from(e: datafusion_sql::sqlparser::parser::ParserError) -> Self {
863        Self::io_source(box_error(e))
864    }
865}
866
867#[cfg(feature = "datafusion")]
868impl From<datafusion_sql::sqlparser::tokenizer::TokenizerError> for Error {
869    #[track_caller]
870    fn from(e: datafusion_sql::sqlparser::tokenizer::TokenizerError) -> Self {
871        Self::io_source(box_error(e))
872    }
873}
874
875#[cfg(feature = "datafusion")]
876impl From<Error> for datafusion_common::DataFusionError {
877    #[track_caller]
878    fn from(e: Error) -> Self {
879        Self::External(Box::new(e))
880    }
881}
882
883#[cfg(feature = "datafusion")]
884impl From<datafusion_common::DataFusionError> for Error {
885    #[track_caller]
886    fn from(e: datafusion_common::DataFusionError) -> Self {
887        match e {
888            datafusion_common::DataFusionError::SQL(..)
889            | datafusion_common::DataFusionError::Plan(..)
890            | datafusion_common::DataFusionError::Configuration(..)
891            | datafusion_common::DataFusionError::SchemaError(..) => {
892                Self::invalid_input_source(box_error(e))
893            }
894            datafusion_common::DataFusionError::ArrowError(arrow_err, _) => Self::from(*arrow_err),
895            datafusion_common::DataFusionError::NotImplemented(..) => {
896                Self::not_supported_source(box_error(e))
897            }
898            datafusion_common::DataFusionError::Execution(..) => Self::execution(e.to_string()),
899            datafusion_common::DataFusionError::External(source) => {
900                // Try to downcast to lance_core::Error first
901                match source.downcast::<Self>() {
902                    Ok(lance_err) => *lance_err,
903                    Err(source) => Self::External { source },
904                }
905            }
906            _ => Self::io_source(box_error(e)),
907        }
908    }
909}
910
911// This is a bit odd but some object_store functions only accept
912// Stream<Result<T, ObjectStoreError>> and so we need to convert
913// to ObjectStoreError to call the methods.
914impl From<Error> for object_store::Error {
915    fn from(err: Error) -> Self {
916        Self::Generic {
917            store: "N/A",
918            source: Box::new(err),
919        }
920    }
921}
922
923#[track_caller]
924pub fn get_caller_location() -> &'static std::panic::Location<'static> {
925    std::panic::Location::caller()
926}
927
928/// Wrap an error in a new error type that implements Clone
929///
930/// This is useful when two threads/streams share a common fallible source
931/// Definite not-found errors preserve typed source-chain detection and their
932/// human-readable representation. Timeout and I/O errors preserve their error
933/// categories. Other cloned results use Error::Cloned with the string
934/// representation of the base error.
935pub struct CloneableError(pub Error);
936
937struct DisplayError(Error);
938
939impl fmt::Debug for DisplayError {
940    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
941        fmt::Display::fmt(self, f)
942    }
943}
944
945impl fmt::Display for DisplayError {
946    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
947        fmt::Display::fmt(&self.0, f)
948    }
949}
950
951impl std::error::Error for DisplayError {
952    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
953        Some(&self.0)
954    }
955}
956
957impl Clone for CloneableError {
958    #[track_caller]
959    fn clone(&self) -> Self {
960        match &self.0 {
961            Error::NotFound { uri, .. } => Self(Error::wrapped(Box::new(DisplayError(
962                Error::not_found(uri.clone()),
963            )))),
964            error if error.is_not_found() => Self(Error::wrapped(Box::new(DisplayError(
965                Error::not_found(error.to_string()),
966            )))),
967            Error::Timeout { message, .. } => Self(Error::timeout(message.clone())),
968            Error::IO { source, .. } => Self(Error::io(source.to_string())),
969            error => Self(Error::cloned(error.to_string())),
970        }
971    }
972}
973
974#[derive(Clone)]
975pub struct CloneableResult<T: Clone>(pub std::result::Result<T, CloneableError>);
976
977impl<T: Clone> From<Result<T>> for CloneableResult<T> {
978    fn from(result: Result<T>) -> Self {
979        Self(result.map_err(CloneableError))
980    }
981}
982
983#[cfg(test)]
984mod test {
985    use super::*;
986    use std::error::Error as _;
987    use std::fmt;
988
989    #[test]
990    fn cloneable_error_preserves_not_found_contract() {
991        let original = CloneableError(Error::not_found("metadata.lance"));
992        let cloned = original.clone();
993        let cloned_again = cloned.clone();
994        assert!(matches!(original.0, Error::NotFound { .. }));
995        assert!(cloned.0.is_not_found());
996        assert!(cloned_again.0.is_not_found());
997        assert!(cloned.0.to_string().to_lowercase().contains("not found"));
998        assert!(
999            cloned_again
1000                .0
1001                .to_string()
1002                .to_lowercase()
1003                .contains("not found")
1004        );
1005        assert!(
1006            format!("{:?}", cloned.0)
1007                .to_lowercase()
1008                .contains("not found")
1009        );
1010        assert!(cloned.0.source().is_some_and(|source| source.is::<Error>()
1011            || source.source().is_some_and(|source| source.is::<Error>())));
1012        let downstream_error = Error::wrapped(Box::new(Error::io_source(Box::new(
1013            object_store::Error::Generic {
1014                store: "N/A",
1015                source: Box::new(cloned.0),
1016            },
1017        ))));
1018        assert!(downstream_error.is_not_found());
1019        assert!(
1020            format!("{downstream_error:?}")
1021                .to_lowercase()
1022                .contains("not found")
1023        );
1024
1025        let original = CloneableError(Error::timeout("metadata read timed out"));
1026        let cloned = original.clone();
1027        assert!(matches!(original.0, Error::Timeout { .. }));
1028        assert!(matches!(cloned.0, Error::Timeout { .. }));
1029
1030        let original = CloneableError(Error::io("metadata read was denied"));
1031        let cloned = original.clone();
1032        assert!(matches!(original.0, Error::IO { .. }));
1033        assert!(matches!(cloned.0, Error::IO { .. }));
1034    }
1035
1036    #[test]
1037    fn test_caller_location_capture() {
1038        let current_fn = get_caller_location();
1039        // make sure ? captures the correct location
1040        // .into() WILL NOT capture the correct location
1041        let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1042            Err(object_store::Error::Generic {
1043                store: "",
1044                source: "".into(),
1045            })?;
1046            Ok(())
1047        });
1048        match f().unwrap_err() {
1049            Error::IO { location, .. } => {
1050                // +4 is the beginning of object_store::Error::Generic...
1051                assert_eq!(location.line(), current_fn.line() + 4, "{}", location)
1052            }
1053            #[allow(unreachable_patterns)]
1054            _ => panic!("expected ObjectStore error"),
1055        }
1056    }
1057
1058    #[test]
1059    fn test_caller_location_capture_not_found() {
1060        let current_fn = get_caller_location();
1061        let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1062            Err(object_store::Error::NotFound {
1063                path: "some/path".to_string(),
1064                source: "not found".into(),
1065            })?;
1066            Ok(())
1067        });
1068        match f().unwrap_err() {
1069            Error::NotFound { location, .. } => {
1070                // +2 is the beginning of object_store::Error::NotFound...
1071                assert_eq!(location.line(), current_fn.line() + 2, "{}", location)
1072            }
1073            #[allow(unreachable_patterns)]
1074            other => panic!("expected NotFound, got {:?}", other),
1075        }
1076    }
1077
1078    #[test]
1079    fn test_object_store_not_found_converts_to_not_found() {
1080        let os_err = object_store::Error::NotFound {
1081            path: "test/path".to_string(),
1082            source: "no such file".into(),
1083        };
1084        let lance_err: Error = os_err.into();
1085        match lance_err {
1086            Error::NotFound { uri, .. } => {
1087                assert_eq!(uri, "test/path");
1088            }
1089            other => panic!("Expected NotFound, got {:?}", other),
1090        }
1091    }
1092
1093    #[derive(Debug)]
1094    struct MyCustomError {
1095        code: i32,
1096        message: String,
1097    }
1098
1099    impl fmt::Display for MyCustomError {
1100        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1101            write!(f, "MyCustomError({}): {}", self.code, self.message)
1102        }
1103    }
1104
1105    impl std::error::Error for MyCustomError {}
1106
1107    #[test]
1108    fn test_io_error_recovers_wrapped_lance_error() {
1109        // A lance Error wrapped in io::Error::other should round-trip back to
1110        // the original variant rather than collapsing into Error::IO.
1111        let io_err = std::io::Error::other(Error::disk_cap_exceeded(100, 50));
1112        let recovered: Error = io_err.into();
1113        match recovered {
1114            Error::DiskCapExceeded {
1115                cap_bytes,
1116                used_bytes,
1117                ..
1118            } => {
1119                assert_eq!(cap_bytes, 100);
1120                assert_eq!(used_bytes, 50);
1121            }
1122            other => panic!("expected DiskCapExceeded, got {other:?}"),
1123        }
1124    }
1125
1126    #[test]
1127    fn test_io_error_without_lance_error_stays_io() {
1128        // A plain io::Error (no wrapped lance Error) should become Error::IO.
1129        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
1130        let converted: Error = io_err.into();
1131        assert!(matches!(converted, Error::IO { .. }));
1132    }
1133
1134    #[test]
1135    fn test_external_error_creation() {
1136        let custom_err = MyCustomError {
1137            code: 42,
1138            message: "test error".to_string(),
1139        };
1140        let err = Error::external(Box::new(custom_err));
1141
1142        match &err {
1143            Error::External { source } => {
1144                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1145                assert_eq!(recovered.code, 42);
1146                assert_eq!(recovered.message, "test error");
1147            }
1148            _ => panic!("Expected External variant"),
1149        }
1150    }
1151
1152    #[test]
1153    fn test_external_source_method() {
1154        let custom_err = MyCustomError {
1155            code: 123,
1156            message: "source test".to_string(),
1157        };
1158        let err = Error::external(Box::new(custom_err));
1159
1160        let source = err.external_source().expect("should have external source");
1161        let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1162        assert_eq!(recovered.code, 123);
1163
1164        // Test that non-External variants return None
1165        let io_err = Error::io("test");
1166        assert!(io_err.external_source().is_none());
1167    }
1168
1169    #[test]
1170    fn test_into_external_method() {
1171        let custom_err = MyCustomError {
1172            code: 456,
1173            message: "into test".to_string(),
1174        };
1175        let err = Error::external(Box::new(custom_err));
1176
1177        match err.into_external() {
1178            Ok(source) => {
1179                let recovered = source.downcast::<MyCustomError>().unwrap();
1180                assert_eq!(recovered.code, 456);
1181            }
1182            Err(_) => panic!("Expected Ok"),
1183        }
1184
1185        // Test that non-External variants return Err(self)
1186        let io_err = Error::io("test");
1187        match io_err.into_external() {
1188            Err(Error::IO { .. }) => {}
1189            _ => panic!("Expected Err with IO variant"),
1190        }
1191    }
1192
1193    #[test]
1194    fn test_arrow_external_error_conversion() {
1195        let custom_err = MyCustomError {
1196            code: 789,
1197            message: "arrow test".to_string(),
1198        };
1199        let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1200        let lance_err: Error = arrow_err.into();
1201
1202        match lance_err {
1203            Error::External { source } => {
1204                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1205                assert_eq!(recovered.code, 789);
1206            }
1207            _ => panic!("Expected External variant, got {:?}", lance_err),
1208        }
1209    }
1210
1211    #[test]
1212    fn test_external_to_arrow_roundtrip() {
1213        let custom_err = MyCustomError {
1214            code: 999,
1215            message: "roundtrip".to_string(),
1216        };
1217        let lance_err = Error::external(Box::new(custom_err));
1218        let arrow_err: ArrowError = lance_err.into();
1219
1220        match arrow_err {
1221            ArrowError::ExternalError(source) => {
1222                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1223                assert_eq!(recovered.code, 999);
1224            }
1225            _ => panic!("Expected ExternalError variant"),
1226        }
1227    }
1228
1229    #[cfg(feature = "datafusion")]
1230    #[test]
1231    fn test_datafusion_schema_error_is_invalid_input() {
1232        // Schema errors from DataFusion (e.g., a filter referencing an unknown
1233        // column) are user-input failures, not internal lance failures. They
1234        // must surface as `Error::InvalidInput` so downstream FFI/Python
1235        // bindings can map them to the right user-facing error code.
1236        use datafusion_common::Column;
1237
1238        let schema_err = datafusion_common::SchemaError::FieldNotFound {
1239            field: Box::new(Column::from_name("missing_col")),
1240            valid_fields: vec![],
1241        };
1242        let df_err =
1243            datafusion_common::DataFusionError::SchemaError(Box::new(schema_err), Box::new(None));
1244        let lance_err: Error = df_err.into();
1245
1246        match lance_err {
1247            Error::InvalidInput { .. } => {
1248                assert!(
1249                    lance_err.to_string().contains("missing_col"),
1250                    "expected the column name to survive in the error message, got: {lance_err}"
1251                );
1252            }
1253            _ => panic!("Expected InvalidInput variant, got {:?}", lance_err),
1254        }
1255    }
1256
1257    #[cfg(feature = "datafusion")]
1258    #[test]
1259    fn test_datafusion_external_error_conversion() {
1260        let custom_err = MyCustomError {
1261            code: 111,
1262            message: "datafusion test".to_string(),
1263        };
1264        let df_err = datafusion_common::DataFusionError::External(Box::new(custom_err));
1265        let lance_err: Error = df_err.into();
1266
1267        match lance_err {
1268            Error::External { source } => {
1269                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1270                assert_eq!(recovered.code, 111);
1271            }
1272            _ => panic!("Expected External variant"),
1273        }
1274    }
1275
1276    #[cfg(feature = "datafusion")]
1277    #[test]
1278    fn test_datafusion_arrow_external_error_conversion() {
1279        // Test the nested case: ArrowError::ExternalError inside DataFusionError::ArrowError
1280        let custom_err = MyCustomError {
1281            code: 222,
1282            message: "nested test".to_string(),
1283        };
1284        let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1285        let df_err = datafusion_common::DataFusionError::ArrowError(Box::new(arrow_err), None);
1286        let lance_err: Error = df_err.into();
1287
1288        match lance_err {
1289            Error::External { source } => {
1290                let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1291                assert_eq!(recovered.code, 222);
1292            }
1293            _ => panic!("Expected External variant, got {:?}", lance_err),
1294        }
1295    }
1296
1297    /// Test that lance_core::Error round-trips through ArrowError.
1298    ///
1299    /// This simulates the case where a user defines an iterator in terms of
1300    /// lance_core::Error, and the error goes through Arrow's error type
1301    /// (e.g., via RecordBatchIterator) before being converted back.
1302    #[test]
1303    fn test_lance_error_roundtrip_through_arrow() {
1304        let original = Error::invalid_input("test validation error");
1305
1306        // Simulate what happens when using ? in an Arrow context
1307        let arrow_err: ArrowError = original.into();
1308
1309        // Convert back to lance error (as happens when Lance consumes the stream)
1310        let recovered: Error = arrow_err.into();
1311
1312        // Should get back the original lance error directly (not wrapped in External)
1313        match recovered {
1314            Error::InvalidInput { .. } => {
1315                assert!(recovered.to_string().contains("test validation error"));
1316            }
1317            _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1318        }
1319    }
1320
1321    /// Test that lance_core::Error round-trips through DataFusionError.
1322    ///
1323    /// This simulates the case where a user defines a stream in terms of
1324    /// lance_core::Error, and the error goes through DataFusion's error type
1325    /// (e.g., via SendableRecordBatchStream) before being converted back.
1326    #[cfg(feature = "datafusion")]
1327    #[test]
1328    fn test_lance_error_roundtrip_through_datafusion() {
1329        let original = Error::invalid_input("test validation error");
1330
1331        // Simulate what happens when using ? in a DataFusion context
1332        let df_err: datafusion_common::DataFusionError = original.into();
1333
1334        // Convert back to lance error (as happens when Lance consumes the stream)
1335        let recovered: Error = df_err.into();
1336
1337        // Should get back the original lance error directly (not wrapped in External)
1338        match recovered {
1339            Error::InvalidInput { .. } => {
1340                assert!(recovered.to_string().contains("test validation error"));
1341            }
1342            _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1343        }
1344    }
1345
1346    #[test]
1347    fn test_backtrace_accessor() {
1348        // Verify that backtrace() returns the expected result based on feature state
1349        let err = Error::io("test backtrace");
1350        let bt = err.backtrace();
1351        #[cfg(feature = "backtrace")]
1352        {
1353            // With the backtrace feature enabled, whether a backtrace is captured
1354            // depends on the RUST_BACKTRACE env var at runtime. We just verify
1355            // the accessor doesn't panic and returns a valid Option.
1356            let _ = bt;
1357        }
1358        #[cfg(not(feature = "backtrace"))]
1359        {
1360            // Without the backtrace feature, this must always be None.
1361            assert!(bt.is_none());
1362        }
1363    }
1364
1365    #[test]
1366    fn test_backtrace_captured_when_feature_enabled() {
1367        // Test that backtrace is actually captured when the feature is on and
1368        // RUST_BACKTRACE=1 is set in the environment before the process starts.
1369        //
1370        // NOTE: std::backtrace::Backtrace caches the RUST_BACKTRACE env check,
1371        // so set_var at runtime does not reliably enable capture. This test
1372        // verifies the accessor works correctly in both cases:
1373        // - If RUST_BACKTRACE=1 was set before the test binary started, we get Some.
1374        // - If not, we get None (even with the feature on), which is expected.
1375        #[cfg(feature = "backtrace")]
1376        {
1377            let err = Error::io("backtrace capture test");
1378            if std::env::var("RUST_BACKTRACE").is_ok() {
1379                assert!(
1380                    err.backtrace().is_some(),
1381                    "Expected a backtrace when RUST_BACKTRACE=1 and backtrace feature is enabled"
1382                );
1383            }
1384            // When RUST_BACKTRACE is not set, backtrace() may return None even
1385            // with the feature enabled — this is correct runtime gating behavior.
1386        }
1387        #[cfg(not(feature = "backtrace"))]
1388        {
1389            let err = Error::io("backtrace capture test");
1390            assert!(err.backtrace().is_none());
1391        }
1392    }
1393
1394    #[test]
1395    fn test_backtrace_returns_none_for_variants_without_location() {
1396        let err = Error::InvalidTableLocation {
1397            message: "test".to_string(),
1398        };
1399        assert!(err.backtrace().is_none());
1400
1401        let err = Error::InvalidRef {
1402            message: "test".to_string(),
1403        };
1404        assert!(err.backtrace().is_none());
1405
1406        let err = Error::Stop;
1407        assert!(err.backtrace().is_none());
1408    }
1409}