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