fix: explain unsupported object storage mounts (#3823)

## Summary

- classify unsupported local-filesystem operations from Lance as a
NotSupported error
- explain that object-storage mounts cannot provide the safe commit
operations Lance requires and direct users to native object-store URIs
- preserve existing error behavior for other local I/O failures and
non-local backends

## Root cause

Mountpoint for Amazon S3 exposes an S3 bucket as a local path but does
not implement atomic rename. Lance uses atomic rename for safe local
commits, and the resulting unsupported I/O error was previously passed
through as a generic Lance error, leaving Python users with an opaque
low-level failure. Transparent support for such mounts is not safe;
direct s3:// access remains the supported path.

## Validation

- cargo test --quiet --features remote -p lancedb error::tests
- cargo test --quiet --features remote -p lancedb --lib (807 passed, 1
ignored)
- cargo check --quiet --features remote --tests --examples
- cargo clippy --quiet --features remote --tests --examples
- cargo fmt --all -- --check

Fixes #2016

<!-- lance-gatekeeper-fix:v1 agent=d53283c18fdb00a3a1b69448b1f40529
generation=1 -->

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
This commit is contained in:
lancedb-gatefixer[bot]
2026-08-07 17:33:02 +08:00
committed by GitHub
parent 2ba7407dc3
commit 11f24b1df4
+70
View File
@@ -169,6 +169,12 @@ impl From<DataFusionError> for Error {
impl From<lance::Error> for Error {
fn from(source: lance::Error) -> Self {
if has_unsupported_local_filesystem_source(&source) {
return Self::NotSupported {
message: "the filesystem does not support an operation required for safe Lance commits (such as atomic rename). Object-storage mounts such as Mountpoint for Amazon S3 are not supported; use the native object-store URI (for example, s3://bucket/path) instead".to_string(),
};
}
// Try to unwrap external errors that were wrapped by lance
match source {
lance::Error::Wrapped { error, .. } => Self::from_box_error(error),
@@ -181,6 +187,27 @@ impl From<lance::Error> for Error {
}
}
fn has_unsupported_local_filesystem_source(error: &(dyn std::error::Error + 'static)) -> bool {
let mut current = Some(error);
let mut is_local_filesystem = false;
let mut is_unsupported = false;
while let Some(error) = current {
is_local_filesystem |= error
.downcast_ref::<object_store::Error>()
.is_some_and(|error| {
matches!(error, object_store::Error::Generic { store, .. } if *store == "LocalFileSystem")
});
is_unsupported |= error
.downcast_ref::<std::io::Error>()
.is_some_and(|error| error.kind() == std::io::ErrorKind::Unsupported);
if is_local_filesystem && is_unsupported {
return true;
}
current = error.source();
}
false
}
impl Error {
fn from_box_error(mut source: Box<dyn std::error::Error + Send + Sync>) -> Self {
source = match source.downcast::<Self>() {
@@ -270,3 +297,46 @@ impl From<candle_core::Error> for Error {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_filesystem_operations_have_actionable_error() {
let object_store_error = object_store::Error::Generic {
store: "LocalFileSystem",
source: Box::new(std::io::Error::from(std::io::ErrorKind::Unsupported)),
};
let lance_error = lance::Error::io_source(Box::new(object_store_error));
let error = Error::from(lance_error);
assert!(matches!(
error,
Error::NotSupported { message }
if message.contains("Mountpoint for Amazon S3")
&& message.contains("s3://bucket/path")
));
}
#[test]
fn other_io_errors_remain_lance_errors() {
let object_store_error = object_store::Error::Generic {
store: "LocalFileSystem",
source: Box::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
};
let lance_error = lance::Error::io_source(Box::new(object_store_error));
assert!(matches!(Error::from(lance_error), Error::Lance { .. }));
}
#[test]
fn unsupported_non_filesystem_errors_remain_lance_errors() {
let lance_error = lance::Error::io_source(Box::new(std::io::Error::from(
std::io::ErrorKind::Unsupported,
)));
assert!(matches!(Error::from(lance_error), Error::Lance { .. }));
}
}