1use 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#[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#[derive(Debug)]
101pub struct CommitStatusUnknownError {
102 version: u64,
103 source: BoxedError,
104}
105
106impl CommitStatusUnknownError {
107 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#[inline]
132pub fn box_error(e: impl std::error::Error + Send + Sync + 'static) -> BoxedError {
133 Box::new(e)
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
139pub enum FenceReason {
140 PeerClaimedEpoch,
142 PersistenceFailure,
145}
146
147impl std::fmt::Display for FenceReason {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 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 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 #[snafu(transparent)]
388 External { source: BoxedError },
389
390 #[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 #[snafu(display("Writer fenced ({reason}): {message}, {location}"))]
407 Fenced {
408 reason: FenceReason,
409 message: String,
410 #[snafu(implicit)]
411 location: Location,
412 },
413 #[snafu(display("Write rejected by backpressure: {message}, {location}"))]
421 Backpressure {
422 message: String,
423 #[snafu(implicit)]
424 location: Location,
425 },
426 #[snafu(transparent)]
433 Shared { source: Arc<Self> },
434}
435
436impl Error {
437 #[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 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 Self::Shared { source } => source.backtrace(),
488 }
489 }
490
491 #[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 #[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 #[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 #[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 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 pub fn fence_reason(&self) -> Option<FenceReason> {
570 match self.find_root() {
571 Self::Fenced { reason, .. } => Some(*reason),
572 _ => None,
573 }
574 }
575
576 #[track_caller]
579 pub fn backpressure(message: impl Into<String>) -> Self {
580 BackpressureSnafu {
581 message: message.into(),
582 }
583 .build()
584 }
585
586 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 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 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 pub fn external(source: BoxedError) -> Self {
805 Self::External { source }
806 }
807
808 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 pub fn external_source(&self) -> Option<&BoxedError> {
823 match self.find_root() {
824 Self::External { source } => Some(source),
825 _ => None,
826 }
827 }
828
829 pub fn into_external(self) -> std::result::Result<BoxedError, Self> {
836 match self {
837 Self::External { source } => Ok(source),
838 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 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 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 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 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 Error::External { source } => Self::ExternalError(source),
985 Error::Schema { message, .. } => Self::SchemaError(message),
987 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_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 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 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 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
1077impl 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
1094pub struct CloneableError(Arc<Error>);
1104
1105impl CloneableError {
1106 pub fn new(error: Error) -> Self {
1112 Self(match error {
1113 Error::Shared { source } => source,
1114 other => Arc::new(other),
1115 })
1116 }
1117
1118 pub fn inner(&self) -> &Error {
1120 &self.0
1121 }
1122
1123 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 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 #[test]
1158 fn cloneable_error_preserves_accessor_contracts() {
1159 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 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 #[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 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 #[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 #[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 #[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(); 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 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]
1720 fn test_lance_error_roundtrip_through_arrow() {
1721 let original = Error::invalid_input("test validation error");
1722
1723 let arrow_err: ArrowError = original.into();
1725
1726 let recovered: Error = arrow_err.into();
1728
1729 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 #[cfg(feature = "datafusion")]
1744 #[test]
1745 fn test_lance_error_roundtrip_through_datafusion() {
1746 let original = Error::invalid_input("test validation error");
1747
1748 let df_err: datafusion_common::DataFusionError = original.into();
1750
1751 let recovered: Error = df_err.into();
1753
1754 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 #[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 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 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 let err = Error::io("test backtrace");
1803 let bt = err.backtrace();
1804 #[cfg(feature = "backtrace")]
1805 {
1806 let _ = bt;
1810 }
1811 #[cfg(not(feature = "backtrace"))]
1812 {
1813 assert!(bt.is_none());
1815 }
1816 }
1817
1818 #[test]
1819 fn test_backtrace_captured_when_feature_enabled() {
1820 #[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 }
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}