Skip to main content

buoyant_kernel_engine/
storage.rs

1use std::collections::HashMap;
2use std::sync::{Arc, LazyLock, RwLock};
3
4use delta_kernel::object_store::path::Path;
5use delta_kernel::object_store::{self, Error, ObjectStore};
6use delta_kernel::Error as DeltaError;
7use url::Url;
8
9/// Alias for convenience
10type ClosureReturn = Result<(Box<dyn ObjectStore>, Path), Error>;
11/// This type alias makes it easier to reference the handler closure(s)
12///
13/// It uses a HashMap<String, String> which _must_ be converted in [store_from_url_opts]
14/// because we cannot use generics in this scenario.
15type HandlerClosure = Arc<dyn Fn(&Url, HashMap<String, String>) -> ClosureReturn + Send + Sync>;
16/// hashmap containing scheme => handler fn mappings to allow consumers of delta-kernel-rs provide
17/// their own url opts parsers for different scemes
18type Handlers = HashMap<String, HandlerClosure>;
19/// The URL_REGISTRY contains the custom URL scheme handlers that will parse URL options
20static URL_REGISTRY: LazyLock<RwLock<Handlers>> = LazyLock::new(|| RwLock::new(HashMap::default()));
21
22/// Insert a new URL handler for [store_from_url_opts] with the given `scheme`. This allows
23/// users to provide their own custom URL handler to plug new
24/// [delta_kernel::object_store::ObjectStore] instances into delta-kernel, which is used by
25/// [store_from_url_opts] to parse the URL.
26pub fn insert_url_handler(
27    scheme: impl AsRef<str>,
28    handler_closure: HandlerClosure,
29) -> Result<(), DeltaError> {
30    let Ok(mut registry) = URL_REGISTRY.write() else {
31        return Err(DeltaError::generic(
32            "failed to acquire lock for adding a URL handler!",
33        ));
34    };
35    registry.insert(scheme.as_ref().into(), handler_closure);
36    Ok(())
37}
38
39/// Create an [`ObjectStore`] from a URL.
40///
41/// Returns an `Arc<dyn ObjectStore>` ready to use with [`crate::DefaultEngine`].
42///
43/// This function checks for custom URL handlers registered via [`insert_url_handler`]
44/// before falling back to [`object_store`]'s default behavior.
45///
46/// # Example
47///
48/// ```rust
49/// # use url::Url;
50/// # use buoyant_kernel_engine as delta_kernel_default_engine;
51/// # use delta_kernel_default_engine::storage::store_from_url;
52/// # use delta_kernel::DeltaResult;
53/// # fn example() -> DeltaResult<()> {
54/// let url = Url::parse("file:///path/to/table")?;
55/// let store = store_from_url(&url)?;
56/// # Ok(())
57/// # }
58/// ```
59pub fn store_from_url(url: &Url) -> delta_kernel::DeltaResult<Arc<dyn ObjectStore>> {
60    store_from_url_opts(url, std::iter::empty::<(&str, &str)>())
61}
62
63/// Create an [`ObjectStore`] from a URL with custom options.
64///
65/// Returns an `Arc<dyn ObjectStore>` ready to use with [`crate::DefaultEngine`].
66///
67/// This function checks for custom URL handlers registered via [`insert_url_handler`]
68/// before falling back to [`object_store`]'s default behavior.
69///
70/// # Example
71///
72/// ```rust
73/// # use url::Url;
74/// # use std::collections::HashMap;
75/// # use buoyant_kernel_engine as delta_kernel_default_engine;
76/// # use delta_kernel_default_engine::storage::store_from_url_opts;
77/// # use delta_kernel::DeltaResult;
78/// # fn example() -> DeltaResult<()> {
79/// let url = Url::parse("s3://my-bucket/path/to/table")?;
80/// let options = HashMap::from([("region", "us-west-2")]);
81/// let store = store_from_url_opts(&url, options)?;
82/// # Ok(())
83/// # }
84/// ```
85pub fn store_from_url_opts<I, K, V>(
86    url: &Url,
87    options: I,
88) -> delta_kernel::DeltaResult<Arc<dyn ObjectStore>>
89where
90    I: IntoIterator<Item = (K, V)>,
91    K: AsRef<str>,
92    V: Into<String>,
93{
94    // First attempt to use any schemes registered via insert_url_handler,
95    // falling back to the default behavior of delta_kernel::object_store::parse_url_opts
96    let (store, _path) = if let Ok(handlers) = URL_REGISTRY.read() {
97        if let Some(handler) = handlers.get(url.scheme()) {
98            let options = options
99                .into_iter()
100                .map(|(k, v)| (k.as_ref().to_string(), v.into()))
101                .collect();
102            handler(url, options)?
103        } else {
104            object_store::parse_url_opts(url, options)?
105        }
106    } else {
107        object_store::parse_url_opts(url, options)?
108    };
109
110    Ok(Arc::new(store))
111}
112
113#[cfg(test)]
114mod tests {
115    use std::collections::HashMap;
116
117    use delta_kernel::object_store::path::Path;
118    use delta_kernel::object_store::{self, ObjectStore};
119    use hdfs_native_object_store::HdfsObjectStoreBuilder;
120
121    use super::{insert_url_handler, store_from_url_opts, URL_REGISTRY};
122    use crate::*;
123
124    /// Example funciton of doing testing of a custom [HdfsObjectStore] construction
125    fn parse_url_opts_hdfs_native<I, K, V>(
126        url: &Url,
127        options: I,
128    ) -> Result<(Box<dyn ObjectStore>, Path), object_store::Error>
129    where
130        I: IntoIterator<Item = (K, V)>,
131        K: AsRef<str>,
132        V: Into<String>,
133    {
134        let options_map = options
135            .into_iter()
136            .map(|(k, v)| (k.as_ref().to_string(), v.into()));
137        let store = HdfsObjectStoreBuilder::new()
138            .with_url(url.as_str())
139            .with_config(options_map)
140            .build()?;
141        let path = Path::parse(url.path())?;
142        Ok((Box::new(store), path))
143    }
144
145    #[test]
146    fn test_add_hdfs_scheme() {
147        let scheme = "hdfs";
148        if let Ok(handlers) = URL_REGISTRY.read() {
149            assert!(handlers.get(scheme).is_none());
150        } else {
151            panic!("Failed to read the RwLock for the registry");
152        }
153        insert_url_handler(scheme, Arc::new(parse_url_opts_hdfs_native))
154            .expect("Failed to add new URL scheme handler");
155
156        if let Ok(handlers) = URL_REGISTRY.read() {
157            assert!(handlers.get(scheme).is_some());
158        } else {
159            panic!("Failed to read the RwLock for the registry");
160        }
161
162        let url: Url = Url::parse("hdfs://example").expect("Failed to parse URL");
163        let options: HashMap<String, String> = HashMap::default();
164        // Currently constructing an [HdfsObjectStore] won't work if there isn't an actual HDFS
165        // to connect to, so the only way to really verify that we got the object store we
166        // expected is to inspect the `store` on the error v_v
167        match store_from_url_opts(&url, options) {
168            Err(delta_kernel::Error::ObjectStore(object_store::Error::Generic {
169                store,
170                source: _,
171            })) => {
172                assert_eq!(store, "HdfsObjectStore");
173            }
174            Err(unexpected) => panic!("Unexpected error happened: {unexpected:?}"),
175            Ok(_) => {
176                panic!("Expected to get an error when constructing an HdfsObjectStore, but something didn't work as expected! Either the parse_url_opts_hdfs_native function didn't get called, or the hdfs-native-object-store no longer errors when it cannot connect to HDFS");
177            }
178        }
179    }
180}