datafusion_cli/
catalog.rs1use std::sync::{Arc, Weak};
19
20use crate::object_storage::{AwsOptions, GcpOptions, get_object_store};
21
22use datafusion::catalog::{CatalogProvider, CatalogProviderList, SchemaProvider};
23
24use datafusion::common::plan_datafusion_err;
25use datafusion::datasource::TableProvider;
26use datafusion::datasource::listing::ListingTableUrl;
27use datafusion::error::Result;
28use datafusion::execution::context::SessionState;
29use datafusion::execution::session_state::SessionStateBuilder;
30
31use async_trait::async_trait;
32use dirs::home_dir;
33use parking_lot::RwLock;
34
35#[derive(Debug)]
37pub struct DynamicObjectStoreCatalog {
38 inner: Arc<dyn CatalogProviderList>,
39 state: Weak<RwLock<SessionState>>,
40}
41
42impl DynamicObjectStoreCatalog {
43 pub fn new(
44 inner: Arc<dyn CatalogProviderList>,
45 state: Weak<RwLock<SessionState>>,
46 ) -> Self {
47 Self { inner, state }
48 }
49}
50
51impl CatalogProviderList for DynamicObjectStoreCatalog {
52 fn register_catalog(
53 &self,
54 name: String,
55 catalog: Arc<dyn CatalogProvider>,
56 ) -> Option<Arc<dyn CatalogProvider>> {
57 self.inner.register_catalog(name, catalog)
58 }
59
60 fn catalog_names(&self) -> Vec<String> {
61 self.inner.catalog_names()
62 }
63
64 fn catalog(&self, name: &str) -> Option<Arc<dyn CatalogProvider>> {
65 let state = self.state.clone();
66 self.inner.catalog(name).map(|catalog| {
67 Arc::new(DynamicObjectStoreCatalogProvider::new(catalog, state)) as _
68 })
69 }
70}
71
72#[derive(Debug)]
74struct DynamicObjectStoreCatalogProvider {
75 inner: Arc<dyn CatalogProvider>,
76 state: Weak<RwLock<SessionState>>,
77}
78
79impl DynamicObjectStoreCatalogProvider {
80 pub fn new(
81 inner: Arc<dyn CatalogProvider>,
82 state: Weak<RwLock<SessionState>>,
83 ) -> Self {
84 Self { inner, state }
85 }
86}
87
88impl CatalogProvider for DynamicObjectStoreCatalogProvider {
89 fn schema_names(&self) -> Vec<String> {
90 self.inner.schema_names()
91 }
92
93 fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
94 let state = self.state.clone();
95 self.inner.schema(name).map(|schema| {
96 Arc::new(DynamicObjectStoreSchemaProvider::new(schema, state)) as _
97 })
98 }
99
100 fn register_schema(
101 &self,
102 name: &str,
103 schema: Arc<dyn SchemaProvider>,
104 ) -> Result<Option<Arc<dyn SchemaProvider>>> {
105 self.inner.register_schema(name, schema)
106 }
107}
108
109#[derive(Debug)]
112struct DynamicObjectStoreSchemaProvider {
113 inner: Arc<dyn SchemaProvider>,
114 state: Weak<RwLock<SessionState>>,
115}
116
117impl DynamicObjectStoreSchemaProvider {
118 pub fn new(
119 inner: Arc<dyn SchemaProvider>,
120 state: Weak<RwLock<SessionState>>,
121 ) -> Self {
122 Self { inner, state }
123 }
124}
125
126#[async_trait]
127impl SchemaProvider for DynamicObjectStoreSchemaProvider {
128 fn table_names(&self) -> Vec<String> {
129 self.inner.table_names()
130 }
131
132 fn register_table(
133 &self,
134 name: String,
135 table: Arc<dyn TableProvider>,
136 ) -> Result<Option<Arc<dyn TableProvider>>> {
137 self.inner.register_table(name, table)
138 }
139
140 async fn table(&self, name: &str) -> Result<Option<Arc<dyn TableProvider>>> {
141 let inner_table = self.inner.table(name).await;
142 if inner_table.is_ok()
143 && let Some(inner_table) = inner_table?
144 {
145 return Ok(Some(inner_table));
146 }
147
148 let mut state = self
151 .state
152 .upgrade()
153 .ok_or_else(|| plan_datafusion_err!("locking error"))?
154 .read()
155 .clone();
156 let mut builder = SessionStateBuilder::from(state.clone());
157 let optimized_name = substitute_tilde(name.to_owned());
158 let table_url = ListingTableUrl::parse(optimized_name.as_str())?;
159 let scheme = table_url.scheme();
160 let url = table_url.as_ref();
161
162 match state.runtime_env().object_store_registry.get_store(url) {
167 Ok(_) => { }
168 Err(_) => {
169 match scheme {
172 "s3" | "oss" | "cos" => {
173 if let Some(table_options) = builder.table_options() {
174 table_options.extensions.insert(AwsOptions::default())
175 }
176 }
177 "gs" | "gcs" => {
178 if let Some(table_options) = builder.table_options() {
179 table_options.extensions.insert(GcpOptions::default())
180 }
181 }
182 _ => {}
183 };
184 state = builder.build();
185 let store = get_object_store(
186 &state,
187 table_url.scheme(),
188 url,
189 &state.default_table_options(),
190 false,
191 )
192 .await?;
193 state.runtime_env().register_object_store(url, store);
194 }
195 }
196 self.inner.table(name).await
197 }
198
199 fn deregister_table(&self, name: &str) -> Result<Option<Arc<dyn TableProvider>>> {
200 self.inner.deregister_table(name)
201 }
202
203 fn table_exist(&self, name: &str) -> bool {
204 self.inner.table_exist(name)
205 }
206}
207
208pub fn substitute_tilde(cur: String) -> String {
209 if let Some(usr_dir_path) = home_dir()
210 && let Some(usr_dir) = usr_dir_path.to_str()
211 && cur.starts_with('~')
212 && !usr_dir.is_empty()
213 {
214 return cur.replacen('~', usr_dir, 1);
215 }
216 cur
217}
218#[cfg(test)]
219mod tests {
220 use std::{env, vec};
221
222 use super::*;
223
224 use datafusion::catalog::SchemaProvider;
225 use datafusion::prelude::SessionContext;
226
227 fn setup_context() -> (SessionContext, Arc<dyn SchemaProvider>) {
228 let ctx = SessionContext::new();
229 ctx.register_catalog_list(Arc::new(DynamicObjectStoreCatalog::new(
230 ctx.state().catalog_list().clone(),
231 ctx.state_weak_ref(),
232 )));
233
234 let provider = &DynamicObjectStoreCatalog::new(
235 ctx.state().catalog_list().clone(),
236 ctx.state_weak_ref(),
237 ) as &dyn CatalogProviderList;
238 let catalog = provider
239 .catalog(provider.catalog_names().first().unwrap())
240 .unwrap();
241 let schema = catalog
242 .schema(catalog.schema_names().first().unwrap())
243 .unwrap();
244 (ctx, schema)
245 }
246
247 #[tokio::test]
248 async fn query_http_location_test() -> Result<()> {
249 let domain = "example.com";
252 let location = format!("http://{domain}/file.parquet");
253
254 let (ctx, schema) = setup_context();
255
256 let table = schema.table(&location).await?;
258 assert!(table.is_none());
259
260 let store = ctx
262 .runtime_env()
263 .object_store(ListingTableUrl::parse(location)?)?;
264
265 assert_eq!(format!("{store}"), "HttpStore");
266
267 let expected_domain = format!("Domain(\"{domain}\")");
269 assert!(format!("{store:?}").contains(&expected_domain));
270
271 Ok(())
272 }
273
274 #[tokio::test]
275 async fn query_s3_location_test() -> Result<()> {
276 let aws_envs = vec![
277 "AWS_ENDPOINT",
278 "AWS_ACCESS_KEY_ID",
279 "AWS_SECRET_ACCESS_KEY",
280 "AWS_ALLOW_HTTP",
281 ];
282 for aws_env in aws_envs {
283 if env::var(aws_env).is_err() {
284 eprint!("aws envs not set, skipping s3 test");
285 return Ok(());
286 }
287 }
288
289 let bucket = "examples3bucket";
290 let location = format!("s3://{bucket}/file.parquet");
291
292 let (ctx, schema) = setup_context();
293
294 let table = schema.table(&location).await?;
295 assert!(table.is_none());
296
297 let store = ctx
298 .runtime_env()
299 .object_store(ListingTableUrl::parse(location)?)?;
300 assert_eq!(format!("{store}"), format!("AmazonS3({bucket})"));
301
302 let expected_bucket = format!("bucket: \"{bucket}\"");
304 assert!(format!("{store:?}").contains(&expected_bucket));
305
306 Ok(())
307 }
308
309 #[tokio::test]
310 async fn query_gs_location_test() -> Result<()> {
311 let bucket = "examplegsbucket";
312 let location = format!("gs://{bucket}/file.parquet");
313
314 let (ctx, schema) = setup_context();
315
316 let table = schema.table(&location).await?;
317 assert!(table.is_none());
318
319 let store = ctx
320 .runtime_env()
321 .object_store(ListingTableUrl::parse(location)?)?;
322 assert_eq!(format!("{store}"), format!("GoogleCloudStorage({bucket})"));
323
324 let expected_bucket = format!("bucket_name_encoded: \"{bucket}\"");
326 assert!(format!("{store:?}").contains(&expected_bucket));
327
328 Ok(())
329 }
330
331 #[tokio::test]
332 async fn query_invalid_location_test() {
333 let location = "ts://file.parquet";
334 let (_ctx, schema) = setup_context();
335
336 assert!(schema.table(location).await.is_err());
337 }
338
339 #[cfg(not(target_os = "windows"))]
340 #[test]
341 fn test_substitute_tilde() {
342 use std::{env, path::PathBuf};
343 let original_home = home_dir();
344 let test_home_path = if cfg!(windows) {
345 "C:\\Users\\user"
346 } else {
347 "/home/user"
348 };
349 unsafe {
350 env::set_var(
351 if cfg!(windows) { "USERPROFILE" } else { "HOME" },
352 test_home_path,
353 );
354 }
355 let input = "~/Code/datafusion/benchmarks/data/tpch_sf1/part/part-0.parquet";
356 let expected = PathBuf::from(test_home_path)
357 .join("Code")
358 .join("datafusion")
359 .join("benchmarks")
360 .join("data")
361 .join("tpch_sf1")
362 .join("part")
363 .join("part-0.parquet")
364 .to_string_lossy()
365 .to_string();
366 let actual = substitute_tilde(input.to_string());
367 assert_eq!(actual, expected);
368 unsafe {
369 match original_home {
370 Some(home_path) => env::set_var(
371 if cfg!(windows) { "USERPROFILE" } else { "HOME" },
372 home_path.to_str().unwrap(),
373 ),
374 None => {
375 env::remove_var(if cfg!(windows) { "USERPROFILE" } else { "HOME" })
376 }
377 }
378 }
379 }
380}