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