blob: aedee7d44460d3e5a0053fe0a0f76f9bc2749b16 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! ObjectStoreRegistry holds all the object stores at Runtime with a scheme for each store.
//! This allows the user to extend DataFusion with different storage systems such as S3 or HDFS
//! and query data inside these systems.
use dashmap::DashMap;
use datafusion_common::{
exec_err, internal_datafusion_err, not_impl_err, DataFusionError, Result,
};
#[cfg(not(target_arch = "wasm32"))]
use object_store::local::LocalFileSystem;
use object_store::ObjectStore;
use std::sync::Arc;
use url::Url;
/// A parsed URL identifying a particular [`ObjectStore`] instance
///
/// For example:
/// * `file://` for local file system
/// * `s3://bucket` for AWS S3 bucket
/// * `oss://bucket` for Aliyun OSS bucket
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectStoreUrl {
url: Url,
}
impl ObjectStoreUrl {
/// Parse an [`ObjectStoreUrl`] from a string
///
/// # Example
/// ```
/// # use url::Url;
/// # use datafusion_execution::object_store::ObjectStoreUrl;
/// let object_store_url = ObjectStoreUrl::parse("s3://bucket").unwrap();
/// assert_eq!(object_store_url.as_str(), "s3://bucket/");
/// // can also access the underlying `Url`
/// let url: &Url = object_store_url.as_ref();
/// assert_eq!(url.scheme(), "s3");
/// assert_eq!(url.host_str(), Some("bucket"));
/// assert_eq!(url.path(), "/");
/// ```
pub fn parse(s: impl AsRef<str>) -> Result<Self> {
let mut parsed =
Url::parse(s.as_ref()).map_err(|e| DataFusionError::External(Box::new(e)))?;
let remaining = &parsed[url::Position::BeforePath..];
if !remaining.is_empty() && remaining != "/" {
return exec_err!(
"ObjectStoreUrl must only contain scheme and authority, got: {remaining}"
);
}
// Always set path for consistency
parsed.set_path("/");
Ok(Self { url: parsed })
}
/// An [`ObjectStoreUrl`] for the local filesystem (`file://`)
///
/// # Example
/// ```
/// # use datafusion_execution::object_store::ObjectStoreUrl;
/// let local_fs = ObjectStoreUrl::parse("file://").unwrap();
/// assert_eq!(local_fs, ObjectStoreUrl::local_filesystem())
/// ```
pub fn local_filesystem() -> Self {
Self::parse("file://").unwrap()
}
/// Returns this [`ObjectStoreUrl`] as a string
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
impl AsRef<str> for ObjectStoreUrl {
fn as_ref(&self) -> &str {
self.url.as_ref()
}
}
impl AsRef<Url> for ObjectStoreUrl {
fn as_ref(&self) -> &Url {
&self.url
}
}
impl std::fmt::Display for ObjectStoreUrl {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
/// [`ObjectStoreRegistry`] maps a URL to an [`ObjectStore`] instance,
/// and allows DataFusion to read from different [`ObjectStore`]
/// instances. For example DataFusion might be configured so that
///
/// 1. `s3://my_bucket/lineitem/` mapped to the `/lineitem` path on an
/// AWS S3 object store bound to `my_bucket`
///
/// 2. `s3://my_other_bucket/lineitem/` mapped to the (same)
/// `/lineitem` path on a *different* AWS S3 object store bound to
/// `my_other_bucket`
///
/// When given a [`ListingTableUrl`], DataFusion tries to find an
/// appropriate [`ObjectStore`]. For example
///
/// ```sql
/// create external table unicorns stored as parquet location 's3://my_bucket/lineitem/';
/// ```
///
/// In this particular case, the url `s3://my_bucket/lineitem/` will be provided to
/// [`ObjectStoreRegistry::get_store`] and one of three things will happen:
///
/// - If an [`ObjectStore`] has been registered with [`ObjectStoreRegistry::register_store`] with
/// `s3://my_bucket`, that [`ObjectStore`] will be returned
///
/// - If an AWS S3 object store can be ad-hoc discovered by the url `s3://my_bucket/lineitem/`, this
/// object store will be registered with key `s3://my_bucket` and returned.
///
/// - Otherwise an error will be returned, indicating that no suitable [`ObjectStore`] could
/// be found
///
/// This allows for two different use-cases:
///
/// 1. Systems where object store buckets are explicitly created using DDL, can register these
/// buckets using [`ObjectStoreRegistry::register_store`]
///
/// 2. Systems relying on ad-hoc discovery, without corresponding DDL, can create [`ObjectStore`]
/// lazily by providing a custom implementation of [`ObjectStoreRegistry`]
///
/// <!-- is in a different crate so normal rustdoc links don't work -->
/// [`ListingTableUrl`]: https://docs.rs/datafusion/latest/datafusion/datasource/listing/struct.ListingTableUrl.html
/// [`ObjectStore`]: object_store::ObjectStore
pub trait ObjectStoreRegistry: Send + Sync + std::fmt::Debug + 'static {
/// If a store with the same key existed before, it is replaced and returned
fn register_store(
&self,
url: &Url,
store: Arc<dyn ObjectStore>,
) -> Option<Arc<dyn ObjectStore>>;
/// Deregister the store previously registered with the same key. Returns the
/// deregistered store if it existed.
#[allow(unused_variables)]
fn deregister_store(&self, url: &Url) -> Result<Arc<dyn ObjectStore>> {
not_impl_err!("ObjectStoreRegistry::deregister_store is not implemented for this ObjectStoreRegistry")
}
/// Get a suitable store for the provided URL. For example:
///
/// - URL with scheme `file:///` or no scheme will return the default LocalFS store
/// - URL with scheme `s3://bucket/` will return the S3 store
/// - URL with scheme `hdfs://hostname:port/` will return the hdfs store
///
/// If no [`ObjectStore`] found for the `url`, ad-hoc discovery may be executed depending on
/// the `url` and [`ObjectStoreRegistry`] implementation. An [`ObjectStore`] may be lazily
/// created and registered.
fn get_store(&self, url: &Url) -> Result<Arc<dyn ObjectStore>>;
}
/// The default [`ObjectStoreRegistry`]
pub struct DefaultObjectStoreRegistry {
/// A map from scheme to object store that serve list / read operations for the store
object_stores: DashMap<String, Arc<dyn ObjectStore>>,
}
impl std::fmt::Debug for DefaultObjectStoreRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("DefaultObjectStoreRegistry")
.field(
"schemes",
&self
.object_stores
.iter()
.map(|o| o.key().clone())
.collect::<Vec<_>>(),
)
.finish()
}
}
impl Default for DefaultObjectStoreRegistry {
fn default() -> Self {
Self::new()
}
}
impl DefaultObjectStoreRegistry {
/// This will register [`LocalFileSystem`] to handle `file://` paths
#[cfg(not(target_arch = "wasm32"))]
pub fn new() -> Self {
let object_stores: DashMap<String, Arc<dyn ObjectStore>> = DashMap::new();
object_stores.insert("file://".to_string(), Arc::new(LocalFileSystem::new()));
Self { object_stores }
}
/// Default without any backend registered.
#[cfg(target_arch = "wasm32")]
pub fn new() -> Self {
let object_stores: DashMap<String, Arc<dyn ObjectStore>> = DashMap::new();
Self { object_stores }
}
}
///
/// Stores are registered based on the scheme, host and port of the provided URL
/// with a [`LocalFileSystem::new`] automatically registered for `file://` (if the
/// target arch is not `wasm32`).
///
/// For example:
///
/// - `file:///my_path` will return the default LocalFS store
/// - `s3://bucket/path` will return a store registered with `s3://bucket` if any
/// - `hdfs://host:port/path` will return a store registered with `hdfs://host:port` if any
impl ObjectStoreRegistry for DefaultObjectStoreRegistry {
fn register_store(
&self,
url: &Url,
store: Arc<dyn ObjectStore>,
) -> Option<Arc<dyn ObjectStore>> {
let s = get_url_key(url);
self.object_stores.insert(s, store)
}
fn deregister_store(&self, url: &Url) -> Result<Arc<dyn ObjectStore>> {
let s = get_url_key(url);
let (_, object_store) = self.object_stores
.remove(&s)
.ok_or_else(|| {
internal_datafusion_err!("Failed to deregister object store. No suitable object store found for {url}. See `RuntimeEnv::register_object_store`")
})?;
Ok(object_store)
}
fn get_store(&self, url: &Url) -> Result<Arc<dyn ObjectStore>> {
let s = get_url_key(url);
self.object_stores
.get(&s)
.map(|o| Arc::clone(o.value()))
.ok_or_else(|| {
internal_datafusion_err!("No suitable object store found for {url}. See `RuntimeEnv::register_object_store`")
})
}
}
/// Get the key of a url for object store registration.
/// The credential info will be removed
fn get_url_key(url: &Url) -> String {
format!(
"{}://{}",
url.scheme(),
&url[url::Position::BeforeHost..url::Position::AfterPort],
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_object_store_url() {
let file = ObjectStoreUrl::parse("file://").unwrap();
assert_eq!(file.as_str(), "file:///");
let url = ObjectStoreUrl::parse("s3://bucket").unwrap();
assert_eq!(url.as_str(), "s3://bucket/");
let url = ObjectStoreUrl::parse("s3://username:password@host:123").unwrap();
assert_eq!(url.as_str(), "s3://username:password@host:123/");
let err = ObjectStoreUrl::parse("s3://bucket:invalid").unwrap_err();
assert_eq!(err.strip_backtrace(), "External error: invalid port number");
let err = ObjectStoreUrl::parse("s3://bucket?").unwrap_err();
assert_eq!(err.strip_backtrace(), "Execution error: ObjectStoreUrl must only contain scheme and authority, got: ?");
let err = ObjectStoreUrl::parse("s3://bucket?foo=bar").unwrap_err();
assert_eq!(err.strip_backtrace(), "Execution error: ObjectStoreUrl must only contain scheme and authority, got: ?foo=bar");
let err = ObjectStoreUrl::parse("s3://host:123/foo").unwrap_err();
assert_eq!(err.strip_backtrace(), "Execution error: ObjectStoreUrl must only contain scheme and authority, got: /foo");
let err =
ObjectStoreUrl::parse("s3://username:password@host:123/foo").unwrap_err();
assert_eq!(err.strip_backtrace(), "Execution error: ObjectStoreUrl must only contain scheme and authority, got: /foo");
}
#[test]
fn test_get_url_key() {
let file = ObjectStoreUrl::parse("file://").unwrap();
let key = get_url_key(&file.url);
assert_eq!(key.as_str(), "file://");
let url = ObjectStoreUrl::parse("s3://bucket").unwrap();
let key = get_url_key(&url.url);
assert_eq!(key.as_str(), "s3://bucket");
let url = ObjectStoreUrl::parse("s3://username:password@host:123").unwrap();
let key = get_url_key(&url.url);
assert_eq!(key.as_str(), "s3://host:123");
}
}