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}
413
414impl Error {
415 #[cfg(feature = "backtrace")]
420 pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
421 match self {
422 Self::InvalidInput { backtrace, .. }
423 | Self::DatasetAlreadyExists { backtrace, .. }
424 | Self::SchemaMismatch { backtrace, .. }
425 | Self::DatasetNotFound { backtrace, .. }
426 | Self::CorruptFile { backtrace, .. }
427 | Self::NotSupported { backtrace, .. }
428 | Self::CommitConflict { backtrace, .. }
429 | Self::IncompatibleTransaction { backtrace, .. }
430 | Self::RetryableCommitConflict { backtrace, .. }
431 | Self::TooMuchWriteContention { backtrace, .. }
432 | Self::Internal { backtrace, .. }
433 | Self::PrerequisiteFailed { backtrace, .. }
434 | Self::Unprocessable { backtrace, .. }
435 | Self::Arrow { backtrace, .. }
436 | Self::Schema { backtrace, .. }
437 | Self::NotFound { backtrace, .. }
438 | Self::IO { backtrace, .. }
439 | Self::Index { backtrace, .. }
440 | Self::IndexNotFound { backtrace, .. }
441 | Self::Wrapped { backtrace, .. }
442 | Self::Cloned { backtrace, .. }
443 | Self::Execution { backtrace, .. }
444 | Self::VersionConflict { backtrace, .. }
445 | Self::Namespace { backtrace, .. } => {
446 use snafu::AsBacktrace;
447 backtrace.as_backtrace()
448 }
449 Self::InvalidTableLocation { .. }
452 | Self::Stop
453 | Self::InvalidRef { .. }
454 | Self::RefConflict { .. }
455 | Self::RefNotFound { .. }
456 | Self::Cleanup { .. }
457 | Self::VersionNotFound { .. }
458 | Self::External { .. }
459 | Self::FieldNotFound { .. }
460 | Self::Timeout { .. }
461 | Self::DiskCapExceeded { .. }
462 | Self::Fenced { .. } => None,
463 }
464 }
465
466 #[cfg(not(feature = "backtrace"))]
470 pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
471 None
472 }
473
474 #[track_caller]
475 pub fn corrupt_file(path: object_store::path::Path, message: impl Into<String>) -> Self {
476 CorruptFileSnafu { path }.into_error(message.into().into())
477 }
478
479 #[track_caller]
487 pub fn corrupt_file_named(name: &str, message: impl Into<String>) -> Self {
488 Self::corrupt_file(object_store::path::Path::from(name), message)
489 }
490
491 #[track_caller]
492 pub fn invalid_input(message: impl Into<String>) -> Self {
493 InvalidInputSnafu.into_error(message.into().into())
494 }
495
496 #[track_caller]
497 pub fn invalid_input_source(source: BoxedError) -> Self {
498 InvalidInputSnafu.into_error(source)
499 }
500
501 #[track_caller]
502 pub fn io(message: impl Into<String>) -> Self {
503 IOSnafu.into_error(message.into().into())
504 }
505
506 #[track_caller]
508 pub fn fenced_by_peer(message: impl Into<String>) -> Self {
509 FencedSnafu {
510 reason: FenceReason::PeerClaimedEpoch,
511 message: message.into(),
512 }
513 .build()
514 }
515
516 #[track_caller]
519 pub fn writer_poisoned(message: impl Into<String>) -> Self {
520 FencedSnafu {
521 reason: FenceReason::PersistenceFailure,
522 message: message.into(),
523 }
524 .build()
525 }
526
527 pub fn fence_reason(&self) -> Option<FenceReason> {
530 match self {
531 Self::Fenced { reason, .. } => Some(*reason),
532 _ => None,
533 }
534 }
535
536 #[track_caller]
537 pub fn io_source(source: BoxedError) -> Self {
538 IOSnafu.into_error(source)
539 }
540
541 #[track_caller]
542 pub fn dataset_already_exists(uri: impl Into<String>) -> Self {
543 DatasetAlreadyExistsSnafu { uri: uri.into() }.build()
544 }
545
546 #[track_caller]
547 pub fn dataset_not_found(path: impl Into<String>, source: BoxedError) -> Self {
548 DatasetNotFoundSnafu { path: path.into() }.into_error(source)
549 }
550
551 #[track_caller]
552 pub fn version_conflict(
553 message: impl Into<String>,
554 major_version: u16,
555 minor_version: u16,
556 ) -> Self {
557 VersionConflictSnafu {
558 message: message.into(),
559 major_version,
560 minor_version,
561 }
562 .build()
563 }
564
565 #[track_caller]
566 pub fn not_found(uri: impl Into<String>) -> Self {
567 NotFoundSnafu { uri: uri.into() }.build()
568 }
569
570 pub fn is_not_found(&self) -> bool {
572 match self {
573 Self::NotFound { .. } => true,
574 Self::Wrapped { error, .. }
575 if error.downcast_ref::<CommitStatusUnknownError>().is_some() =>
576 {
577 false
578 }
579 Self::IO { source, .. } | Self::Wrapped { error: source, .. } => {
580 error_source_is_not_found(source.as_ref())
581 }
582 _ => false,
583 }
584 }
585
586 #[track_caller]
587 pub fn wrapped(error: BoxedError) -> Self {
588 WrappedSnafu.into_error(error)
589 }
590
591 #[track_caller]
592 pub fn schema(message: impl Into<String>) -> Self {
593 SchemaSnafu {
594 message: message.into(),
595 }
596 .build()
597 }
598
599 #[track_caller]
600 pub fn not_supported(message: impl Into<String>) -> Self {
601 NotSupportedSnafu.into_error(message.into().into())
602 }
603
604 #[track_caller]
605 pub fn not_supported_source(source: BoxedError) -> Self {
606 NotSupportedSnafu.into_error(source)
607 }
608
609 #[track_caller]
610 pub fn internal(message: impl Into<String>) -> Self {
611 InternalSnafu {
612 message: message.into(),
613 }
614 .build()
615 }
616
617 #[track_caller]
618 pub fn timeout(message: impl Into<String>) -> Self {
619 TimeoutSnafu {
620 message: message.into(),
621 }
622 .build()
623 }
624
625 #[track_caller]
626 pub fn namespace(message: impl Into<String>) -> Self {
627 NamespaceSnafu.into_error(message.into().into())
628 }
629
630 #[track_caller]
631 pub fn namespace_source(source: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
632 NamespaceSnafu.into_error(source)
633 }
634
635 #[track_caller]
636 pub fn arrow(message: impl Into<String>) -> Self {
637 ArrowSnafu {
638 message: message.into(),
639 }
640 .build()
641 }
642
643 #[track_caller]
644 pub fn execution(message: impl Into<String>) -> Self {
645 ExecutionSnafu {
646 message: message.into(),
647 }
648 .build()
649 }
650
651 #[track_caller]
652 pub fn cloned(message: impl Into<String>) -> Self {
653 ClonedSnafu {
654 message: message.into(),
655 }
656 .build()
657 }
658
659 #[track_caller]
660 pub fn schema_mismatch(difference: impl Into<String>) -> Self {
661 SchemaMismatchSnafu {
662 difference: difference.into(),
663 }
664 .build()
665 }
666
667 #[track_caller]
668 pub fn unprocessable(message: impl Into<String>) -> Self {
669 UnprocessableSnafu {
670 message: message.into(),
671 }
672 .build()
673 }
674
675 #[track_caller]
676 pub fn too_much_write_contention(message: impl Into<String>) -> Self {
677 TooMuchWriteContentionSnafu {
678 message: message.into(),
679 }
680 .build()
681 }
682
683 #[track_caller]
684 pub fn prerequisite_failed(message: impl Into<String>) -> Self {
685 PrerequisiteFailedSnafu {
686 message: message.into(),
687 }
688 .build()
689 }
690
691 #[track_caller]
692 pub fn index(message: impl Into<String>) -> Self {
693 IndexSnafu {
694 message: message.into(),
695 }
696 .build()
697 }
698
699 #[track_caller]
700 pub fn index_not_found(identity: impl Into<String>) -> Self {
701 IndexNotFoundSnafu {
702 identity: identity.into(),
703 }
704 .build()
705 }
706
707 #[track_caller]
708 pub fn commit_conflict_source(version: u64, source: BoxedError) -> Self {
709 CommitConflictSnafu { version }.into_error(source)
710 }
711
712 #[track_caller]
713 pub fn retryable_commit_conflict_source(version: u64, source: BoxedError) -> Self {
714 RetryableCommitConflictSnafu { version }.into_error(source)
715 }
716
717 #[track_caller]
718 pub fn commit_status_unknown_source(version: u64, source: BoxedError) -> Self {
719 Self::wrapped(box_error(CommitStatusUnknownError { version, source }))
720 }
721
722 pub fn is_commit_status_unknown(&self) -> bool {
725 matches!(
726 self,
727 Self::Wrapped { error, .. }
728 if error.downcast_ref::<CommitStatusUnknownError>().is_some()
729 )
730 }
731
732 #[track_caller]
733 pub fn incompatible_transaction_source(source: BoxedError) -> Self {
734 IncompatibleTransactionSnafu.into_error(source)
735 }
736
737 #[track_caller]
738 pub fn disk_cap_exceeded(cap_bytes: u64, used_bytes: u64) -> Self {
739 DiskCapExceededSnafu {
740 cap_bytes,
741 used_bytes,
742 }
743 .build()
744 }
745
746 pub fn external(source: BoxedError) -> Self {
748 Self::External { source }
749 }
750
751 pub fn field_not_found(field_name: impl Into<String>, candidates: Vec<String>) -> Self {
753 Self::FieldNotFound {
754 source: FieldNotFoundError {
755 field_name: field_name.into(),
756 candidates,
757 },
758 }
759 }
760
761 pub fn external_source(&self) -> Option<&BoxedError> {
765 match self {
766 Self::External { source } => Some(source),
767 _ => None,
768 }
769 }
770
771 pub fn into_external(self) -> std::result::Result<BoxedError, Self> {
775 match self {
776 Self::External { source } => Ok(source),
777 other => Err(other),
778 }
779 }
780}
781
782fn error_source_is_not_found(source: &(dyn std::error::Error + 'static)) -> bool {
783 if let Some(error) = source.downcast_ref::<Error>() {
784 return error.is_not_found();
785 }
786 if let Some(error) = source.downcast_ref::<object_store::Error>() {
787 return matches!(error, object_store::Error::NotFound { .. })
788 || std::error::Error::source(error).is_some_and(error_source_is_not_found);
789 }
790 source.source().is_some_and(error_source_is_not_found)
791}
792
793pub trait LanceOptionExt<T> {
794 fn expect_ok(self) -> Result<T>;
798}
799
800impl<T> LanceOptionExt<T> for Option<T> {
801 #[track_caller]
802 fn expect_ok(self) -> Result<T> {
803 self.ok_or_else(|| Error::internal("Expected option to have value"))
804 }
805}
806
807pub type Result<T> = std::result::Result<T, Error>;
808pub type ArrowResult<T> = std::result::Result<T, ArrowError>;
809#[cfg(feature = "datafusion")]
810pub type DataFusionResult<T> = std::result::Result<T, datafusion_common::DataFusionError>;
811
812impl From<ArrowError> for Error {
813 #[track_caller]
814 fn from(e: ArrowError) -> Self {
815 match e {
816 ArrowError::ExternalError(source) => {
817 match source.downcast::<Self>() {
819 Ok(lance_err) => *lance_err,
820 Err(source) => Self::External { source },
821 }
822 }
823 other => Self::arrow(other.to_string()),
824 }
825 }
826}
827
828impl From<&ArrowError> for Error {
829 #[track_caller]
830 fn from(e: &ArrowError) -> Self {
831 Self::arrow(e.to_string())
832 }
833}
834
835impl From<std::io::Error> for Error {
836 #[track_caller]
837 fn from(e: std::io::Error) -> Self {
838 if e.get_ref().is_some_and(|inner| inner.is::<Self>()) {
843 return *e
844 .into_inner()
845 .expect("checked Some above")
846 .downcast::<Self>()
847 .expect("checked type above");
848 }
849 Self::io_source(box_error(e))
850 }
851}
852
853impl From<object_store::Error> for Error {
854 #[track_caller]
855 fn from(e: object_store::Error) -> Self {
856 match e {
857 object_store::Error::NotFound { path, .. } => Self::not_found(path),
859 other => Self::io_source(box_error(other)),
860 }
861 }
862}
863
864impl From<prost::DecodeError> for Error {
865 #[track_caller]
866 fn from(e: prost::DecodeError) -> Self {
867 Self::io_source(box_error(e))
868 }
869}
870
871impl From<prost::EncodeError> for Error {
872 #[track_caller]
873 fn from(e: prost::EncodeError) -> Self {
874 Self::io_source(box_error(e))
875 }
876}
877
878impl From<prost::UnknownEnumValue> for Error {
879 #[track_caller]
880 fn from(e: prost::UnknownEnumValue) -> Self {
881 Self::io_source(box_error(e))
882 }
883}
884
885impl From<tokio::task::JoinError> for Error {
886 #[track_caller]
887 fn from(e: tokio::task::JoinError) -> Self {
888 Self::io_source(box_error(e))
889 }
890}
891
892impl From<object_store::path::Error> for Error {
893 #[track_caller]
894 fn from(e: object_store::path::Error) -> Self {
895 Self::io_source(box_error(e))
896 }
897}
898
899impl From<url::ParseError> for Error {
900 #[track_caller]
901 fn from(e: url::ParseError) -> Self {
902 Self::io_source(box_error(e))
903 }
904}
905
906impl From<serde_json::Error> for Error {
907 #[track_caller]
908 fn from(e: serde_json::Error) -> Self {
909 Self::arrow(e.to_string())
910 }
911}
912
913impl From<Error> for ArrowError {
914 fn from(value: Error) -> Self {
915 match value {
916 Error::External { source } => Self::ExternalError(source),
918 Error::Schema { message, .. } => Self::SchemaError(message),
920 e => Self::ExternalError(Box::new(e)),
922 }
923 }
924}
925
926#[cfg(feature = "datafusion")]
927impl From<datafusion_sql::sqlparser::parser::ParserError> for Error {
928 #[track_caller]
929 fn from(e: datafusion_sql::sqlparser::parser::ParserError) -> Self {
930 Self::io_source(box_error(e))
931 }
932}
933
934#[cfg(feature = "datafusion")]
935impl From<datafusion_sql::sqlparser::tokenizer::TokenizerError> for Error {
936 #[track_caller]
937 fn from(e: datafusion_sql::sqlparser::tokenizer::TokenizerError) -> Self {
938 Self::io_source(box_error(e))
939 }
940}
941
942#[cfg(feature = "datafusion")]
943impl From<Error> for datafusion_common::DataFusionError {
944 #[track_caller]
945 fn from(e: Error) -> Self {
946 Self::External(Box::new(e))
947 }
948}
949
950#[cfg(feature = "datafusion")]
951impl From<datafusion_common::DataFusionError> for Error {
952 #[track_caller]
953 fn from(e: datafusion_common::DataFusionError) -> Self {
954 match e {
955 datafusion_common::DataFusionError::SQL(..)
956 | datafusion_common::DataFusionError::Plan(..)
957 | datafusion_common::DataFusionError::Configuration(..)
958 | datafusion_common::DataFusionError::SchemaError(..) => {
959 Self::invalid_input_source(box_error(e))
960 }
961 datafusion_common::DataFusionError::ArrowError(arrow_err, _) => Self::from(*arrow_err),
962 datafusion_common::DataFusionError::NotImplemented(..) => {
963 Self::not_supported_source(box_error(e))
964 }
965 datafusion_common::DataFusionError::Execution(..) => Self::execution(e.to_string()),
966 datafusion_common::DataFusionError::Shared(shared) => {
967 match std::sync::Arc::try_unwrap(shared) {
973 Ok(inner) => Self::from(inner),
974 Err(shared) => Self::execution(shared.to_string()),
975 }
976 }
977 datafusion_common::DataFusionError::External(source) => {
978 match source.downcast::<Self>() {
980 Ok(lance_err) => *lance_err,
981 Err(source) => Self::External { source },
982 }
983 }
984 _ => Self::io_source(box_error(e)),
985 }
986 }
987}
988
989impl From<Error> for object_store::Error {
993 fn from(err: Error) -> Self {
994 Self::Generic {
995 store: "N/A",
996 source: Box::new(err),
997 }
998 }
999}
1000
1001#[track_caller]
1002pub fn get_caller_location() -> &'static std::panic::Location<'static> {
1003 std::panic::Location::caller()
1004}
1005
1006pub struct CloneableError(pub Error);
1014
1015struct DisplayError(Error);
1016
1017impl fmt::Debug for DisplayError {
1018 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1019 fmt::Display::fmt(self, f)
1020 }
1021}
1022
1023impl fmt::Display for DisplayError {
1024 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1025 fmt::Display::fmt(&self.0, f)
1026 }
1027}
1028
1029impl std::error::Error for DisplayError {
1030 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1031 Some(&self.0)
1032 }
1033}
1034
1035impl Clone for CloneableError {
1036 #[track_caller]
1037 fn clone(&self) -> Self {
1038 match &self.0 {
1039 Error::NotFound { uri, .. } => Self(Error::wrapped(Box::new(DisplayError(
1040 Error::not_found(uri.clone()),
1041 )))),
1042 error if error.is_not_found() => Self(Error::wrapped(Box::new(DisplayError(
1043 Error::not_found(error.to_string()),
1044 )))),
1045 Error::Timeout { message, .. } => Self(Error::timeout(message.clone())),
1046 Error::IO { source, .. } => Self(Error::io(source.to_string())),
1047 error => Self(Error::cloned(error.to_string())),
1048 }
1049 }
1050}
1051
1052#[derive(Clone)]
1053pub struct CloneableResult<T: Clone>(pub std::result::Result<T, CloneableError>);
1054
1055impl<T: Clone> From<Result<T>> for CloneableResult<T> {
1056 fn from(result: Result<T>) -> Self {
1057 Self(result.map_err(CloneableError))
1058 }
1059}
1060
1061#[cfg(test)]
1062mod test {
1063 use super::*;
1064 use std::error::Error as _;
1065 use std::fmt;
1066
1067 #[test]
1068 fn cloneable_error_preserves_not_found_contract() {
1069 let original = CloneableError(Error::not_found("metadata.lance"));
1070 let cloned = original.clone();
1071 let cloned_again = cloned.clone();
1072 assert!(matches!(original.0, Error::NotFound { .. }));
1073 assert!(cloned.0.is_not_found());
1074 assert!(cloned_again.0.is_not_found());
1075 assert!(cloned.0.to_string().to_lowercase().contains("not found"));
1076 assert!(
1077 cloned_again
1078 .0
1079 .to_string()
1080 .to_lowercase()
1081 .contains("not found")
1082 );
1083 assert!(
1084 format!("{:?}", cloned.0)
1085 .to_lowercase()
1086 .contains("not found")
1087 );
1088 assert!(cloned.0.source().is_some_and(|source| source.is::<Error>()
1089 || source.source().is_some_and(|source| source.is::<Error>())));
1090 let downstream_error = Error::wrapped(Box::new(Error::io_source(Box::new(
1091 object_store::Error::Generic {
1092 store: "N/A",
1093 source: Box::new(cloned.0),
1094 },
1095 ))));
1096 assert!(downstream_error.is_not_found());
1097 assert!(
1098 format!("{downstream_error:?}")
1099 .to_lowercase()
1100 .contains("not found")
1101 );
1102
1103 let original = CloneableError(Error::timeout("metadata read timed out"));
1104 let cloned = original.clone();
1105 assert!(matches!(original.0, Error::Timeout { .. }));
1106 assert!(matches!(cloned.0, Error::Timeout { .. }));
1107
1108 let original = CloneableError(Error::io("metadata read was denied"));
1109 let cloned = original.clone();
1110 assert!(matches!(original.0, Error::IO { .. }));
1111 assert!(matches!(cloned.0, Error::IO { .. }));
1112 }
1113
1114 #[test]
1115 fn test_caller_location_capture() {
1116 let current_fn = get_caller_location();
1117 let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1120 Err(object_store::Error::Generic {
1121 store: "",
1122 source: "".into(),
1123 })?;
1124 Ok(())
1125 });
1126 match f().unwrap_err() {
1127 Error::IO { location, .. } => {
1128 assert_eq!(location.line(), current_fn.line() + 4, "{}", location)
1130 }
1131 #[allow(unreachable_patterns)]
1132 _ => panic!("expected ObjectStore error"),
1133 }
1134 }
1135
1136 #[test]
1137 fn test_caller_location_capture_not_found() {
1138 let current_fn = get_caller_location();
1139 let f: Box<dyn Fn() -> Result<()>> = Box::new(|| {
1140 Err(object_store::Error::NotFound {
1141 path: "some/path".to_string(),
1142 source: "not found".into(),
1143 })?;
1144 Ok(())
1145 });
1146 match f().unwrap_err() {
1147 Error::NotFound { location, .. } => {
1148 assert_eq!(location.line(), current_fn.line() + 2, "{}", location)
1150 }
1151 #[allow(unreachable_patterns)]
1152 other => panic!("expected NotFound, got {:?}", other),
1153 }
1154 }
1155
1156 #[test]
1157 fn test_object_store_not_found_converts_to_not_found() {
1158 let os_err = object_store::Error::NotFound {
1159 path: "test/path".to_string(),
1160 source: "no such file".into(),
1161 };
1162 let lance_err: Error = os_err.into();
1163 match lance_err {
1164 Error::NotFound { uri, .. } => {
1165 assert_eq!(uri, "test/path");
1166 }
1167 other => panic!("Expected NotFound, got {:?}", other),
1168 }
1169 }
1170
1171 #[derive(Debug)]
1172 struct MyCustomError {
1173 code: i32,
1174 message: String,
1175 }
1176
1177 impl fmt::Display for MyCustomError {
1178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1179 write!(f, "MyCustomError({}): {}", self.code, self.message)
1180 }
1181 }
1182
1183 impl std::error::Error for MyCustomError {}
1184
1185 #[test]
1186 fn test_io_error_recovers_wrapped_lance_error() {
1187 let io_err = std::io::Error::other(Error::disk_cap_exceeded(100, 50));
1190 let recovered: Error = io_err.into();
1191 match recovered {
1192 Error::DiskCapExceeded {
1193 cap_bytes,
1194 used_bytes,
1195 ..
1196 } => {
1197 assert_eq!(cap_bytes, 100);
1198 assert_eq!(used_bytes, 50);
1199 }
1200 other => panic!("expected DiskCapExceeded, got {other:?}"),
1201 }
1202 }
1203
1204 #[test]
1205 fn test_io_error_without_lance_error_stays_io() {
1206 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
1208 let converted: Error = io_err.into();
1209 assert!(matches!(converted, Error::IO { .. }));
1210 }
1211
1212 #[test]
1213 fn test_commit_status_unknown_is_structured_without_masking_as_not_found() {
1214 let error = Error::commit_status_unknown_source(
1215 42,
1216 box_error(Error::not_found("temporarily invisible manifest")),
1217 );
1218
1219 assert!(error.is_commit_status_unknown());
1220 assert!(!error.is_not_found());
1221 assert!(error.to_string().contains("version 42 is unknown"));
1222 let Error::Wrapped { error, .. } = error else {
1223 panic!("commit-status-unknown must use the semver-compatible wrapper")
1224 };
1225 let status = error
1226 .downcast_ref::<CommitStatusUnknownError>()
1227 .expect("wrapper must retain the typed commit status");
1228 assert_eq!(status.version(), 42);
1229 }
1230
1231 #[test]
1232 fn test_external_error_creation() {
1233 let custom_err = MyCustomError {
1234 code: 42,
1235 message: "test error".to_string(),
1236 };
1237 let err = Error::external(Box::new(custom_err));
1238
1239 match &err {
1240 Error::External { source } => {
1241 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1242 assert_eq!(recovered.code, 42);
1243 assert_eq!(recovered.message, "test error");
1244 }
1245 _ => panic!("Expected External variant"),
1246 }
1247 }
1248
1249 #[test]
1250 fn test_external_source_method() {
1251 let custom_err = MyCustomError {
1252 code: 123,
1253 message: "source test".to_string(),
1254 };
1255 let err = Error::external(Box::new(custom_err));
1256
1257 let source = err.external_source().expect("should have external source");
1258 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1259 assert_eq!(recovered.code, 123);
1260
1261 let io_err = Error::io("test");
1263 assert!(io_err.external_source().is_none());
1264 }
1265
1266 #[test]
1267 fn test_into_external_method() {
1268 let custom_err = MyCustomError {
1269 code: 456,
1270 message: "into test".to_string(),
1271 };
1272 let err = Error::external(Box::new(custom_err));
1273
1274 match err.into_external() {
1275 Ok(source) => {
1276 let recovered = source.downcast::<MyCustomError>().unwrap();
1277 assert_eq!(recovered.code, 456);
1278 }
1279 Err(_) => panic!("Expected Ok"),
1280 }
1281
1282 let io_err = Error::io("test");
1284 match io_err.into_external() {
1285 Err(Error::IO { .. }) => {}
1286 _ => panic!("Expected Err with IO variant"),
1287 }
1288 }
1289
1290 #[test]
1291 fn test_arrow_external_error_conversion() {
1292 let custom_err = MyCustomError {
1293 code: 789,
1294 message: "arrow test".to_string(),
1295 };
1296 let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1297 let lance_err: Error = arrow_err.into();
1298
1299 match lance_err {
1300 Error::External { source } => {
1301 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1302 assert_eq!(recovered.code, 789);
1303 }
1304 _ => panic!("Expected External variant, got {:?}", lance_err),
1305 }
1306 }
1307
1308 #[test]
1309 fn test_external_to_arrow_roundtrip() {
1310 let custom_err = MyCustomError {
1311 code: 999,
1312 message: "roundtrip".to_string(),
1313 };
1314 let lance_err = Error::external(Box::new(custom_err));
1315 let arrow_err: ArrowError = lance_err.into();
1316
1317 match arrow_err {
1318 ArrowError::ExternalError(source) => {
1319 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1320 assert_eq!(recovered.code, 999);
1321 }
1322 _ => panic!("Expected ExternalError variant"),
1323 }
1324 }
1325
1326 #[cfg(feature = "datafusion")]
1327 #[test]
1328 fn test_datafusion_schema_error_is_invalid_input() {
1329 use datafusion_common::Column;
1334
1335 let schema_err = datafusion_common::SchemaError::FieldNotFound {
1336 field: Box::new(Column::from_name("missing_col")),
1337 valid_fields: vec![],
1338 };
1339 let df_err =
1340 datafusion_common::DataFusionError::SchemaError(Box::new(schema_err), Box::new(None));
1341 let lance_err: Error = df_err.into();
1342
1343 match lance_err {
1344 Error::InvalidInput { .. } => {
1345 assert!(
1346 lance_err.to_string().contains("missing_col"),
1347 "expected the column name to survive in the error message, got: {lance_err}"
1348 );
1349 }
1350 _ => panic!("Expected InvalidInput variant, got {:?}", lance_err),
1351 }
1352 }
1353
1354 #[cfg(feature = "datafusion")]
1355 #[test]
1356 fn test_datafusion_external_error_conversion() {
1357 let custom_err = MyCustomError {
1358 code: 111,
1359 message: "datafusion test".to_string(),
1360 };
1361 let df_err = datafusion_common::DataFusionError::External(Box::new(custom_err));
1362 let lance_err: Error = df_err.into();
1363
1364 match lance_err {
1365 Error::External { source } => {
1366 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1367 assert_eq!(recovered.code, 111);
1368 }
1369 _ => panic!("Expected External variant"),
1370 }
1371 }
1372
1373 #[cfg(feature = "datafusion")]
1374 #[test]
1375 fn test_datafusion_arrow_external_error_conversion() {
1376 let custom_err = MyCustomError {
1378 code: 222,
1379 message: "nested test".to_string(),
1380 };
1381 let arrow_err = ArrowError::ExternalError(Box::new(custom_err));
1382 let df_err = datafusion_common::DataFusionError::ArrowError(Box::new(arrow_err), None);
1383 let lance_err: Error = df_err.into();
1384
1385 match lance_err {
1386 Error::External { source } => {
1387 let recovered = source.downcast_ref::<MyCustomError>().unwrap();
1388 assert_eq!(recovered.code, 222);
1389 }
1390 _ => panic!("Expected External variant, got {:?}", lance_err),
1391 }
1392 }
1393
1394 #[test]
1400 fn test_lance_error_roundtrip_through_arrow() {
1401 let original = Error::invalid_input("test validation error");
1402
1403 let arrow_err: ArrowError = original.into();
1405
1406 let recovered: Error = arrow_err.into();
1408
1409 match recovered {
1411 Error::InvalidInput { .. } => {
1412 assert!(recovered.to_string().contains("test validation error"));
1413 }
1414 _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1415 }
1416 }
1417
1418 #[cfg(feature = "datafusion")]
1424 #[test]
1425 fn test_lance_error_roundtrip_through_datafusion() {
1426 let original = Error::invalid_input("test validation error");
1427
1428 let df_err: datafusion_common::DataFusionError = original.into();
1430
1431 let recovered: Error = df_err.into();
1433
1434 match recovered {
1436 Error::InvalidInput { .. } => {
1437 assert!(recovered.to_string().contains("test validation error"));
1438 }
1439 _ => panic!("Expected InvalidInput variant, got {:?}", recovered),
1440 }
1441 }
1442
1443 #[test]
1444 fn test_backtrace_accessor() {
1445 let err = Error::io("test backtrace");
1447 let bt = err.backtrace();
1448 #[cfg(feature = "backtrace")]
1449 {
1450 let _ = bt;
1454 }
1455 #[cfg(not(feature = "backtrace"))]
1456 {
1457 assert!(bt.is_none());
1459 }
1460 }
1461
1462 #[test]
1463 fn test_backtrace_captured_when_feature_enabled() {
1464 #[cfg(feature = "backtrace")]
1473 {
1474 let err = Error::io("backtrace capture test");
1475 if std::env::var("RUST_BACKTRACE").is_ok() {
1476 assert!(
1477 err.backtrace().is_some(),
1478 "Expected a backtrace when RUST_BACKTRACE=1 and backtrace feature is enabled"
1479 );
1480 }
1481 }
1484 #[cfg(not(feature = "backtrace"))]
1485 {
1486 let err = Error::io("backtrace capture test");
1487 assert!(err.backtrace().is_none());
1488 }
1489 }
1490
1491 #[test]
1492 fn test_backtrace_returns_none_for_variants_without_location() {
1493 let err = Error::InvalidTableLocation {
1494 message: "test".to_string(),
1495 };
1496 assert!(err.backtrace().is_none());
1497
1498 let err = Error::InvalidRef {
1499 message: "test".to_string(),
1500 };
1501 assert!(err.backtrace().is_none());
1502
1503 let err = Error::Stop;
1504 assert!(err.backtrace().is_none());
1505 }
1506}