Skip to main content

krishiv_sql/
object_store_registry.rs

1//! An object-store registry that builds cloud stores on first use.
2//!
3//! DataFusion resolves an object store by scheme+authority through the
4//! runtime's [`ObjectStoreRegistry`]. The default registry only knows what was
5//! explicitly registered, which forces every code path that might touch
6//! `s3://` to remember to register the bucket first. That is a rule nothing
7//! enforces, and the paths that forgot did not fail loudly:
8//!
9//! - the stage builder planned on a context with no store, so `register_parquet`
10//!   errored, the caller read that as "decline to stage", and the query silently
11//!   ran on ONE executor instead of the cluster;
12//! - the executor decoding a `dfplan:` fragment failed outright with
13//!   "No suitable object store found for s3://…", because a serialized physical
14//!   plan carries file paths but no way to register their backing store.
15//!
16//! The executor case in particular cannot be fixed by registering ahead of
17//! time: the executor learns which buckets a plan touches only by decoding the
18//! plan, and the decode is what needs the store. So resolution has to be lazy.
19//!
20//! This registry delegates to the default one and, on a miss for an
21//! object-store scheme it can construct, builds the store, caches it, and
22//! returns it. Explicit registration still wins, so a caller that wants
23//! specific credentials or an emulator endpoint can install its own store and
24//! this never overrides it.
25
26use std::sync::Arc;
27
28use datafusion::error::{DataFusionError, Result as DataFusionResult};
29use datafusion::execution::object_store::{DefaultObjectStoreRegistry, ObjectStoreRegistry};
30use object_store::ObjectStore;
31use url::Url;
32
33/// Registry that lazily constructs S3-compatible stores on first reference.
34#[derive(Debug, Default)]
35pub struct LazyCloudObjectStoreRegistry {
36    inner: DefaultObjectStoreRegistry,
37}
38
39impl LazyCloudObjectStoreRegistry {
40    pub fn new() -> Self {
41        Self::default()
42    }
43}
44
45impl ObjectStoreRegistry for LazyCloudObjectStoreRegistry {
46    fn register_store(
47        &self,
48        url: &Url,
49        store: Arc<dyn ObjectStore>,
50    ) -> Option<Arc<dyn ObjectStore>> {
51        self.inner.register_store(url, store)
52    }
53
54    fn get_store(&self, url: &Url) -> DataFusionResult<Arc<dyn ObjectStore>> {
55        // Explicit registration wins: only fall through to construction when
56        // the default registry does not already have a store for this bucket.
57        if let Ok(store) = self.inner.get_store(url) {
58            return Ok(store);
59        }
60
61        if matches!(url.scheme(), "s3" | "s3a") {
62            let bucket = url.host_str().unwrap_or_default();
63            if bucket.is_empty() {
64                return Err(DataFusionError::Execution(format!(
65                    "object-store url {url} has no bucket"
66                )));
67            }
68            let store = crate::build_s3_object_store(bucket).map_err(|error| {
69                DataFusionError::Execution(format!(
70                    "cannot build an S3 object store for bucket '{bucket}': {error}"
71                ))
72            })?;
73            let store: Arc<dyn ObjectStore> = store;
74            // Cache under the scheme+authority key DataFusion looks up, so the
75            // next reference to this bucket is a plain map hit.
76            self.inner.register_store(url, Arc::clone(&store));
77            return Ok(store);
78        }
79
80        // Not a scheme we can construct: surface the default registry's own
81        // error, which names the url and points at register_object_store.
82        self.inner.get_store(url)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    /// The point of the registry: a bucket nobody registered still resolves.
91    /// Under the default registry this is an error, which is exactly what made
92    /// staged planning decline and dfplan decode fail.
93    #[test]
94    fn an_unregistered_s3_bucket_resolves() {
95        let registry = LazyCloudObjectStoreRegistry::new();
96        let url = Url::parse("s3://krishiv-bench/tpch/sf100/lineitem/").expect("url");
97        assert!(
98            registry.get_store(&url).is_ok(),
99            "an s3 bucket must resolve without prior registration"
100        );
101    }
102
103    /// Two references to the same bucket must return the same cached store
104    /// rather than rebuilding a client per file scan.
105    #[test]
106    fn repeated_lookups_reuse_one_store() {
107        let registry = LazyCloudObjectStoreRegistry::new();
108        let url = Url::parse("s3://krishiv-bench/a").expect("url");
109        let first = registry.get_store(&url).expect("first lookup");
110        let second = registry.get_store(&url).expect("second lookup");
111        assert!(
112            Arc::ptr_eq(&first, &second),
113            "the store must be cached, not rebuilt per lookup"
114        );
115    }
116
117    /// Explicit registration must not be overridden — callers that install a
118    /// store with specific credentials keep it.
119    #[test]
120    fn explicit_registration_wins() {
121        let registry = LazyCloudObjectStoreRegistry::new();
122        let url = Url::parse("s3://explicit-bucket/").expect("url");
123        let installed: Arc<dyn ObjectStore> = Arc::new(object_store::memory::InMemory::new());
124        registry.register_store(&url, Arc::clone(&installed));
125        let resolved = registry.get_store(&url).expect("lookup");
126        assert!(
127            Arc::ptr_eq(&installed, &resolved),
128            "an explicitly registered store must win over lazy construction"
129        );
130    }
131
132    /// A scheme the registry cannot build must still produce the default
133    /// registry's error rather than a confusing S3-flavoured one.
134    #[test]
135    fn unknown_schemes_keep_the_default_error() {
136        let registry = LazyCloudObjectStoreRegistry::new();
137        let url = Url::parse("ftp://somewhere/path").expect("url");
138        assert!(registry.get_store(&url).is_err());
139    }
140}