1use std::fmt;
5
6use arrow_schema::ArrowError;
7use snafu::{IntoError as _, Location, Snafu};
8
9type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
10
11#[cfg(feature = "backtrace")]
12mod backtrace_support {
13 use std::backtrace::Backtrace;
14
15 use snafu::{AsBacktrace, GenerateImplicitData};
16
17 #[derive(Debug)]
18 pub struct MaybeBacktrace(pub Option<Backtrace>);
19
20 impl GenerateImplicitData for MaybeBacktrace {
21 fn generate() -> Self {
22 Self(<Option<Backtrace>>::generate())
23 }
24 }
25
26 impl AsBacktrace for MaybeBacktrace {
27 fn as_backtrace(&self) -> Option<&Backtrace> {
28 self.0.as_ref()
29 }
30 }
31}
32
33#[cfg(not(feature = "backtrace"))]
34mod backtrace_support {
35 use std::backtrace::Backtrace;
36
37 use snafu::{AsBacktrace, GenerateImplicitData};
38
39 #[derive(Debug)]
40 pub struct MaybeBacktrace;
41
42 impl GenerateImplicitData for MaybeBacktrace {
43 fn generate() -> Self {
44 Self
45 }
46 }
47
48 impl AsBacktrace for MaybeBacktrace {
49 fn as_backtrace(&self) -> Option<&Backtrace> {
50 None
51 }
52 }
53}
54
55use backtrace_support::MaybeBacktrace;
56
57#[derive(Debug)]
62pub struct FieldNotFoundError {
63 pub field_name: String,
64 pub candidates: Vec<String>,
65}
66
67impl fmt::Display for FieldNotFoundError {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "Field '{}' not found.", self.field_name)?;
70 let suggestion =
71 crate::levenshtein::find_best_suggestion(&self.field_name, &self.candidates);
72 if let Some(suggestion) = suggestion {
73 write!(f, " Did you mean '{}'?", suggestion)?;
74 }
75 write!(f, "\nAvailable fields: [")?;
76 for (i, candidate) in self.candidates.iter().take(10).enumerate() {
77 if i > 0 {
78 write!(f, ", ")?;
79 }
80 write!(f, "'{}'", candidate)?;
81 }
82 if self.candidates.len() > 10 {
83 let remaining = self.candidates.len() - 10;
84 write!(f, ", ... and {} more]", remaining)?;
85 } else {
86 write!(f, "]")?;
87 }
88 Ok(())
89 }
90}
91
92impl std::error::Error for FieldNotFoundError {}
93
94#[derive(Debug)]
100pub struct CommitStatusUnknownError {
101 version: u64,
102 source: BoxedError,
103}
104
105impl CommitStatusUnknownError {
106 pub fn version(&self) -> u64 {
108 self.version
109 }
110}
111
112impl std::fmt::Display for CommitStatusUnknownError {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 write!(
115 f,
116 "Commit result for version {} is unknown: the commit may or may not have been \
117 applied; check the table state before retrying: {}",
118 self.version, self.source
119 )
120 }
121}
122
123impl std::error::Error for CommitStatusUnknownError {
124 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
125 Some(self.source.as_ref())
126 }
127}
128
129#[inline]
131pub fn box_error(e: impl std::error::Error + Send + Sync + 'static) -> BoxedError {
132 Box::new(e)
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub enum FenceReason {
139 PeerClaimedEpoch,
141 PersistenceFailure,
144}
145
146impl std::fmt::Display for FenceReason {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 let s = match self {
150 Self::PeerClaimedEpoch => "peer claimed epoch",
151 Self::PersistenceFailure => "persistence failure",
152 };
153 f.write_str(s)
154 }
155}
156
157#[derive(Debug, Snafu)]
158#[snafu(visibility(pub))]
159pub enum Error {
160 #[snafu(display("Invalid user input: {source}, {location}"))]
161 InvalidInput {
162 source: BoxedError,
163 #[snafu(implicit)]
164 location: Location,
165 #[snafu(implicit)]
166 backtrace: MaybeBacktrace,
167 },
168 #[snafu(display("Dataset already exists: {uri}, {location}"))]
169 DatasetAlreadyExists {
170 uri: String,
171 #[snafu(implicit)]
172 location: Location,
173 #[snafu(implicit)]
174 backtrace: MaybeBacktrace,
175 },
176 #[snafu(display("Append with different schema: {difference}, location: {location}"))]
177 SchemaMismatch {
178 difference: String,
179 #[snafu(implicit)]
180 location: Location,
181 #[snafu(implicit)]
182 backtrace: MaybeBacktrace,
183 },
184 #[snafu(display("Dataset at path {path} was not found: {source}, {location}"))]
185 DatasetNotFound {
186 path: String,
187 source: BoxedError,
188 #[snafu(implicit)]
189 location: Location,
190 #[snafu(implicit)]
191 backtrace: MaybeBacktrace,
192 },
193 #[snafu(display("Encountered corrupt file {path}: {source}, {location}"))]
194 CorruptFile {
195 path: object_store::path::Path,
196 source: BoxedError,
197 #[snafu(implicit)]
198 location: Location,
199 #[snafu(implicit)]
200 backtrace: MaybeBacktrace,
201 },
202 #[snafu(display("Not supported: {source}, {location}"))]
203 NotSupported {
204 source: BoxedError,
205 #[snafu(implicit)]
206 location: Location,
207 #[snafu(implicit)]
208 backtrace: MaybeBacktrace,
209 },
210 #[snafu(display("Commit conflict for version {version}: {source}, {location}"))]
211 CommitConflict {
212 version: u64,
213 source: BoxedError,
214 #[snafu(implicit)]
215 location: Location,
216 #[snafu(implicit)]
217 backtrace: MaybeBacktrace,
218 },
219 #[snafu(display("Incompatible transaction: {source}, {location}"))]
220 IncompatibleTransaction {
221 source: BoxedError,
222 #[snafu(implicit)]
223 location: Location,
224 #[snafu(implicit)]
225 backtrace: MaybeBacktrace,
226 },
227 #[snafu(display("Retryable commit conflict for version {version}: {source}, {location}"))]
228 RetryableCommitConflict {
229 version: u64,
230 source: BoxedError,
231 #[snafu(implicit)]
232 location: Location,
233 #[snafu(implicit)]
234 backtrace: MaybeBacktrace,
235 },
236 #[snafu(display("Too many concurrent writers. {message}, {location}"))]
237 TooMuchWriteContention {
238 message: String,
239 #[snafu(implicit)]
240 location: Location,
241 #[snafu(implicit)]
242 backtrace: MaybeBacktrace,
243 },
244 #[snafu(display("Operation timed out: {message}, {location}"))]
245 Timeout {
246 message: String,
247 #[snafu(implicit)]
248 location: Location,
249 },
250 #[snafu(display(
251 "Encountered internal error. Please file a bug report at https://github.com/lance-format/lance/issues. {message}, {location}"
252 ))]
253 Internal {
254 message: String,
255 #[snafu(implicit)]
256 location: Location,
257 #[snafu(implicit)]
258 backtrace: MaybeBacktrace,
259 },
260 #[snafu(display("A prerequisite task failed: {message}, {location}"))]
261 PrerequisiteFailed {
262 message: String,
263 #[snafu(implicit)]
264 location: Location,
265 #[snafu(implicit)]
266 backtrace: MaybeBacktrace,
267 },
268 #[snafu(display("Unprocessable: {message}, {location}"))]
269 Unprocessable {
270 message: String,
271 #[snafu(implicit)]
272 location: Location,
273 #[snafu(implicit)]
274 backtrace: MaybeBacktrace,
275 },
276 #[snafu(display("LanceError(Arrow): {message}, {location}"))]
277 Arrow {
278 message: String,
279 #[snafu(implicit)]
280 location: Location,
281 #[snafu(implicit)]
282 backtrace: MaybeBacktrace,
283 },
284 #[snafu(display("LanceError(Schema): {message}, {location}"))]
285 Schema {
286 message: String,
287 #[snafu(implicit)]
288 location: Location,
289 #[snafu(implicit)]
290 backtrace: MaybeBacktrace,
291 },
292 #[snafu(display("Not found: {uri}, {location}"))]
293 NotFound {
294 uri: String,
295 #[snafu(implicit)]
296 location: Location,
297 #[snafu(implicit)]
298 backtrace: MaybeBacktrace,
299 },
300 #[snafu(display("LanceError(IO): {source}, {location}"))]
301 IO {
302 source: BoxedError,
303 #[snafu(implicit)]
304 location: Location,
305 #[snafu(implicit)]
306 backtrace: MaybeBacktrace,
307 },
308 #[snafu(display("LanceError(Index): {message}, {location}"))]
309 Index {
310 message: String,
311 #[snafu(implicit)]
312 location: Location,
313 #[snafu(implicit)]
314 backtrace: MaybeBacktrace,
315 },
316 #[snafu(display("Lance index not found: {identity}, {location}"))]
317 IndexNotFound {
318 identity: String,
319 #[snafu(implicit)]
320 location: Location,
321 #[snafu(implicit)]
322 backtrace: MaybeBacktrace,
323 },
324 #[snafu(display("Cannot infer storage location from: {message}"))]
325 InvalidTableLocation { message: String },
326 Stop,
328 #[snafu(display("Wrapped error: {error}, {location}"))]
329 Wrapped {
330 #[snafu(source)]
331 error: BoxedError,
332 #[snafu(implicit)]
333 location: Location,
334 #[snafu(implicit)]
335 backtrace: MaybeBacktrace,
336 },
337 #[snafu(display("Cloned error: {message}, {location}"))]
338 Cloned {
339 message: String,
340 #[snafu(implicit)]
341 location: Location,
342 #[snafu(implicit)]
343 backtrace: MaybeBacktrace,
344 },
345 #[snafu(display("Query Execution error: {message}, {location}"))]
346 Execution {
347 message: String,
348 #[snafu(implicit)]
349 location: Location,
350 #[snafu(implicit)]
351 backtrace: MaybeBacktrace,
352 },
353 #[snafu(display("Ref is invalid: {message}"))]
354 InvalidRef { message: String },
355 #[snafu(display("Ref conflict error: {message}"))]
356 RefConflict { message: String },
357 #[snafu(display("Ref not found error: {message}"))]
358 RefNotFound { message: String },
359 #[snafu(display("Cleanup error: {message}"))]
360 Cleanup { message: String },
361 #[snafu(display("Version not found error: {message}"))]
362 VersionNotFound { message: String },
363 #[snafu(display("Version conflict error: {message}"))]
364 VersionConflict {
365 message: String,
366 major_version: u16,
367 minor_version: u16,
368 #[snafu(implicit)]
369 location: Location,
370 #[snafu(implicit)]
371 backtrace: MaybeBacktrace,
372 },
373 #[snafu(display("Namespace error: {source}, {location}"))]
374 Namespace {
375 source: BoxedError,
376 #[snafu(implicit)]
377 location: Location,
378 #[snafu(implicit)]
379 backtrace: MaybeBacktrace,
380 },
381 #[snafu(transparent)]
387 External { source: BoxedError },
388
389 #[snafu(transparent)]
391 FieldNotFound { source: FieldNotFoundError },
392
393 #[snafu(display(
394 "Spill disk cap of {cap_bytes} bytes exceeded; currently using {used_bytes} bytes, {location}"
395 ))]
396 DiskCapExceeded {
397 cap_bytes: u64,
398 used_bytes: u64,
399 #[snafu(implicit)]
400 location: Location,
401 },
402 #[snafu(display("Writer fenced ({reason}): {message}, {location}"))]
406 Fenced {
407 reason: FenceReason,
408 message: String,
409 #[snafu(implicit)]
410 location: Location,
411 },
412 #[snafu(display("Write rejected by backpressure: {message}, {location}"))]
420 Backpressure {
421 message: String,
422 #[snafu(implicit)]
423 location: Location,
424 },
425}
426
427impl Error {
428 #[cfg(feature = "backtrace")]
433 pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
434 match self {
435 Self::InvalidInput { backtrace, .. }
436 | Self::DatasetAlreadyExists { backtrace, .. }
437 | Self::SchemaMismatch { backtrace, .. }
438 | Self::DatasetNotFound { backtrace, .. }
439 | Self::CorruptFile { backtrace, .. }
440 | Self::NotSupported { backtrace, .. }
441 | Self::CommitConflict { backtrace, .. }
442 | Self::IncompatibleTransaction { backtrace, .. }
443 | Self::RetryableCommitConflict { backtrace, .. }
444 | Self::TooMuchWriteContention { backtrace, .. }
445 | Self::Internal { backtrace, .. }
446 | Self::PrerequisiteFailed { backtrace, .. }
447 | Self::Unprocessable { backtrace, .. }
448 | Self::Arrow { backtrace, .. }
449 | Self::Schema { backtrace, .. }
450 | Self::NotFound { backtrace, .. }
451 | Self::IO { backtrace, .. }
452 | Self::Index { backtrace, .. }
453 | Self::IndexNotFound { backtrace, .. }
454 | Self::Wrapped { backtrace, .. }
455 | Self::Cloned { backtrace, .. }
456 | Self::Execution { backtrace, .. }
457 | Self::VersionConflict { backtrace, .. }
458 | Self::Namespace { backtrace, .. } => {
459 use snafu::AsBacktrace;
460 backtrace.as_backtrace()
461 }
462 Self::InvalidTableLocation { .. }
465 | Self::Stop
466 | Self::InvalidRef { .. }
467 | Self::RefConflict { .. }
468 | Self::RefNotFound { .. }
469 | Self::Cleanup { .. }
470 | Self::VersionNotFound { .. }
471 | Self::External { .. }
472 | Self::FieldNotFound { .. }
473 | Self::Timeout { .. }
474 | Self::DiskCapExceeded { .. }
475 | Self::Fenced { .. }
476 | Self::Backpressure { .. } => None,
477 }
478 }
479
480 #[cfg(not(feature = "backtrace"))]
484 pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
485 None
486 }
487
488 #[track_caller]
489 pub fn corrupt_file(path: object_store::path::Path, message: impl Into<String>) -> Self {
490 CorruptFileSnafu { path }.into_error(message.into().into())
491 }
492
493 #[track_caller]
501 pub fn corrupt_file_named(name: &str, message: impl Into<String>) -> Self {
502 Self::corrupt_file(object_store::path::Path::from(name), message)
503 }
504
505 #[track_caller]
506 pub fn invalid_input(message: impl Into<String>) -> Self {
507 InvalidInputSnafu.into_error(message.into().into())
508 }
509
510 #[track_caller]
511 pub fn invalid_input_source(source: BoxedError) -> Self {
512 InvalidInputSnafu.into_error(source)
513 }
514
515 #[track_caller]
516 pub fn io(message: impl Into<String>) -> Self {
517 IOSnafu.into_error(message.into().into())
518 }
519
520 #[track_caller]
522 pub fn fenced_by_peer(message: impl Into<String>) -> Self {
523 FencedSnafu {
524 reason: FenceReason::PeerClaimedEpoch,
525 message: message.into(),
526 }
527 .build()
528 }
529
530 #[track_caller]
533 pub fn writer_poisoned(message: impl Into<String>) -> Self {
534 FencedSnafu {
535 reason: FenceReason::PersistenceFailure,
536 message: message.into(),
537 }
538 .build()
539 }
540
541 pub fn fence_reason(&self) -> Option<FenceReason> {
544 match self {
545 Self::Fenced { reason, .. } => Some(*reason),
546 _ => None,
547 }
548 }
549
550 #[track_caller]
553 pub fn backpressure(message: impl Into<String>) -> Self {
554 BackpressureSnafu {
555 message: message.into(),
556 }
557 .build()
558 }
559
560 pub fn is_backpressure(&self) -> bool {
564 matches!(self, Self::Backpressure { .. })
565 }
566
567 #[track_caller]
568 pub fn io_source(source: BoxedError) -> Self {
569 IOSnafu.into_error(source)
570 }
571
572 #[track_caller]
573 pub fn dataset_already_exists(uri: impl Into<String>) -> Self {
574 DatasetAlreadyExistsSnafu { uri: uri.into() }.build()
575 }
576
577 #[track_caller]
578 pub fn dataset_not_found(path: impl Into<String>, source: BoxedError) -> Self {
579 DatasetNotFoundSnafu { path: path.into() }.into_error(source)
580 }
581
582 #[track_caller]
583 pub fn version_conflict(
584 message: impl Into<String>,
585 major_version: u16,
586 minor_version: u16,
587 ) -> Self {
588 VersionConflictSnafu {
589 message: message.into(),
590 major_version,
591 minor_version,
592 }
593 .build()
594 }
595
596 #[track_caller]
597 pub fn not_found(uri: impl Into<String>) -> Self {
598 NotFoundSnafu { uri: uri.into() }.build()
599 }
600
601 pub fn is_not_found(&self) -> bool {
603 match self {
604 Self::NotFound { .. } => true,
605 Self::Wrapped { error, .. }
606 if error.downcast_ref::<CommitStatusUnknownError>().is_some() =>
607 {
608 false
609 }
610 Self::IO { source, .. } | Self::Wrapped { error: source, .. } => {
611 error_source_is_not_found(source.as_ref())
612 }
613 _ => false,
614 }
615 }
616
617 #[track_caller]
618 pub fn wrapped(error: BoxedError) -> Self {
619 WrappedSnafu.into_error(error)
620 }
621
622 #[track_caller]
623 pub fn schema(message: impl Into<String>) -> Self {
624 SchemaSnafu {
625 message: message.into(),
626 }
627 .build()
628 }
629
630 #[track_caller]
631 pub fn not_supported(message: impl Into<String>) -> Self {
632 NotSupportedSnafu.into_error(message.into().into())
633 }
634
635 #[track_caller]
636 pub fn not_supported_source(source: BoxedError) -> Self {
637 NotSupportedSnafu.into_error(source)
638 }
639
640 #[track_caller]
641 pub fn internal(message: impl Into<String>) -> Self {
642 InternalSnafu {
643 message: message.into(),
644 }
645 .build()
646 }
647
648 #[track_caller]
649 pub fn timeout(message: impl Into<String>) -> Self {
650 TimeoutSnafu {
651 message: message.into(),
652 }
653 .build()
654 }
655
656 #[track_caller]
657 pub fn namespace(message: impl Into<String>) -> Self {
658 NamespaceSnafu.into_error(message.into().into())
659 }
660
661 #[track_caller]
662 pub fn namespace_source(source: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
663 NamespaceSnafu.into_error(source)
664 }
665
666 #[track_caller]
667 pub fn arrow(message: impl Into<String>) -> Self {
668 ArrowSnafu {
669 message: message.into(),
670 }
671 .build()
672 }
673
674 #[track_caller]
675 pub fn execution(message: impl Into<String>) -> Self {
676 ExecutionSnafu {
677 message: message.into(),
678 }
679 .build()
680 }
681
682 #[track_caller]
683 pub fn cloned(message: impl Into<String>) -> Self {
684 ClonedSnafu {
685 message: message.into(),
686 }
687 .build()
688 }
689
690 #[track_caller]
691 pub fn schema_mismatch(difference: impl Into<String>) -> Self {
692 SchemaMismatchSnafu {
693 difference: difference.into(),
694 }
695 .build()
696 }
697
698 #[track_caller]
699 pub fn unprocessable(message: impl Into<String>) -> Self {
700 UnprocessableSnafu {
701 message: message.into(),
702 }
703 .build()
704 }
705
706 #[track_caller]
707 pub fn too_much_write_contention(message: impl Into<String>) -> Self {
708 TooMuchWriteContentionSnafu {
709 message: message.into(),
710 }
711 .build()
712 }
713
714 #[track_caller]
715 pub fn prerequisite_failed(message: impl Into<String>) -> Self {
716 PrerequisiteFailedSnafu {
717 message: message.into(),
718 }
719 .build()
720 }
721
722 #[track_caller]
723 pub fn index(message: impl Into<String>) -> Self {
724 IndexSnafu {
725 message: message.into(),
726 }
727 .build()
728 }
729
730 #[track_caller]
731 pub fn index_not_found(identity: impl Into<String>) -> Self {
732 IndexNotFoundSnafu {
733 identity: identity.into(),
734 }
735 .build()
736 }
737
738 #[track_caller]
739 pub fn commit_conflict_source(version: u64, source: BoxedError) -> Self {
740 CommitConflictSnafu { version }.into_error(source)
741 }
742
743 #[track_caller]
744 pub fn retryable_commit_conflict_source(version: u64, source: BoxedError) -> Self {
745 RetryableCommitConflictSnafu { version }.into_error(source)
746 }
747
748 #[track_caller]
749 pub fn commit_status_unknown_source(version: u64, source: BoxedError) -> Self {
750 Self::wrapped(box_error(CommitStatusUnknownError { version, source }))
751 }
752
753 pub fn is_commit_status_unknown(&self) -> bool {
756 matches!(
757 self,
758 Self::Wrapped { error, .. }
759 if error.downcast_ref::<CommitStatusUnknownError>().is_some()
760 )
761 }
762
763 #[track_caller]
764 pub fn incompatible_transaction_source(source: BoxedError) -> Self {
765 IncompatibleTransactionSnafu.into_error(source)
766 }
767
768 #[track_caller]
769 pub fn disk_cap_exceeded(cap_bytes: u64, used_bytes: u64) -> Self {
770 DiskCapExceededSnafu {
771 cap_bytes,
772 used_bytes,
773 }
774 .build()
775 }
776
777 pub fn external(source: BoxedError) -> Self {
779 Self::External { source }
780 }
781
782 pub fn field_not_found(field_name: impl Into<String>, candidates: Vec<String>) -> Self {
784 Self::FieldNotFound {
785 source: FieldNotFoundError {
786 field_name: field_name.into(),
787 candidates,
788 },
789 }
790 }
791
792 pub fn external_source(&self) -> Option<&BoxedError> {
796 match self {
797 Self::External { source } => Some(source),
798 _ => None,
799 }
800 }
801
802 pub fn into_external(self) -> std::result::Result<BoxedError, Self> {
806 match self {
807 Self::External { source } => Ok(source),
808 other => Err(other),
809 }
810 }
811}
812
813fn error_source_is_not_found(source: &(dyn std::error::Error + 'static)) -> bool {
814 if let Some(error) = source.downcast_ref::<Error>() {
815 return error.is_not_found();
816 }
817 if let Some(error) = source.downcast_ref::<object_store::Error>() {
818 return matches!(error, object_store::Error::NotFound { .. })
819 || std::error::Error::source(error).is_some_and(error_source_is_not_found);
820 }
821 source.source().is_some_and(error_source_is_not_found)
822}
823
824pub trait LanceOptionExt<T> {
825 fn expect_ok(self) -> Result<T>;
829}
830
831impl<T> LanceOptionExt<T> for Option<T> {
832 #[track_caller]
833 fn expect_ok(self) -> Result<T> {
834 self.ok_or_else(|| Error::internal("Expected option to have value"))
835 }
836}
837
838pub type Result<T> = std::result::Result<T, Error>;
839pub type ArrowResult<T> = std::result::Result<T, ArrowError>;
840#[cfg(feature = "datafusion")]
841pub type DataFusionResult<T> = std::result::Result<T, datafusion_common::DataFusionError>;
842
843impl From<ArrowError> for Error {
844 #[track_caller]
845 fn from(e: ArrowError) -> Self {
846 match e {
847 ArrowError::ExternalError(source) => {
848 match source.downcast::<Self>() {
850 Ok(lance_err) => *lance_err,
851 Err(source) => Self::External { source },
852 }
853 }
854 other => Self::arrow(other.to_string()),
855 }
856 }
857}
858
859impl From<&ArrowError> for Error {
860 #[track_caller]
861 fn from(e: &ArrowError) -> Self {
862 Self::arrow(e.to_string())
863 }
864}
865
866impl From<std::io::Error> for Error {
867 #[track_caller]
868 fn from(e: std::io::Error) -> Self {
869 if e.get_ref().is_some_and(|inner| inner.is::<Self>()) {
874 return *e
875 .into_inner()
876 .expect("checked Some above")
877 .downcast::<Self>()
878 .expect("checked type above");
879 }
880 Self::io_source(box_error(e))
881 }
882}
883
884impl From<object_store::Error> for Error {
885 #[track_caller]
886 fn from(e: object_store::Error) -> Self {
887 match e {
888 object_store::Error::NotFound { path, .. } => Self::not_found(path),
890 other => Self::io_source(box_error(other)),
891 }
892 }
893}
894
895impl From<prost::DecodeError> for Error {
896 #[track_caller]
897 fn from(e: prost::DecodeError) -> Self {
898 Self::io_source(box_error(e))
899 }
900}
901
902impl From<prost::EncodeError> for Error {
903 #[track_caller]
904 fn from(e: prost::EncodeError) -> Self {
905 Self::io_source(box_error(e))
906 }
907}
908
909impl From<prost::UnknownEnumValue> for Error {
910 #[track_caller]
911 fn from(e: prost::UnknownEnumValue) -> Self {
912 Self::io_source(box_error(e))
913 }
914}
915
916impl From<tokio::task::JoinError> for Error {
917 #[track_caller]
918 fn from(e: tokio::task::JoinError) -> Self {
919 Self::io_source(box_error(e))
920 }
921}
922
923impl From<object_store::path::Error> for Error {
924 #[track_caller]
925 fn from(e: object_store::path::Error) -> Self {
926 Self::io_source(box_error(e))
927 }
928}
929
930impl From<url::ParseError> for Error {
931 #[track_caller]
932 fn from(e: url::ParseError) -> Self {
933 Self::io_source(box_error(e))
934 }
935}
936
937impl From<serde_json::Error> for Error {
938 #[track_caller]
939 fn from(e: serde_json::Error) -> Self {
940 Self::arrow(e.to_string())
941 }
942}
943
944impl From<Error> for ArrowError {
945 fn from(value: Error) -> Self {
946 match value {
947 Error::External { source } => Self::ExternalError(source),
949 Error::Schema { message, .. } => Self::SchemaError(message),
951 e => Self::ExternalError(Box::new(e)),
953 }
954 }
955}
956
957#[cfg(feature = "datafusion")]
958impl From<datafusion_sql::sqlparser::parser::ParserError> for Error {
959 #[track_caller]
960 fn from(e: datafusion_sql::sqlparser::parser::ParserError) -> Self {
961 Self::io_source(box_error(e))
962 }
963}
964
965#[cfg(feature = "datafusion")]
966impl From<datafusion_sql::sqlparser::tokenizer::TokenizerError> for Error {
967 #[track_caller]
968 fn from(e: datafusion_sql::sqlparser::tokenizer::TokenizerError) -> Self {
969 Self::io_source(box_error(e))
970 }
971}
972
973#[cfg(feature = "datafusion")]
974impl From<Error> for datafusion_common::DataFusionError {
975 #[track_caller]
976 fn from(e: Error) -> Self {
977 Self::External(Box::new(e))
978 }
979}
980
981#[cfg(feature = "datafusion")]
982impl From<datafusion_common::DataFusionError> for Error {
983 #[track_caller]
984 fn from(e: datafusion_common::DataFusionError) -> Self {
985 match e {
986 datafusion_common::DataFusionError::Diagnostic(_, inner)
993 | datafusion_common::DataFusionError::Context(_, inner) => Self::from(*inner),
994 datafusion_common::DataFusionError::Collection(errors) => {
995 match errors.into_iter().next() {
996 Some(first) => Self::from(first),
999 None => Self::execution("DataFusion returned an empty error collection"),
1000 }
1001 }
1002 datafusion_common::DataFusionError::SQL(..)
1003 | datafusion_common::DataFusionError::Plan(..)
1004 | datafusion_common::DataFusionError::Configuration(..)
1005 | datafusion_common::DataFusionError::SchemaError(..) => {
1006 Self::invalid_input_source(box_error(e))
1007 }
1008 datafusion_common::DataFusionError::ArrowError(arrow_err, _) => Self::from(*arrow_err),
1009 datafusion_common::DataFusionError::NotImplemented(..) => {
1010 Self::not_supported_source(box_error(e))
1011 }
1012 datafusion_common::DataFusionError::Execution(..) => Self::execution(e.to_string()),
1013 datafusion_common::DataFusionError::Shared(shared) => {
1014 match std::sync::Arc::try_unwrap(shared) {
1020 Ok(inner) => Self::from(inner),
1021 Err(shared) => {
1022 let rewrapped = datafusion_common::DataFusionError::Shared(shared);
1023 Self::External {
1024 source: box_error(rewrapped),
1025 }
1026 }
1027 }
1028 }
1029 datafusion_common::DataFusionError::External(source) => {
1030 match source.downcast::<Self>() {
1032 Ok(lance_err) => *lance_err,
1033 Err(source) => Self::External { source },
1034 }
1035 }
1036 _ => Self::io_source(box_error(e)),
1037 }
1038 }
1039}
1040
1041impl From<Error> for object_store::Error {
1045 fn from(err: Error) -> Self {
1046 Self::Generic {
1047 store: "N/A",
1048 source: Box::new(err),
1049 }
1050 }
1051}
1052
1053#[track_caller]
1054pub fn get_caller_location() -> &'static std::panic::Location<'static> {
1055 std::panic::Location::caller()
1056}
1057
1058pub struct CloneableError(pub Error);
1066
1067struct DisplayError(Error);
1068
1069impl fmt::Debug for DisplayError {
1070 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1071 fmt::Display::fmt(self, f)
1072 }
1073}
1074
1075impl fmt::Display for DisplayError {
1076 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1077 fmt::Display::fmt(&self.0, f)
1078 }
1079}
1080
1081impl std::error::Error for DisplayError {
1082 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1083 Some(&self.0)
1084 }
1085}
1086
1087impl Clone for CloneableError {
1088 #[track_caller]
1089 fn clone(&self) -> Self {
1090 match &self.0 {
1091 Error::NotFound { uri, .. } => Self(Error::wrapped(Box::new(DisplayError(
1092 Error::not_found(uri.clone()),
1093 )))),
1094 error if error.is_not_found() => Self(Error::wrapped(Box::new(DisplayError(
1095 Error::not_found(error.to_string()),
1096 )))),
1097 Error::Timeout { message, .. } => Self(Error::timeout(message.clone())),
1098 Error::IO { source, .. } => Self(Error::io(source.to_string())),
1099 error => Self(Error::cloned(error.to_string())),
1100 }
1101 }
1102}
1103
1104#[derive(Clone)]
1105pub struct CloneableResult<T: Clone>(pub std::result::Result<T, CloneableError>);
1106
1107impl<T: Clone> From<Result<T>> for CloneableResult<T> {
1108 fn from(result: Result<T>) -> Self {
1109 Self(result.map_err(CloneableError))
1110 }
1111}
1112
1113#[cfg(test)]
1114mod test {
1115 use super::*;
1116 use std::error::Error as _;
1117 use std::fmt;
1118
1119 #[test]
1120 fn cloneable_error_preserves_not_found_contract() {
1121 let original = CloneableError(Error::not_found("metadata.lance"));
1122 let cloned = original.clone();
1123 let cloned_again = cloned.clone();
1124 assert!(matches!(original.0, Error::NotFound { .. }));
1125 assert!(cloned.0.is_not_found());
1126 assert!(cloned_again.0.is_not_found());
1127 assert!(cloned.0.to_string().to_lowercase().contains("not found"));
1128 assert!(
1129 cloned_again
1130 .0
1131 .to_string()
1132 .to_lowercase()
1133 .contains("not found")
1134 );
1135 assert!(
1136 format!("{:?}", cloned.0)
1137 .to_lowercase()
1138 .contains("not found")
1139 );
1140 assert!(cloned.0.source().is_some_and(|source| source.is::<Error>()
1141 || source.source().is_some_and(|source| source.is::<Error>())));
1142 let downstream_error = Error::wrapped(Box::new(Error::io_source(Box::new(
1143 object_store::Error::Generic {
1144 store: "N/A",
1145 source: Box::new(cloned.0),
1146 },
1147 ))));
1148 assert!(downstream_error.is_not_found());
1149 assert!(
1150 format!("{downstream_error:?}")
1151 .to_lowercase()
1152 .contains("not found")
1153 );
1154
1155 let original = CloneableError(Error::timeout("metadata read timed out"));
1156 let cloned = original.clone();
1157 assert!(matches!(original.0, Error::Timeout { .. }));
1158 assert!(matches!(cloned.0, Error::Timeout { .. }));
1159
1160 let original = CloneableError(Error::io("metadata read was denied"));
1161 let cloned = original.clone();
1162 assert!(matches!(original.0, Error::IO { .. }));
1163 assert!(matches!(cloned.0, Error::IO { .. }));
1164 }
1165
1166 #[test]
1167 fn test_caller_location_capture() {
1168 let current_fn = get_caller_location();
1169 let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1172 Err(object_store::Error::Generic {
1173 store: "",
1174 source: "".into(),
1175 })?;
1176 Ok(())
1177 });
1178 match f().unwrap_err() {
1179 Error::IO { location, .. } => {
1180 assert_eq!(location.line(), current_fn.line() + 4, "{}", location)
1182 }
1183 #[allow(unreachable_patterns)]
1184 _ => panic!("expected ObjectStore error"),
1185 }
1186 }
1187
1188 #[test]
1189 fn test_caller_location_capture_not_found() {
1190 let current_fn = get_caller_location();
1191 let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1192 Err(object_store::Error::NotFound {
1193 path: "some/path".to_string(),
1194 source: "not found".into(),
1195 })?;
1196 Ok(())
1197 });
1198 match f().unwrap_err() {
1199 Error::NotFound { location, .. } => {
1200 assert_eq!(location.line(), current_fn.line() + 2, "{}", location)
1202 }
1203 #[allow(unreachable_patterns)]
1204 other => panic!("expected NotFound, got {:?}", other),
1205 }
1206 }
1207
1208 #[test]
1209 fn test_object_store_not_found_converts_to_not_found() {
1210 let os_err = object_store::Error::NotFound {
1211 path: "test/path".to_string(),
1212 source: "no such file".into(),
1213 };
1214 let lance_err: Error = os_err.into();
1215 match lance_err {
1216 Error::NotFound { uri, .. } => {
1217 assert_eq!(uri, "test/path");
1218 }
1219 other => panic!("Expected NotFound, got {:?}", other),
1220 }
1221 }
1222
1223 #[derive(Debug)]
1224 struct MyCustomError {
1225 code: i32,
1226 message: String,
1227 }
1228
1229 impl fmt::Display for MyCustomError {
1230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1231 write!(f, "MyCustomError({}): {}", self.code, self.message)
1232 }
1233 }
1234
1235 impl std::error::Error for MyCustomError {}
1236
1237 #[test]
1238 fn test_io_error_recovers_wrapped_lance_error() {
1239 let io_err = std::io::Error::other(Error::disk_cap_exceeded(100, 50));
1242 let recovered: Error = io_err.into();
1243 match recovered {
1244 Error::DiskCapExceeded {
1245 cap_bytes,
1246 used_bytes,
1247 ..
1248 } => {
1249 assert_eq!(cap_bytes, 100);
1250 assert_eq!(used_bytes, 50);
1251 }
1252 other => panic!("expected DiskCapExceeded, got {other:?}"),
1253 }
1254 }
1255
1256 #[test]
1257 fn test_io_error_without_lance_error_stays_io() {
1258 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
1260 let converted: Error = io_err.into();
1261 assert!(matches!(converted, Error::IO { .. }));
1262 }
1263
1264 #[test]
1265 fn test_commit_status_unknown_is_structured_without_masking_as_not_found() {
1266 let error = Error::commit_status_unknown_source(
1267 42,
1268 box_error(Error::not_found("temporarily invisible manifest")),
1269 );
1270
1271 assert!(error.is_commit_status_unknown());
1272 assert!(!error.is_not_found());
1273 assert!(error.to_string().contains("version 42 is unknown"));
1274 let Error::Wrapped { error, .. } = error else {
1275 panic!("commit-status-unknown must use the semver-compatible wrapper")
1276 };
1277 let status = error
1278 .downcast_ref::<CommitStatusUnknownError>()
1279 .expect("wrapper must retain the typed commit status");
1280 assert_eq!(status.version(), 42);
1281 }
1282
1283 #[test]
1284 fn test_external_error_creation() {
1285 let custom_err = MyCustomError {
1286 code: 42,
1287 message: "test error".to_string(),
1288 };
1289 let err = Error::external(Box::new(custom_err));
1290
1291 match &err {
1292 Error::External { source } => {
1293 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1294 assert_eq!(recovered.code, 42);
1295 assert_eq!(recovered.message, "test error");
1296 }
1297 _ => panic!("Expected External variant"),
1298 }
1299 }
1300
1301 #[test]
1302 fn test_external_source_method() {
1303 let custom_err = MyCustomError {
1304 code: 123,
1305 message: "source test".to_string(),
1306 };
1307 let err = Error::external(Box::new(custom_err));
1308
1309 let source = err.external_source().expect("should have external source");
1310 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1311 assert_eq!(recovered.code, 123);
1312
1313 let io_err = Error::io("test");
1315 assert!(io_err.external_source().is_none());
1316 }
1317
1318 #[test]
1319 fn test_into_external_method() {
1320 let custom_err = MyCustomError {
1321 code: 456,
1322 message: "into test".to_string(),
1323 };
1324 let err = Error::external(Box::new(custom_err));
1325
1326 match err.into_external() {
1327 Ok(source) => {
1328 let recovered = source.downcast::<MyCustomError>().unwrap();
1329 assert_eq!(recovered.code, 456);
1330 }
1331 Err(_) => panic!("Expected Ok"),
1332 }
1333
1334 let io_err = Error::io("test");
1336 match io_err.into_external() {
1337 Err(Error::IO { .. }) => {}
1338 _ => panic!("Expected Err with IO variant"),
1339 }
1340 }
1341
1342 #[test]
1343 fn test_arrow_external_error_conversion() {
1344 let custom_err = MyCustomError {
1345 code: 789,
1346 message: "arrow test".to_string(),
1347 };
1348 let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1349 let lance_err: Error = arrow_err.into();
1350
1351 match lance_err {
1352 Error::External { source } => {
1353 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1354 assert_eq!(recovered.code, 789);
1355 }
1356 _ => panic!("Expected External variant, got {:?}", lance_err),
1357 }
1358 }
1359
1360 #[test]
1361 fn test_external_to_arrow_roundtrip() {
1362 let custom_err = MyCustomError {
1363 code: 999,
1364 message: "roundtrip".to_string(),
1365 };
1366 let lance_err = Error::external(Box::new(custom_err));
1367 let arrow_err: ArrowError = lance_err.into();
1368
1369 match arrow_err {
1370 ArrowError::ExternalError(source) => {
1371 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1372 assert_eq!(recovered.code, 999);
1373 }
1374 _ => panic!("Expected ExternalError variant"),
1375 }
1376 }
1377
1378 #[cfg(feature = "datafusion")]
1379 #[test]
1380 fn test_datafusion_schema_error_is_invalid_input() {
1381 use datafusion_common::Column;
1386
1387 let schema_err = datafusion_common::SchemaError::FieldNotFound {
1388 field: Box::new(Column::from_name("missing_col")),
1389 valid_fields: vec![],
1390 };
1391 let df_err =
1392 datafusion_common::DataFusionError::SchemaError(Box::new(schema_err), Box::new(None));
1393 let lance_err: Error = df_err.into();
1394
1395 match lance_err {
1396 Error::InvalidInput { .. } => {
1397 assert!(
1398 lance_err.to_string().contains("missing_col"),
1399 "expected the column name to survive in the error message, got: {lance_err}"
1400 );
1401 }
1402 _ => panic!("Expected InvalidInput variant, got {:?}", lance_err),
1403 }
1404 }
1405
1406 #[cfg(feature = "datafusion")]
1412 #[rstest::rstest]
1413 #[case::diagnostic(|inner| datafusion_common::DataFusionError::Diagnostic(
1414 Box::new(datafusion_common::Diagnostic::new_error("invalid function", None)),
1415 Box::new(inner),
1416 ))]
1417 #[case::context(|inner| datafusion_common::DataFusionError::Context(
1418 "type_coercion".to_string(),
1419 Box::new(inner),
1420 ))]
1421 #[case::collection(|inner| datafusion_common::DataFusionError::Collection(vec![inner]))]
1422 #[case::nested(|inner| datafusion_common::DataFusionError::Diagnostic(
1423 Box::new(datafusion_common::Diagnostic::new_error("invalid function", None)),
1424 Box::new(datafusion_common::DataFusionError::Context(
1425 "type_coercion".to_string(),
1426 Box::new(inner),
1427 )),
1428 ))]
1429 fn test_datafusion_wrapped_plan_error_is_invalid_input(
1430 #[case] wrap: fn(datafusion_common::DataFusionError) -> datafusion_common::DataFusionError,
1431 ) {
1432 let df_err = wrap(datafusion_common::DataFusionError::Plan(
1433 "Invalid function 'no_such_function'".to_string(),
1434 ));
1435 let lance_err = Error::from(df_err);
1436
1437 assert!(
1438 matches!(lance_err, Error::InvalidInput { .. }),
1439 "expected InvalidInput, got {lance_err:?}"
1440 );
1441 assert!(
1442 lance_err.to_string().contains("no_such_function"),
1443 "expected the function name to survive, got: {lance_err}"
1444 );
1445 }
1446
1447 #[cfg(feature = "datafusion")]
1450 #[test]
1451 fn test_datafusion_wrapped_internal_error_is_not_invalid_input() {
1452 let df_err = datafusion_common::DataFusionError::Context(
1453 "while running".to_string(),
1454 Box::new(datafusion_common::DataFusionError::Internal(
1455 "invariant violated".to_string(),
1456 )),
1457 );
1458
1459 assert!(
1460 matches!(Error::from(df_err), Error::IO { .. }),
1461 "an internal DataFusion failure must not be reported as user input"
1462 );
1463 }
1464
1465 #[cfg(feature = "datafusion")]
1468 #[test]
1469 fn test_wrapped_external_lance_error_keeps_its_category() {
1470 let df_err = datafusion_common::DataFusionError::Context(
1471 "while scanning".to_string(),
1472 Box::new(datafusion_common::DataFusionError::from(Error::io(
1473 "object store unavailable",
1474 ))),
1475 );
1476
1477 match Error::from(df_err) {
1478 Error::IO { source, .. } => assert!(
1479 source.to_string().contains("object store unavailable"),
1480 "expected the original message, got: {source}"
1481 ),
1482 other => panic!("expected the original IO error, got {other:?}"),
1483 }
1484 }
1485
1486 #[cfg(feature = "datafusion")]
1487 #[test]
1488 fn test_datafusion_external_error_conversion() {
1489 let custom_err = MyCustomError {
1490 code: 111,
1491 message: "datafusion test".to_string(),
1492 };
1493 let df_err = datafusion_common::DataFusionError::External(Box::new(custom_err));
1494 let lance_err: Error = df_err.into();
1495
1496 match lance_err {
1497 Error::External { source } => {
1498 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1499 assert_eq!(recovered.code, 111);
1500 }
1501 _ => panic!("Expected External variant"),
1502 }
1503 }
1504
1505 #[cfg(feature = "datafusion")]
1506 #[test]
1507 fn test_datafusion_arrow_external_error_conversion() {
1508 let custom_err = MyCustomError {
1510 code: 222,
1511 message: "nested test".to_string(),
1512 };
1513 let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1514 let df_err = datafusion_common::DataFusionError::ArrowError(Box::new(arrow_err), None);
1515 let lance_err: Error = df_err.into();
1516
1517 match lance_err {
1518 Error::External { source } => {
1519 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1520 assert_eq!(recovered.code, 222);
1521 }
1522 _ => panic!("Expected External variant, got {:?}", lance_err),
1523 }
1524 }
1525
1526 #[test]
1532 fn test_lance_error_roundtrip_through_arrow() {
1533 let original = Error::invalid_input("test validation error");
1534
1535 let arrow_err: ArrowError = original.into();
1537
1538 let recovered: Error = arrow_err.into();
1540
1541 match recovered {
1543 Error::InvalidInput { .. } => {
1544 assert!(recovered.to_string().contains("test validation error"));
1545 }
1546 _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1547 }
1548 }
1549
1550 #[cfg(feature = "datafusion")]
1556 #[test]
1557 fn test_lance_error_roundtrip_through_datafusion() {
1558 let original = Error::invalid_input("test validation error");
1559
1560 let df_err: datafusion_common::DataFusionError = original.into();
1562
1563 let recovered: Error = df_err.into();
1565
1566 match recovered {
1568 Error::InvalidInput { .. } => {
1569 assert!(recovered.to_string().contains("test validation error"));
1570 }
1571 _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1572 }
1573 }
1574
1575 #[cfg(feature = "datafusion")]
1581 #[test]
1582 fn test_datafusion_shared_multi_owner_preserves_type() {
1583 let custom_err = MyCustomError {
1584 code: 42,
1585 message: "shared typed error".to_string(),
1586 };
1587 let marker = datafusion_common::DataFusionError::External(Box::new(custom_err));
1588 let arc = std::sync::Arc::new(marker);
1590 let _arc2 = arc.clone();
1591 let shared = datafusion_common::DataFusionError::Shared(arc);
1592
1593 let lance_err: Error = shared.into();
1594
1595 let mut found = false;
1597 let mut src: Option<&dyn std::error::Error> = Some(&lance_err);
1598 while let Some(e) = src {
1599 if e.downcast_ref::<MyCustomError>().is_some() {
1600 found = true;
1601 break;
1602 }
1603 src = e.source();
1604 }
1605 assert!(
1606 found,
1607 "MyCustomError not found in source chain: {lance_err:?}"
1608 );
1609 }
1610
1611 #[test]
1612 fn test_backtrace_accessor() {
1613 let err = Error::io("test backtrace");
1615 let bt = err.backtrace();
1616 #[cfg(feature = "backtrace")]
1617 {
1618 let _ = bt;
1622 }
1623 #[cfg(not(feature = "backtrace"))]
1624 {
1625 assert!(bt.is_none());
1627 }
1628 }
1629
1630 #[test]
1631 fn test_backtrace_captured_when_feature_enabled() {
1632 #[cfg(feature = "backtrace")]
1641 {
1642 let err = Error::io("backtrace capture test");
1643 if std::env::var("RUST_BACKTRACE").is_ok() {
1644 assert!(
1645 err.backtrace().is_some(),
1646 "Expected a backtrace when RUST_BACKTRACE=1 and backtrace feature is enabled"
1647 );
1648 }
1649 }
1652 #[cfg(not(feature = "backtrace"))]
1653 {
1654 let err = Error::io("backtrace capture test");
1655 assert!(err.backtrace().is_none());
1656 }
1657 }
1658
1659 #[test]
1660 fn test_backtrace_returns_none_for_variants_without_location() {
1661 let err = Error::InvalidTableLocation {
1662 message: "test".to_string(),
1663 };
1664 assert!(err.backtrace().is_none());
1665
1666 let err = Error::InvalidRef {
1667 message: "test".to_string(),
1668 };
1669 assert!(err.backtrace().is_none());
1670
1671 let err = Error::Stop;
1672 assert!(err.backtrace().is_none());
1673 }
1674}