diff --git a/bindings/python/src/datafusion_table_provider.rs b/bindings/python/src/datafusion_table_provider.rs index 7fa9f53dbd..d43068edf7 100644 --- a/bindings/python/src/datafusion_table_provider.rs +++ b/bindings/python/src/datafusion_table_provider.rs @@ -118,7 +118,9 @@ impl PyIcebergDataFusionTable { builder = builder.with_props(props); } - let file_io = builder.build(); + let file_io = builder + .build(None) + .map_err(|e| PyRuntimeError::new_err(format!("Failed to build FileIO: {e}")))?; let static_table = StaticTable::from_metadata_file(&metadata_location, table_ident, file_io) diff --git a/crates/catalog/glue/src/catalog.rs b/crates/catalog/glue/src/catalog.rs index 9e9d4580c3..409340729e 100644 --- a/crates/catalog/glue/src/catalog.rs +++ b/crates/catalog/glue/src/catalog.rs @@ -209,7 +209,7 @@ impl GlueCatalog { }); let file_io = FileIOBuilder::new(factory) .with_props(file_io_props) - .build(); + .build(None)?; Ok(GlueCatalog { config, diff --git a/crates/catalog/hms/src/catalog.rs b/crates/catalog/hms/src/catalog.rs index bd78193732..c72e4b664c 100644 --- a/crates/catalog/hms/src/catalog.rs +++ b/crates/catalog/hms/src/catalog.rs @@ -217,7 +217,7 @@ impl HmsCatalog { })?; let file_io = FileIOBuilder::new(factory) .with_props(&config.props) - .build(); + .build(None)?; Ok(Self { config, diff --git a/crates/catalog/hms/tests/hms_catalog_test.rs b/crates/catalog/hms/tests/hms_catalog_test.rs index f19cf7bff4..897f5342ec 100644 --- a/crates/catalog/hms/tests/hms_catalog_test.rs +++ b/crates/catalog/hms/tests/hms_catalog_test.rs @@ -64,7 +64,8 @@ async fn get_catalog() -> HmsCatalog { customized_credential_load: None, })) .with_props(props.clone()) - .build(); + .build(None) + .unwrap(); let mut retries = 0; while retries < 30 { diff --git a/crates/catalog/loader/tests/common/mod.rs b/crates/catalog/loader/tests/common/mod.rs index 90b72df8ab..8ff15916b2 100644 --- a/crates/catalog/loader/tests/common/mod.rs +++ b/crates/catalog/loader/tests/common/mod.rs @@ -236,7 +236,8 @@ async fn glue_catalog() -> GlueCatalog { customized_credential_load: None, })) .with_props(props.clone()) - .build(); + .build(None) + .unwrap(); let mut retries = 0; while retries < 30 { @@ -287,7 +288,8 @@ async fn hms_catalog() -> HmsCatalog { customized_credential_load: None, })) .with_props(props.clone()) - .build(); + .build(None) + .unwrap(); let mut retries = 0; while retries < 30 { diff --git a/crates/catalog/rest/src/catalog.rs b/crates/catalog/rest/src/catalog.rs index 3551b05160..9812684118 100644 --- a/crates/catalog/rest/src/catalog.rs +++ b/crates/catalog/rest/src/catalog.rs @@ -24,6 +24,7 @@ use std::sync::Arc; use async_trait::async_trait; use iceberg::io::{FileIO, FileIOBuilder, StorageFactory}; +use iceberg::spec::TableMetadata; use iceberg::table::Table; use iceberg::{ Catalog, CatalogBuilder, Error, ErrorKind, Namespace, NamespaceIdent, Result, TableCommit, @@ -33,7 +34,7 @@ use itertools::Itertools; use reqwest::header::{ HeaderMap, HeaderName, HeaderValue, {self}, }; -use reqwest::{Client, Method, StatusCode, Url}; +use reqwest::{Client, Method, StatusCode}; use tokio::sync::OnceCell; use typed_builder::TypedBuilder; @@ -406,7 +407,7 @@ impl RestCatalog { async fn load_file_io( &self, - metadata_location: Option<&str>, + metadata: &TableMetadata, extra_config: Option>, ) -> Result { let mut props = self.context().await?.config.props.clone(); @@ -414,21 +415,6 @@ impl RestCatalog { props.extend(config); } - // If the warehouse is a logical identifier instead of a URL we don't want - // to raise an exception - let warehouse_path = match self.context().await?.config.warehouse.as_deref() { - Some(url) if Url::parse(url).is_ok() => Some(url), - Some(_) => None, - None => None, - }; - - if metadata_location.or(warehouse_path).is_none() { - return Err(Error::new( - ErrorKind::Unexpected, - "Unable to load file io, neither warehouse nor metadata location is set!", - )); - } - // Require a StorageFactory to be provided let factory = self .storage_factory @@ -440,7 +426,9 @@ impl RestCatalog { ) })?; - let file_io = FileIOBuilder::new(factory).with_props(props).build(); + let file_io = FileIOBuilder::new(factory) + .with_props(props) + .build(Some(metadata))?; Ok(file_io) } @@ -743,10 +731,12 @@ impl Catalog for RestCatalog { } }; - let metadata_location = response.metadata_location.as_ref().ok_or(Error::new( - ErrorKind::DataInvalid, - "Metadata location missing in `create_table` response!", - ))?; + if response.metadata_location.is_none() { + return Err(Error::new( + ErrorKind::DataInvalid, + "Metadata location missing in `create_table` response!", + )); + } let config = response .config @@ -754,9 +744,7 @@ impl Catalog for RestCatalog { .chain(self.user_config.props.clone()) .collect(); - let file_io = self - .load_file_io(Some(metadata_location), Some(config)) - .await?; + let file_io = self.load_file_io(&response.metadata, Some(config)).await?; let table_builder = Table::builder() .identifier(table_ident.clone()) @@ -810,9 +798,7 @@ impl Catalog for RestCatalog { .chain(self.user_config.props.clone()) .collect(); - let file_io = self - .load_file_io(response.metadata_location.as_deref(), Some(config)) - .await?; + let file_io = self.load_file_io(&response.metadata, Some(config)).await?; let table_builder = Table::builder() .identifier(table_ident.clone()) @@ -960,7 +946,7 @@ impl Catalog for RestCatalog { "Metadata location missing in `register_table` response!", ))?; - let file_io = self.load_file_io(Some(metadata_location), None).await?; + let file_io = self.load_file_io(&response.metadata, None).await?; Table::builder() .identifier(table_ident.clone()) @@ -1030,9 +1016,7 @@ impl Catalog for RestCatalog { } }; - let file_io = self - .load_file_io(Some(&response.metadata_location), None) - .await?; + let file_io = self.load_file_io(&response.metadata, None).await?; Table::builder() .identifier(commit.identifier().clone()) diff --git a/crates/catalog/s3tables/src/catalog.rs b/crates/catalog/s3tables/src/catalog.rs index a416c38f22..b7c0de15fb 100644 --- a/crates/catalog/s3tables/src/catalog.rs +++ b/crates/catalog/s3tables/src/catalog.rs @@ -208,7 +208,7 @@ impl S3TablesCatalog { }); let file_io = FileIOBuilder::new(factory) .with_props(&config.props) - .build(); + .build(None)?; Ok(Self { config, diff --git a/crates/catalog/sql/src/catalog.rs b/crates/catalog/sql/src/catalog.rs index 195f6c9de4..6ef3074c02 100644 --- a/crates/catalog/sql/src/catalog.rs +++ b/crates/catalog/sql/src/catalog.rs @@ -244,7 +244,7 @@ impl SqlCatalog { "StorageFactory must be provided for SqlCatalog. Use `with_storage_factory` to configure it.", ) })?; - let fileio = FileIOBuilder::new(factory).build(); + let fileio = FileIOBuilder::new(factory).build(None)?; install_default_drivers(); let max_connections: u32 = config diff --git a/crates/iceberg/src/catalog/memory/catalog.rs b/crates/iceberg/src/catalog/memory/catalog.rs index 25ae004417..c755435a2d 100644 --- a/crates/iceberg/src/catalog/memory/catalog.rs +++ b/crates/iceberg/src/catalog/memory/catalog.rs @@ -134,7 +134,9 @@ impl MemoryCatalog { Ok(Self { root_namespace_state: Mutex::new(NamespaceState::default()), - file_io: FileIOBuilder::new(factory).with_props(config.props).build(), + file_io: FileIOBuilder::new(factory) + .with_props(config.props) + .build(None)?, warehouse_location: config.warehouse, }) } diff --git a/crates/iceberg/src/io/file_io.rs b/crates/iceberg/src/io/file_io.rs index 594b070e03..95d0ef6288 100644 --- a/crates/iceberg/src/io/file_io.rs +++ b/crates/iceberg/src/io/file_io.rs @@ -16,7 +16,7 @@ // under the License. use std::ops::Range; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; use bytes::Bytes; use futures::{Stream, StreamExt}; @@ -25,11 +25,11 @@ use super::storage::{ LocalFsStorageFactory, MemoryStorageFactory, Storage, StorageConfig, StorageFactory, }; use crate::Result; +use crate::spec::TableMetadata; /// FileIO implementation, used to manipulate files in underlying storage. /// -/// FileIO wraps a `dyn Storage` with lazy initialization via `StorageFactory`. -/// The storage is created on first use and cached for subsequent operations. +/// FileIO wraps a `dyn Storage` that is eagerly constructed at build time. /// /// # Note /// @@ -57,16 +57,14 @@ use crate::Result; /// // Create FileIO with custom factory /// let file_io = FileIOBuilder::new(Arc::new(LocalFsStorageFactory)) /// .with_prop("key", "value") -/// .build(); +/// .build(None)?; /// ``` #[derive(Clone, Debug)] pub struct FileIO { /// Storage configuration containing properties config: StorageConfig, - /// Factory for creating storage instances - factory: Arc, - /// Cached storage instance (lazily initialized) - storage: Arc>>, + /// Eagerly constructed storage instance + storage: Arc, } impl FileIO { @@ -74,22 +72,22 @@ impl FileIO { /// /// This is useful for testing scenarios where persistent storage is not needed. pub fn new_with_memory() -> Self { - Self { - config: StorageConfig::new(), - factory: Arc::new(MemoryStorageFactory), - storage: Arc::new(OnceLock::new()), - } + let config = StorageConfig::new(); + let storage = MemoryStorageFactory + .build(&config, None) + .expect("MemoryStorage construction should not fail"); + Self { config, storage } } /// Create a new FileIO backed by local filesystem storage. /// /// This is useful for local development and testing with real files. pub fn new_with_fs() -> Self { - Self { - config: StorageConfig::new(), - factory: Arc::new(LocalFsStorageFactory), - storage: Arc::new(OnceLock::new()), - } + let config = StorageConfig::new(); + let storage = LocalFsStorageFactory + .build(&config, None) + .expect("LocalFsStorage construction should not fail"); + Self { config, storage } } /// Get the storage configuration. @@ -97,33 +95,13 @@ impl FileIO { &self.config } - /// Get or create the storage instance. - /// - /// The factory is invoked on first access and the result is cached - /// for all subsequent operations. - fn get_storage(&self) -> Result> { - // Check if already initialized - if let Some(storage) = self.storage.get() { - return Ok(storage.clone()); - } - - // Build the storage - let storage = self.factory.build(&self.config)?; - - // Try to set it (another thread might have set it first) - let _ = self.storage.set(storage.clone()); - - // Return whatever is in the cell (either ours or another thread's) - Ok(self.storage.get().unwrap().clone()) - } - /// Deletes file. /// /// # Arguments /// /// * path: It should be *absolute* path starting with scheme string used to construct [`FileIO`]. pub async fn delete(&self, path: impl AsRef) -> Result<()> { - self.get_storage()?.delete(path.as_ref()).await + self.storage.delete(path.as_ref()).await } /// Remove the path and all nested dirs and files recursively. @@ -138,7 +116,7 @@ impl FileIO { /// - If the path is a empty directory, this function will remove the directory itself. /// - If the path is a non-empty directory, this function will remove the directory and all nested files and directories. pub async fn delete_prefix(&self, path: impl AsRef) -> Result<()> { - self.get_storage()?.delete_prefix(path.as_ref()).await + self.storage.delete_prefix(path.as_ref()).await } /// Delete multiple files from a stream of paths. @@ -150,7 +128,7 @@ impl FileIO { &self, paths: impl Stream + Send + 'static, ) -> Result<()> { - self.get_storage()?.delete_stream(paths.boxed()).await + self.storage.delete_stream(paths.boxed()).await } /// Check file exists. @@ -159,7 +137,7 @@ impl FileIO { /// /// * path: It should be *absolute* path starting with scheme string used to construct [`FileIO`]. pub async fn exists(&self, path: impl AsRef) -> Result { - self.get_storage()?.exists(path.as_ref()).await + self.storage.exists(path.as_ref()).await } /// Creates input file. @@ -168,7 +146,7 @@ impl FileIO { /// /// * path: It should be *absolute* path starting with scheme string used to construct [`FileIO`]. pub fn new_input(&self, path: impl AsRef) -> Result { - self.get_storage()?.new_input(path.as_ref()) + self.storage.new_input(path.as_ref()) } /// Creates output file. @@ -177,14 +155,14 @@ impl FileIO { /// /// * path: It should be *absolute* path starting with scheme string used to construct [`FileIO`]. pub fn new_output(&self, path: impl AsRef) -> Result { - self.get_storage()?.new_output(path.as_ref()) + self.storage.new_output(path.as_ref()) } } /// Builder for [`FileIO`]. /// /// The builder accepts an explicit `StorageFactory` and configuration properties. -/// Storage is lazily initialized on first use. +/// Storage is eagerly constructed when `build()` is called. #[derive(Clone, Debug)] pub struct FileIOBuilder { /// Factory for creating storage instances @@ -224,13 +202,18 @@ impl FileIOBuilder { &self.config } - /// Builds [`FileIO`]. - pub fn build(self) -> FileIO { - FileIO { + /// Builds [`FileIO`] by eagerly constructing the storage backend. + /// + /// # Arguments + /// + /// * `metadata` - Optional table metadata for storage backends that need + /// table-level configuration. + pub fn build(self, metadata: Option<&TableMetadata>) -> Result { + let storage = self.factory.build(&self.config, metadata)?; + Ok(FileIO { config: self.config, - factory: self.factory, - storage: Arc::new(OnceLock::new()), - } + storage, + }) } } @@ -522,7 +505,8 @@ mod tests { let file_io = FileIOBuilder::new(factory) .with_prop("key1", "value1") .with_prop("key2", "value2") - .build(); + .build(None) + .unwrap(); assert_eq!(file_io.config().get("key1"), Some(&"value1".to_string())); assert_eq!(file_io.config().get("key2"), Some(&"value2".to_string())); @@ -532,7 +516,10 @@ mod tests { async fn test_file_io_builder_with_multiple_props() { let factory = Arc::new(LocalFsStorageFactory); let props = vec![("key1", "value1"), ("key2", "value2")]; - let file_io = FileIOBuilder::new(factory).with_props(props).build(); + let file_io = FileIOBuilder::new(factory) + .with_props(props) + .build(None) + .unwrap(); assert_eq!(file_io.config().get("key1"), Some(&"value1".to_string())); assert_eq!(file_io.config().get("key2"), Some(&"value2".to_string())); diff --git a/crates/iceberg/src/io/mod.rs b/crates/iceberg/src/io/mod.rs index 59a21c4b66..b94b5f0932 100644 --- a/crates/iceberg/src/io/mod.rs +++ b/crates/iceberg/src/io/mod.rs @@ -36,7 +36,7 @@ //! // Build with explicit factory and configuration //! let file_io = FileIOBuilder::new(Arc::new(LocalFsStorageFactory)) //! .with_prop("key", "value") -//! .build(); +//! .build(None)?; //! ``` //! //! # How to use `FileIO` diff --git a/crates/iceberg/src/io/storage/local_fs.rs b/crates/iceberg/src/io/storage/local_fs.rs index e96e951baa..44c9d70b2a 100644 --- a/crates/iceberg/src/io/storage/local_fs.rs +++ b/crates/iceberg/src/io/storage/local_fs.rs @@ -328,7 +328,11 @@ pub struct LocalFsStorageFactory; #[typetag::serde] impl StorageFactory for LocalFsStorageFactory { - fn build(&self, _config: &StorageConfig) -> Result> { + fn build( + &self, + _config: &StorageConfig, + _metadata: Option<&crate::spec::TableMetadata>, + ) -> Result> { Ok(Arc::new(LocalFsStorage::new())) } } @@ -525,7 +529,7 @@ mod tests { fn test_local_fs_storage_factory() { let factory = LocalFsStorageFactory; let config = StorageConfig::new(); - let storage = factory.build(&config).unwrap(); + let storage = factory.build(&config, None).unwrap(); // Verify we got a valid storage instance assert!(format!("{storage:?}").contains("LocalFsStorage")); diff --git a/crates/iceberg/src/io/storage/memory.rs b/crates/iceberg/src/io/storage/memory.rs index f33dbd07b1..1a61d4cfaa 100644 --- a/crates/iceberg/src/io/storage/memory.rs +++ b/crates/iceberg/src/io/storage/memory.rs @@ -248,7 +248,11 @@ pub struct MemoryStorageFactory; #[typetag::serde] impl StorageFactory for MemoryStorageFactory { - fn build(&self, _config: &StorageConfig) -> Result> { + fn build( + &self, + _config: &StorageConfig, + _metadata: Option<&crate::spec::TableMetadata>, + ) -> Result> { Ok(Arc::new(MemoryStorage::new())) } } @@ -561,7 +565,7 @@ mod tests { fn test_memory_storage_factory() { let factory = MemoryStorageFactory; let config = StorageConfig::new(); - let storage = factory.build(&config).unwrap(); + let storage = factory.build(&config, None).unwrap(); // Verify we got a valid storage instance assert!(format!("{storage:?}").contains("MemoryStorage")); @@ -579,7 +583,7 @@ mod tests { // Verify the deserialized factory works let config = StorageConfig::new(); - let storage = deserialized.build(&config).unwrap(); + let storage = deserialized.build(&config, None).unwrap(); assert!(format!("{storage:?}").contains("MemoryStorage")); } diff --git a/crates/iceberg/src/io/storage/mod.rs b/crates/iceberg/src/io/storage/mod.rs index 5276c7771f..9022d49b0f 100644 --- a/crates/iceberg/src/io/storage/mod.rs +++ b/crates/iceberg/src/io/storage/mod.rs @@ -33,6 +33,7 @@ pub use memory::{MemoryStorage, MemoryStorageFactory}; use super::{FileMetadata, FileRead, FileWrite, InputFile, OutputFile}; use crate::Result; +use crate::spec::TableMetadata; /// Trait for storage operations in Iceberg. /// @@ -107,8 +108,7 @@ pub trait Storage: Debug + Send + Sync { /// Factory for creating Storage instances from configuration. /// /// Implement this trait to provide custom storage backends. The factory pattern -/// allows for lazy initialization of storage instances and enables users to -/// inject custom storage implementations into catalogs. +/// enables users to inject custom storage implementations into catalogs. /// /// # Example /// @@ -120,7 +120,11 @@ pub trait Storage: Debug + Send + Sync { /// /// #[typetag::serde] /// impl StorageFactory for MyCustomStorageFactory { -/// fn build(&self, config: &StorageConfig) -> Result> { +/// fn build( +/// &self, +/// config: &StorageConfig, +/// metadata: Option<&TableMetadata>, +/// ) -> Result> { /// // Create and return custom storage implementation /// todo!() /// } @@ -133,10 +137,16 @@ pub trait StorageFactory: Debug + Send + Sync { /// # Arguments /// /// * `config` - The storage configuration containing scheme and properties + /// * `metadata` - Optional table metadata that storage backends can use + /// for table-level configuration (e.g., table properties). /// /// # Returns /// /// A `Result` containing an `Arc` on success, or an error /// if the storage could not be created. - fn build(&self, config: &StorageConfig) -> Result>; + fn build( + &self, + config: &StorageConfig, + metadata: Option<&TableMetadata>, + ) -> Result>; } diff --git a/crates/integrations/datafusion/src/table/table_provider_factory.rs b/crates/integrations/datafusion/src/table/table_provider_factory.rs index 9692340cac..85a4175b03 100644 --- a/crates/integrations/datafusion/src/table/table_provider_factory.rs +++ b/crates/integrations/datafusion/src/table/table_provider_factory.rs @@ -209,7 +209,7 @@ async fn create_static_table( let table_ident = TableIdent::from_strs(table_name.to_vec())?; let file_io = FileIOBuilder::new(storage_factory) .with_props(props) - .build(); + .build(None)?; StaticTable::from_metadata_file(metadata_file_path, table_ident, file_io).await } diff --git a/crates/storage/opendal/src/lib.rs b/crates/storage/opendal/src/lib.rs index 8160680523..f1c2f9b010 100644 --- a/crates/storage/opendal/src/lib.rs +++ b/crates/storage/opendal/src/lib.rs @@ -132,7 +132,11 @@ pub enum OpenDalStorageFactory { #[typetag::serde(name = "OpenDalStorageFactory")] impl StorageFactory for OpenDalStorageFactory { #[allow(unused_variables)] - fn build(&self, config: &StorageConfig) -> Result> { + fn build( + &self, + config: &StorageConfig, + metadata: Option<&iceberg::spec::TableMetadata>, + ) -> Result> { match self { #[cfg(feature = "opendal-memory")] OpenDalStorageFactory::Memory => { diff --git a/crates/storage/opendal/src/resolving.rs b/crates/storage/opendal/src/resolving.rs index 7c06cf96a5..67cc0bde92 100644 --- a/crates/storage/opendal/src/resolving.rs +++ b/crates/storage/opendal/src/resolving.rs @@ -147,7 +147,7 @@ fn build_storage_for_scheme( /// let factory = OpenDalResolvingStorageFactory::new(); /// let file_io = FileIOBuilder::new(Arc::new(factory)) /// .with_prop("s3.region", "us-east-1") -/// .build(); +/// .build(None)?; /// ``` #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OpenDalResolvingStorageFactory { @@ -182,7 +182,11 @@ impl OpenDalResolvingStorageFactory { #[typetag::serde] impl StorageFactory for OpenDalResolvingStorageFactory { - fn build(&self, config: &StorageConfig) -> Result> { + fn build( + &self, + config: &StorageConfig, + _metadata: Option<&iceberg::spec::TableMetadata>, + ) -> Result> { Ok(Arc::new(OpenDalResolvingStorage { props: config.props().clone(), storages: RwLock::new(HashMap::new()), diff --git a/crates/storage/opendal/tests/file_io_gcs_test.rs b/crates/storage/opendal/tests/file_io_gcs_test.rs index 5e04491131..4db0d935c1 100644 --- a/crates/storage/opendal/tests/file_io_gcs_test.rs +++ b/crates/storage/opendal/tests/file_io_gcs_test.rs @@ -44,7 +44,8 @@ mod tests { (GCS_SERVICE_PATH, gcs_endpoint), (GCS_NO_AUTH, "true".to_string()), ]) - .build() + .build(None) + .unwrap() } // Create a bucket against the emulated GCS storage server. diff --git a/crates/storage/opendal/tests/file_io_s3_test.rs b/crates/storage/opendal/tests/file_io_s3_test.rs index 207a4454d7..dfcf688a8f 100644 --- a/crates/storage/opendal/tests/file_io_s3_test.rs +++ b/crates/storage/opendal/tests/file_io_s3_test.rs @@ -48,7 +48,8 @@ mod tests { (S3_SECRET_ACCESS_KEY, "password".to_string()), (S3_REGION, "us-east-1".to_string()), ]) - .build() + .build(None) + .unwrap() } #[tokio::test] @@ -161,7 +162,8 @@ mod tests { (S3_ENDPOINT, minio_endpoint), (S3_REGION, "us-east-1".to_string()), ]) - .build(); + .build(None) + .unwrap(); // Test that the FileIO was built successfully with the custom loader match file_io_with_custom_creds.exists("s3://bucket1/any").await { @@ -189,7 +191,8 @@ mod tests { (S3_ENDPOINT, minio_endpoint), (S3_REGION, "us-east-1".to_string()), ]) - .build(); + .build(None) + .unwrap(); // Test that the FileIO was built successfully with the custom loader match file_io_with_custom_creds.exists("s3://bucket1/any").await { diff --git a/crates/storage/opendal/tests/resolving_storage_test.rs b/crates/storage/opendal/tests/resolving_storage_test.rs index 4572ad2c2d..7e1999d181 100644 --- a/crates/storage/opendal/tests/resolving_storage_test.rs +++ b/crates/storage/opendal/tests/resolving_storage_test.rs @@ -46,7 +46,8 @@ mod tests { (S3_SECRET_ACCESS_KEY, "password".to_string()), (S3_REGION, "us-east-1".to_string()), ]) - .build() + .build(None) + .unwrap() } fn temp_fs_path(name: &str) -> String { @@ -289,7 +290,8 @@ mod tests { (S3_ENDPOINT, minio_endpoint), (S3_REGION, "us-east-1".to_string()), ]) - .build(); + .build(None) + .unwrap(); // Should be able to access S3 using the custom credential loader assert!(file_io.exists("s3://bucket1/").await.unwrap());