This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new 9bc39a0522 Introduce the `DynamicFileCatalog` in `datafusion-catalog` 
(#11035)
9bc39a0522 is described below

commit 9bc39a0522840ed90de2a4d23157de2e192cd00f
Author: Jax Liu <[email protected]>
AuthorDate: Mon Sep 9 21:09:07 2024 +0800

    Introduce the `DynamicFileCatalog` in `datafusion-catalog` (#11035)
    
    * early draft
    
    * fmt
    
    * add example for dynamic file query
    
    * add test and refactor
    
    * clippy and add doc
    
    * cargo fmt
    
    * extract substitute_tilde function
    
    * fix the error handling
    
    * fmt and clippy
    
    * fix test
    
    * fix sqllogictests
    
    * ignore dirs for windows test
    
    * enhance the test for every file format
    
    * disable the test for windows
    
    * make dynamic file query configurable
    
    * revert array_query.slt
    
    * modified the test and add example
    
    * make dirs be optional
    
    * enable dynamic file query in cli
    
    * cargo fmt
    
    * modified example
    
    * fix test
    
    * fix merge conflict
    
    * tmp
    
    * tmp
    
    * tmp
    
    * fix the catalog and schema
    
    * move dynamic file catalog to datafusion-catalog
    
    * add copyright
    
    * fix tests
    
    * rename catalog in cli and update lock
    
    * enable home_dir feature
    
    * update lock
    
    * fix compile
    
    * fix clippy
    
    * fmt toml
    
    * fix doc test and add more doc
    
    * fix clippy
    
    * add home_dir feature doc
    
    * rollback the unused changed
    
    * update lock
    
    * fix sqllogictest
    
    * separate dynamic file test to another slt
    
    * add test for querying url table but disabled this feature
    
    * add dynamic_file.slt
    
    * remove home_dir feature
    
    * update cli lock
    
    * fix msrv check
    
    * fix msrv check
    
    * rollback the lock change
    
    * address review comment and enhance the doc
    
    * remove the legacy comment
    
    * add missing doc
---
 datafusion-cli/Cargo.lock                          |   1 +
 datafusion-cli/src/catalog.rs                      |  76 ++++-----
 datafusion-cli/src/main.rs                         |  10 +-
 datafusion-examples/examples/dataframe.rs          |   6 +
 .../examples/external_dependency/query-aws-s3.rs   |   9 +
 datafusion/catalog/Cargo.toml                      |   1 +
 datafusion/catalog/src/dynamic_file/catalog.rs     | 183 +++++++++++++++++++++
 .../catalog/src/{lib.rs => dynamic_file/mod.rs}    |  10 +-
 datafusion/catalog/src/lib.rs                      |   2 +
 datafusion/catalog/src/session.rs                  |  34 +++-
 datafusion/core/src/datasource/dynamic_file.rs     |  80 +++++++++
 datafusion/core/src/datasource/mod.rs              |   1 +
 datafusion/core/src/execution/context/mod.rs       |  90 +++++++++-
 datafusion/sqllogictest/src/test_context.rs        |   3 +
 datafusion/sqllogictest/test_files/arrow_files.slt |   5 +
 datafusion/sqllogictest/test_files/csv_files.slt   |   5 +
 datafusion/sqllogictest/test_files/describe.slt    |   2 +-
 .../sqllogictest/test_files/dynamic_file.slt       | 106 ++++++++++++
 datafusion/sqllogictest/test_files/json.slt        |   5 +
 datafusion/sqllogictest/test_files/parquet.slt     |   5 +
 20 files changed, 573 insertions(+), 61 deletions(-)

diff --git a/datafusion-cli/Cargo.lock b/datafusion-cli/Cargo.lock
index ddc33b43d1..d3af3d5596 100644
--- a/datafusion-cli/Cargo.lock
+++ b/datafusion-cli/Cargo.lock
@@ -1261,6 +1261,7 @@ dependencies = [
  "datafusion-execution",
  "datafusion-expr",
  "datafusion-physical-plan",
+ "parking_lot",
 ]
 
 [[package]]
diff --git a/datafusion-cli/src/catalog.rs b/datafusion-cli/src/catalog.rs
index c4636f1ce0..9b9afc1c24 100644
--- a/datafusion-cli/src/catalog.rs
+++ b/datafusion-cli/src/catalog.rs
@@ -21,10 +21,9 @@ use std::sync::{Arc, Weak};
 use crate::object_storage::{get_object_store, AwsOptions, GcpOptions};
 
 use datafusion::catalog::{CatalogProvider, CatalogProviderList, 
SchemaProvider};
+
 use datafusion::common::plan_datafusion_err;
-use datafusion::datasource::listing::{
-    ListingTable, ListingTableConfig, ListingTableUrl,
-};
+use datafusion::datasource::listing::ListingTableUrl;
 use datafusion::datasource::TableProvider;
 use datafusion::error::Result;
 use datafusion::execution::context::SessionState;
@@ -34,14 +33,13 @@ use async_trait::async_trait;
 use dirs::home_dir;
 use parking_lot::RwLock;
 
-/// Wraps another catalog, automatically creating table providers
-/// for local files if needed
-pub struct DynamicFileCatalog {
+/// Wraps another catalog, automatically register require object stores for 
the file locations
+pub struct DynamicObjectStoreCatalog {
     inner: Arc<dyn CatalogProviderList>,
     state: Weak<RwLock<SessionState>>,
 }
 
-impl DynamicFileCatalog {
+impl DynamicObjectStoreCatalog {
     pub fn new(
         inner: Arc<dyn CatalogProviderList>,
         state: Weak<RwLock<SessionState>>,
@@ -50,7 +48,7 @@ impl DynamicFileCatalog {
     }
 }
 
-impl CatalogProviderList for DynamicFileCatalog {
+impl CatalogProviderList for DynamicObjectStoreCatalog {
     fn as_any(&self) -> &dyn Any {
         self
     }
@@ -69,19 +67,19 @@ impl CatalogProviderList for DynamicFileCatalog {
 
     fn catalog(&self, name: &str) -> Option<Arc<dyn CatalogProvider>> {
         let state = self.state.clone();
-        self.inner
-            .catalog(name)
-            .map(|catalog| Arc::new(DynamicFileCatalogProvider::new(catalog, 
state)) as _)
+        self.inner.catalog(name).map(|catalog| {
+            Arc::new(DynamicObjectStoreCatalogProvider::new(catalog, state)) 
as _
+        })
     }
 }
 
 /// Wraps another catalog provider
-struct DynamicFileCatalogProvider {
+struct DynamicObjectStoreCatalogProvider {
     inner: Arc<dyn CatalogProvider>,
     state: Weak<RwLock<SessionState>>,
 }
 
-impl DynamicFileCatalogProvider {
+impl DynamicObjectStoreCatalogProvider {
     pub fn new(
         inner: Arc<dyn CatalogProvider>,
         state: Weak<RwLock<SessionState>>,
@@ -90,7 +88,7 @@ impl DynamicFileCatalogProvider {
     }
 }
 
-impl CatalogProvider for DynamicFileCatalogProvider {
+impl CatalogProvider for DynamicObjectStoreCatalogProvider {
     fn as_any(&self) -> &dyn Any {
         self
     }
@@ -101,9 +99,9 @@ impl CatalogProvider for DynamicFileCatalogProvider {
 
     fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
         let state = self.state.clone();
-        self.inner
-            .schema(name)
-            .map(|schema| Arc::new(DynamicFileSchemaProvider::new(schema, 
state)) as _)
+        self.inner.schema(name).map(|schema| {
+            Arc::new(DynamicObjectStoreSchemaProvider::new(schema, state)) as _
+        })
     }
 
     fn register_schema(
@@ -115,13 +113,14 @@ impl CatalogProvider for DynamicFileCatalogProvider {
     }
 }
 
-/// Wraps another schema provider
-struct DynamicFileSchemaProvider {
+/// Wraps another schema provider. [DynamicObjectStoreSchemaProvider] is 
responsible for registering the required
+/// object stores for the file locations.
+struct DynamicObjectStoreSchemaProvider {
     inner: Arc<dyn SchemaProvider>,
     state: Weak<RwLock<SessionState>>,
 }
 
-impl DynamicFileSchemaProvider {
+impl DynamicObjectStoreSchemaProvider {
     pub fn new(
         inner: Arc<dyn SchemaProvider>,
         state: Weak<RwLock<SessionState>>,
@@ -131,7 +130,7 @@ impl DynamicFileSchemaProvider {
 }
 
 #[async_trait]
-impl SchemaProvider for DynamicFileSchemaProvider {
+impl SchemaProvider for DynamicObjectStoreSchemaProvider {
     fn as_any(&self) -> &dyn Any {
         self
     }
@@ -149,9 +148,11 @@ impl SchemaProvider for DynamicFileSchemaProvider {
     }
 
     async fn table(&self, name: &str) -> Result<Option<Arc<dyn 
TableProvider>>> {
-        let inner_table = self.inner.table(name).await?;
-        if inner_table.is_some() {
-            return Ok(inner_table);
+        let inner_table = self.inner.table(name).await;
+        if inner_table.is_ok() {
+            if let Some(inner_table) = inner_table? {
+                return Ok(Some(inner_table));
+            }
         }
 
         // if the inner schema provider didn't have a table by
@@ -201,16 +202,7 @@ impl SchemaProvider for DynamicFileSchemaProvider {
                 state.runtime_env().register_object_store(url, store);
             }
         }
-
-        let config = match 
ListingTableConfig::new(table_url).infer(&state).await {
-            Ok(cfg) => cfg,
-            Err(_) => {
-                // treat as non-existing
-                return Ok(None);
-            }
-        };
-
-        Ok(Some(Arc::new(ListingTable::try_new(config)?)))
+        self.inner.table(name).await
     }
 
     fn deregister_table(&self, name: &str) -> Result<Option<Arc<dyn 
TableProvider>>> {
@@ -221,7 +213,8 @@ impl SchemaProvider for DynamicFileSchemaProvider {
         self.inner.table_exist(name)
     }
 }
-fn substitute_tilde(cur: String) -> String {
+
+pub fn substitute_tilde(cur: String) -> String {
     if let Some(usr_dir_path) = home_dir() {
         if let Some(usr_dir) = usr_dir_path.to_str() {
             if cur.starts_with('~') && !usr_dir.is_empty() {
@@ -231,9 +224,9 @@ fn substitute_tilde(cur: String) -> String {
     }
     cur
 }
-
 #[cfg(test)]
 mod tests {
+
     use super::*;
 
     use datafusion::catalog::SchemaProvider;
@@ -241,12 +234,12 @@ mod tests {
 
     fn setup_context() -> (SessionContext, Arc<dyn SchemaProvider>) {
         let ctx = SessionContext::new();
-        ctx.register_catalog_list(Arc::new(DynamicFileCatalog::new(
+        ctx.register_catalog_list(Arc::new(DynamicObjectStoreCatalog::new(
             ctx.state().catalog_list().clone(),
             ctx.state_weak_ref(),
         )));
 
-        let provider = &DynamicFileCatalog::new(
+        let provider = &DynamicObjectStoreCatalog::new(
             ctx.state().catalog_list().clone(),
             ctx.state_weak_ref(),
         ) as &dyn CatalogProviderList;
@@ -269,7 +262,7 @@ mod tests {
         let (ctx, schema) = setup_context();
 
         // That's a non registered table so expecting None here
-        let table = schema.table(&location).await.unwrap();
+        let table = schema.table(&location).await?;
         assert!(table.is_none());
 
         // It should still create an object store for the location in the 
SessionState
@@ -293,7 +286,7 @@ mod tests {
 
         let (ctx, schema) = setup_context();
 
-        let table = schema.table(&location).await.unwrap();
+        let table = schema.table(&location).await?;
         assert!(table.is_none());
 
         let store = ctx
@@ -315,7 +308,7 @@ mod tests {
 
         let (ctx, schema) = setup_context();
 
-        let table = schema.table(&location).await.unwrap();
+        let table = schema.table(&location).await?;
         assert!(table.is_none());
 
         let store = ctx
@@ -337,6 +330,7 @@ mod tests {
 
         assert!(schema.table(location).await.is_err());
     }
+
     #[cfg(not(target_os = "windows"))]
     #[test]
     fn test_substitute_tilde() {
diff --git a/datafusion-cli/src/main.rs b/datafusion-cli/src/main.rs
index 6e94e6ea41..cdefada5e2 100644
--- a/datafusion-cli/src/main.rs
+++ b/datafusion-cli/src/main.rs
@@ -26,7 +26,7 @@ use datafusion::execution::context::SessionConfig;
 use datafusion::execution::memory_pool::{FairSpillPool, GreedyMemoryPool};
 use datafusion::execution::runtime_env::{RuntimeConfig, RuntimeEnv};
 use datafusion::prelude::SessionContext;
-use datafusion_cli::catalog::DynamicFileCatalog;
+use datafusion_cli::catalog::DynamicObjectStoreCatalog;
 use datafusion_cli::functions::ParquetMetadataFunc;
 use datafusion_cli::{
     exec,
@@ -173,11 +173,13 @@ async fn main_inner() -> Result<()> {
 
     let runtime_env = create_runtime_env(rt_config.clone())?;
 
+    // enable dynamic file query
     let ctx =
-        SessionContext::new_with_config_rt(session_config.clone(), 
Arc::new(runtime_env));
+        SessionContext::new_with_config_rt(session_config.clone(), 
Arc::new(runtime_env))
+            .enable_url_table();
     ctx.refresh_catalogs().await?;
-    // install dynamic catalog provider that knows how to open files
-    ctx.register_catalog_list(Arc::new(DynamicFileCatalog::new(
+    // install dynamic catalog provider that can register required object 
stores
+    ctx.register_catalog_list(Arc::new(DynamicObjectStoreCatalog::new(
         ctx.state().catalog_list().clone(),
         ctx.state_weak_ref(),
     )));
diff --git a/datafusion-examples/examples/dataframe.rs 
b/datafusion-examples/examples/dataframe.rs
index ea01c53b1c..d7e0068ef8 100644
--- a/datafusion-examples/examples/dataframe.rs
+++ b/datafusion-examples/examples/dataframe.rs
@@ -64,6 +64,12 @@ async fn main() -> Result<()> {
         .await?;
     parquet_df.describe().await.unwrap().show().await?;
 
+    let dyn_ctx = ctx.enable_url_table();
+    let df = dyn_ctx
+        .sql(&format!("SELECT * FROM '{}'", file_path.to_str().unwrap()))
+        .await?;
+    df.show().await?;
+
     Ok(())
 }
 
diff --git a/datafusion-examples/examples/external_dependency/query-aws-s3.rs 
b/datafusion-examples/examples/external_dependency/query-aws-s3.rs
index e32286e30e..9c4d76703c 100644
--- a/datafusion-examples/examples/external_dependency/query-aws-s3.rs
+++ b/datafusion-examples/examples/external_dependency/query-aws-s3.rs
@@ -63,5 +63,14 @@ async fn main() -> Result<()> {
     // print the results
     df.show().await?;
 
+    // dynamic query by the file path
+    ctx.enable_url_table();
+    let df = ctx
+        .sql(format!(r#"SELECT * FROM '{}' LIMIT 10"#, &path).as_str())
+        .await?;
+
+    // print the results
+    df.show().await?;
+
     Ok(())
 }
diff --git a/datafusion/catalog/Cargo.toml b/datafusion/catalog/Cargo.toml
index 533bd1eeba..f980135208 100644
--- a/datafusion/catalog/Cargo.toml
+++ b/datafusion/catalog/Cargo.toml
@@ -34,6 +34,7 @@ datafusion-common = { workspace = true }
 datafusion-execution = { workspace = true }
 datafusion-expr = { workspace = true }
 datafusion-physical-plan = { workspace = true }
+parking_lot = { workspace = true }
 
 [lints]
 workspace = true
diff --git a/datafusion/catalog/src/dynamic_file/catalog.rs 
b/datafusion/catalog/src/dynamic_file/catalog.rs
new file mode 100644
index 0000000000..cd586446f8
--- /dev/null
+++ b/datafusion/catalog/src/dynamic_file/catalog.rs
@@ -0,0 +1,183 @@
+// 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.
+
+//! [`DynamicFileCatalog`] that creates tables from file paths
+
+use crate::{CatalogProvider, CatalogProviderList, SchemaProvider, 
TableProvider};
+use async_trait::async_trait;
+use std::any::Any;
+use std::sync::Arc;
+
+/// Wrap another catalog provider list
+pub struct DynamicFileCatalog {
+    /// The inner catalog provider list
+    inner: Arc<dyn CatalogProviderList>,
+    /// The factory that can create a table provider from the file path
+    factory: Arc<dyn UrlTableFactory>,
+}
+
+impl DynamicFileCatalog {
+    pub fn new(
+        inner: Arc<dyn CatalogProviderList>,
+        factory: Arc<dyn UrlTableFactory>,
+    ) -> Self {
+        Self { inner, factory }
+    }
+}
+
+impl CatalogProviderList for DynamicFileCatalog {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn register_catalog(
+        &self,
+        name: String,
+        catalog: Arc<dyn CatalogProvider>,
+    ) -> Option<Arc<dyn CatalogProvider>> {
+        self.inner.register_catalog(name, catalog)
+    }
+
+    fn catalog_names(&self) -> Vec<String> {
+        self.inner.catalog_names()
+    }
+
+    fn catalog(&self, name: &str) -> Option<Arc<dyn CatalogProvider>> {
+        self.inner.catalog(name).map(|catalog| {
+            Arc::new(DynamicFileCatalogProvider::new(
+                catalog,
+                Arc::clone(&self.factory),
+            )) as _
+        })
+    }
+}
+
+/// Wraps another catalog provider
+struct DynamicFileCatalogProvider {
+    /// The inner catalog provider
+    inner: Arc<dyn CatalogProvider>,
+    /// The factory that can create a table provider from the file path
+    factory: Arc<dyn UrlTableFactory>,
+}
+
+impl DynamicFileCatalogProvider {
+    pub fn new(
+        inner: Arc<dyn CatalogProvider>,
+        factory: Arc<dyn UrlTableFactory>,
+    ) -> Self {
+        Self { inner, factory }
+    }
+}
+
+impl CatalogProvider for DynamicFileCatalogProvider {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn schema_names(&self) -> Vec<String> {
+        self.inner.schema_names()
+    }
+
+    fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
+        self.inner.schema(name).map(|schema| {
+            Arc::new(DynamicFileSchemaProvider::new(
+                schema,
+                Arc::clone(&self.factory),
+            )) as _
+        })
+    }
+
+    fn register_schema(
+        &self,
+        name: &str,
+        schema: Arc<dyn SchemaProvider>,
+    ) -> datafusion_common::Result<Option<Arc<dyn SchemaProvider>>> {
+        self.inner.register_schema(name, schema)
+    }
+}
+
+/// Implements the [DynamicFileSchemaProvider] that can create tables provider 
from the file path.
+///
+/// The provider will try to create a table provider from the file path if the 
table provider
+/// isn't exist in the inner schema provider.
+pub struct DynamicFileSchemaProvider {
+    /// The inner schema provider
+    inner: Arc<dyn SchemaProvider>,
+    /// The factory that can create a table provider from the file path
+    factory: Arc<dyn UrlTableFactory>,
+}
+
+impl DynamicFileSchemaProvider {
+    /// Create a new [DynamicFileSchemaProvider] with the given inner schema 
provider.
+    pub fn new(
+        inner: Arc<dyn SchemaProvider>,
+        factory: Arc<dyn UrlTableFactory>,
+    ) -> Self {
+        Self { inner, factory }
+    }
+}
+
+#[async_trait]
+impl SchemaProvider for DynamicFileSchemaProvider {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn table_names(&self) -> Vec<String> {
+        self.inner.table_names()
+    }
+
+    async fn table(
+        &self,
+        name: &str,
+    ) -> datafusion_common::Result<Option<Arc<dyn TableProvider>>> {
+        if let Some(table) = self.inner.table(name).await? {
+            return Ok(Some(table));
+        };
+
+        self.factory.try_new(name).await
+    }
+
+    fn register_table(
+        &self,
+        name: String,
+        table: Arc<dyn TableProvider>,
+    ) -> datafusion_common::Result<Option<Arc<dyn TableProvider>>> {
+        self.inner.register_table(name, table)
+    }
+
+    fn deregister_table(
+        &self,
+        name: &str,
+    ) -> datafusion_common::Result<Option<Arc<dyn TableProvider>>> {
+        self.inner.deregister_table(name)
+    }
+
+    fn table_exist(&self, name: &str) -> bool {
+        self.inner.table_exist(name)
+    }
+}
+
+/// [UrlTableFactory] is a factory that can create a table provider from the 
given url.
+#[async_trait]
+pub trait UrlTableFactory: Sync + Send {
+    /// create a new table provider from the provided url
+    async fn try_new(
+        &self,
+        url: &str,
+    ) -> datafusion_common::Result<Option<Arc<dyn TableProvider>>>;
+}
diff --git a/datafusion/catalog/src/lib.rs 
b/datafusion/catalog/src/dynamic_file/mod.rs
similarity index 86%
copy from datafusion/catalog/src/lib.rs
copy to datafusion/catalog/src/dynamic_file/mod.rs
index fe76b5dc9c..59142333dd 100644
--- a/datafusion/catalog/src/lib.rs
+++ b/datafusion/catalog/src/dynamic_file/mod.rs
@@ -15,12 +15,4 @@
 // specific language governing permissions and limitations
 // under the License.
 
-mod catalog;
-mod schema;
-mod session;
-mod table;
-
-pub use catalog::*;
-pub use schema::*;
-pub use session::*;
-pub use table::*;
+pub(crate) mod catalog;
diff --git a/datafusion/catalog/src/lib.rs b/datafusion/catalog/src/lib.rs
index fe76b5dc9c..21630f267d 100644
--- a/datafusion/catalog/src/lib.rs
+++ b/datafusion/catalog/src/lib.rs
@@ -16,11 +16,13 @@
 // under the License.
 
 mod catalog;
+mod dynamic_file;
 mod schema;
 mod session;
 mod table;
 
 pub use catalog::*;
+pub use dynamic_file::catalog::*;
 pub use schema::*;
 pub use session::*;
 pub use table::*;
diff --git a/datafusion/catalog/src/session.rs 
b/datafusion/catalog/src/session.rs
index 05d2684ed3..61d9c2d8a7 100644
--- a/datafusion/catalog/src/session.rs
+++ b/datafusion/catalog/src/session.rs
@@ -24,9 +24,10 @@ use datafusion_execution::TaskContext;
 use datafusion_expr::execution_props::ExecutionProps;
 use datafusion_expr::{AggregateUDF, Expr, LogicalPlan, ScalarUDF, WindowUDF};
 use datafusion_physical_plan::{ExecutionPlan, PhysicalExpr};
+use parking_lot::{Mutex, RwLock};
 use std::any::Any;
 use std::collections::HashMap;
-use std::sync::Arc;
+use std::sync::{Arc, Weak};
 
 /// Interface for accessing [`SessionState`] from the catalog.
 ///
@@ -136,3 +137,34 @@ impl From<&dyn Session> for TaskContext {
         )
     }
 }
+type SessionRefLock = Arc<Mutex<Option<Weak<RwLock<dyn Session>>>>>;
+/// The state store that stores the reference of the runtime session state.
+pub struct SessionStore {
+    session: SessionRefLock,
+}
+
+impl SessionStore {
+    /// Create a new [SessionStore]
+    pub fn new() -> Self {
+        Self {
+            session: Arc::new(Mutex::new(None)),
+        }
+    }
+
+    /// Set the session state of the store
+    pub fn with_state(&self, state: Weak<RwLock<dyn Session>>) {
+        let mut lock = self.session.lock();
+        *lock = Some(state);
+    }
+
+    /// Get the current session of the store
+    pub fn get_session(&self) -> Weak<RwLock<dyn Session>> {
+        self.session.lock().clone().unwrap()
+    }
+}
+
+impl Default for SessionStore {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/datafusion/core/src/datasource/dynamic_file.rs 
b/datafusion/core/src/datasource/dynamic_file.rs
new file mode 100644
index 0000000000..a95f3abb93
--- /dev/null
+++ b/datafusion/core/src/datasource/dynamic_file.rs
@@ -0,0 +1,80 @@
+// 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.
+
+//! dynamic_file_schema contains an [`UrlTableFactory`] implementation that
+//! can create a [`ListingTable`] from the given url.
+
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use datafusion_catalog::{SessionStore, UrlTableFactory};
+use datafusion_common::plan_datafusion_err;
+
+use crate::datasource::listing::{ListingTable, ListingTableConfig, 
ListingTableUrl};
+use crate::datasource::TableProvider;
+use crate::error::Result;
+use crate::execution::context::SessionState;
+
+/// [DynamicListTableFactory] is a factory that can create a [ListingTable] 
from the given url.
+#[derive(Default)]
+pub struct DynamicListTableFactory {
+    /// The session store that contains the current session.
+    session_store: SessionStore,
+}
+
+impl DynamicListTableFactory {
+    /// Create a new [DynamicListTableFactory] with the given state store.
+    pub fn new(session_store: SessionStore) -> Self {
+        Self { session_store }
+    }
+
+    /// Get the session store.
+    pub fn session_store(&self) -> &SessionStore {
+        &self.session_store
+    }
+}
+
+#[async_trait]
+impl UrlTableFactory for DynamicListTableFactory {
+    async fn try_new(&self, url: &str) -> Result<Option<Arc<dyn 
TableProvider>>> {
+        let Ok(table_url) = ListingTableUrl::parse(url) else {
+            return Ok(None);
+        };
+
+        let state = &self
+            .session_store()
+            .get_session()
+            .upgrade()
+            .and_then(|session| {
+                session
+                    .read()
+                    .as_any()
+                    .downcast_ref::<SessionState>()
+                    .cloned()
+            })
+            .ok_or_else(|| plan_datafusion_err!("get current SessionStore 
error"))?;
+
+        match ListingTableConfig::new(table_url.clone())
+            .infer(state)
+            .await
+        {
+            Ok(cfg) => ListingTable::try_new(cfg)
+                .map(|table| Some(Arc::new(table) as Arc<dyn TableProvider>)),
+            Err(_) => Ok(None),
+        }
+    }
+}
diff --git a/datafusion/core/src/datasource/mod.rs 
b/datafusion/core/src/datasource/mod.rs
index 529bb799e2..0ed53418fe 100644
--- a/datafusion/core/src/datasource/mod.rs
+++ b/datafusion/core/src/datasource/mod.rs
@@ -22,6 +22,7 @@
 pub mod avro_to_arrow;
 pub mod cte_worktable;
 pub mod default_table_source;
+pub mod dynamic_file;
 pub mod empty;
 pub mod file_format;
 pub mod function;
diff --git a/datafusion/core/src/execution/context/mod.rs 
b/datafusion/core/src/execution/context/mod.rs
index 06dc797ae2..621b214818 100644
--- a/datafusion/core/src/execution/context/mod.rs
+++ b/datafusion/core/src/execution/context/mod.rs
@@ -69,17 +69,18 @@ use datafusion_expr::{
 // backwards compatibility
 pub use crate::execution::session_state::SessionState;
 
+use crate::datasource::dynamic_file::DynamicListTableFactory;
+use crate::execution::session_state::SessionStateBuilder;
 use async_trait::async_trait;
 use chrono::{DateTime, Utc};
-use object_store::ObjectStore;
-use parking_lot::RwLock;
-use url::Url;
-
-use crate::execution::session_state::SessionStateBuilder;
+use datafusion_catalog::{DynamicFileCatalog, SessionStore, UrlTableFactory};
 pub use datafusion_execution::config::SessionConfig;
 pub use datafusion_execution::TaskContext;
 pub use datafusion_expr::execution_props::ExecutionProps;
 use datafusion_optimizer::{AnalyzerRule, OptimizerRule};
+use object_store::ObjectStore;
+use parking_lot::RwLock;
+use url::Url;
 
 mod avro;
 mod csv;
@@ -356,6 +357,53 @@ impl SessionContext {
         }
     }
 
+    /// Enable dynamic file querying for the current session.
+    ///
+    /// This allows queries to directly access arbitrary file names via SQL 
like
+    /// `SELECT * from 'my_file.parquet'`
+    /// so it should only be enabled for systems that such access is not a 
security risk
+    ///
+    /// See [DynamicFileCatalog] for more details
+    ///
+    /// ```
+    /// # use datafusion::prelude::*;
+    /// # use datafusion::{error::Result, assert_batches_eq};
+    /// # #[tokio::main]
+    /// # async fn main() -> Result<()> {
+    /// let ctx = SessionContext::new().enable_url_table();
+    /// let results = ctx
+    ///   .sql("SELECT a, MIN(b) FROM 'tests/data/example.csv' as example 
GROUP BY a LIMIT 100")
+    ///   .await?
+    ///   .collect()
+    ///   .await?;
+    /// assert_batches_eq!(
+    ///  &[
+    ///    "+---+----------------+",
+    ///    "| a | min(example.b) |",
+    ///    "+---+----------------+",
+    ///    "| 1 | 2              |",
+    ///    "+---+----------------+",
+    ///  ],
+    ///  &results
+    /// );
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn enable_url_table(&self) -> Self {
+        let state_ref = self.state();
+        let factory = 
Arc::new(DynamicListTableFactory::new(SessionStore::new()));
+        let catalog_list = Arc::new(DynamicFileCatalog::new(
+            Arc::clone(state_ref.catalog_list()),
+            Arc::clone(&factory) as Arc<dyn UrlTableFactory>,
+        ));
+        let new_state = SessionStateBuilder::new_from_existing(self.state())
+            .with_catalog_list(catalog_list)
+            .build();
+        let ctx = SessionContext::new_with_state(new_state);
+        factory.session_store().with_state(ctx.state_weak_ref());
+        ctx
+    }
+
     /// Creates a new `SessionContext` using the provided [`SessionState`]
     #[deprecated(since = "32.0.0", note = "Use 
SessionContext::new_with_state")]
     pub fn with_state(state: SessionState) -> Self {
@@ -1790,6 +1838,38 @@ mod tests {
         Ok(())
     }
 
+    #[tokio::test]
+    async fn test_dynamic_file_query() -> Result<()> {
+        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+        let path = path.join("tests/tpch-csv/customer.csv");
+        let url = format!("file://{}", path.display());
+        let cfg = SessionConfig::new();
+        let session_state = SessionStateBuilder::new()
+            .with_default_features()
+            .with_config(cfg)
+            .build();
+        let ctx = 
SessionContext::new_with_state(session_state).enable_url_table();
+        let result = plan_and_collect(
+            &ctx,
+            format!("select c_name from '{}' limit 3;", &url).as_str(),
+        )
+        .await?;
+
+        let actual = arrow::util::pretty::pretty_format_batches(&result)
+            .unwrap()
+            .to_string();
+        let expected = r#"+--------------------+
+| c_name             |
++--------------------+
+| Customer#000000002 |
+| Customer#000000003 |
+| Customer#000000004 |
++--------------------+"#;
+        assert_eq!(actual, expected);
+
+        Ok(())
+    }
+
     #[tokio::test]
     async fn custom_query_planner() -> Result<()> {
         let runtime = Arc::new(RuntimeEnv::default());
diff --git a/datafusion/sqllogictest/src/test_context.rs 
b/datafusion/sqllogictest/src/test_context.rs
index 224a0e18ea..ef2fa863e6 100644
--- a/datafusion/sqllogictest/src/test_context.rs
+++ b/datafusion/sqllogictest/src/test_context.rs
@@ -98,6 +98,9 @@ impl TestContext {
                     return None;
                 }
             }
+            "dynamic_file.slt" => {
+                test_ctx.ctx = test_ctx.ctx.enable_url_table();
+            }
             "joins.slt" => {
                 info!("Registering partition table tables");
                 let example_udf = create_example_udf();
diff --git a/datafusion/sqllogictest/test_files/arrow_files.slt 
b/datafusion/sqllogictest/test_files/arrow_files.slt
index 8cf3550fdb..e66ba7477f 100644
--- a/datafusion/sqllogictest/test_files/arrow_files.slt
+++ b/datafusion/sqllogictest/test_files/arrow_files.slt
@@ -43,6 +43,11 @@ SELECT * FROM arrow_simple
 3 baz false
 4 NULL true
 
+# Ensure that local files can not be read by default (a potential security 
issue)
+# (url table is only supported when DynamicFileCatalog is enabled)
+statement error DataFusion error: Error during planning: table 
'datafusion.public.../core/tests/data/example.arrow' not found
+SELECT * FROM '../core/tests/data/example.arrow';
+
 # ARROW partitioned table
 statement ok
 CREATE EXTERNAL TABLE arrow_partitioned (
diff --git a/datafusion/sqllogictest/test_files/csv_files.slt 
b/datafusion/sqllogictest/test_files/csv_files.slt
index d6600e06dc..01d0f4ac39 100644
--- a/datafusion/sqllogictest/test_files/csv_files.slt
+++ b/datafusion/sqllogictest/test_files/csv_files.slt
@@ -50,6 +50,11 @@ id7 value7
 id8 value8
 id9 value9
 
+# Ensure that local files can not be read by default (a potential security 
issue)
+# (url table is only supported when DynamicFileCatalog is enabled)
+statement error DataFusion error: Error during planning: table 
'datafusion.public.../core/tests/data/quote.csv' not found
+select * from '../core/tests/data/quote.csv';
+
 query TT
 select * from csv_with_escape;
 ----
diff --git a/datafusion/sqllogictest/test_files/describe.slt 
b/datafusion/sqllogictest/test_files/describe.slt
index a15c3a109c..077e8e6474 100644
--- a/datafusion/sqllogictest/test_files/describe.slt
+++ b/datafusion/sqllogictest/test_files/describe.slt
@@ -57,7 +57,7 @@ statement ok
 DROP TABLE aggregate_simple;
 
 ##########
-# Describe file (currently we can only describe file in datafusion-cli, fix 
this after issue (#4850) has been done)
+# Describe file (we can only describe file if the default catalog is 
`DynamicFileCatalog`)
 ##########
 
 statement error Error during planning: table 
'datafusion.public.../core/tests/data/aggregate_simple.csv' not found
diff --git a/datafusion/sqllogictest/test_files/dynamic_file.slt 
b/datafusion/sqllogictest/test_files/dynamic_file.slt
new file mode 100644
index 0000000000..e177fd3de2
--- /dev/null
+++ b/datafusion/sqllogictest/test_files/dynamic_file.slt
@@ -0,0 +1,106 @@
+# 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.
+
+#
+# Note: This file runs with a SessionContext that has the `enable_url_table` 
flag set
+#
+# dynamic select arrow file in the folder
+query ITB
+SELECT * FROM '../core/tests/data/partitioned_table_arrow/part=123' ORDER BY 
f0;
+----
+1 foo true
+2 bar false
+
+# dynamic file query doesn't support partitioned table
+statement error DataFusion error: Error during planning: table 
'datafusion.public.../core/tests/data/partitioned_table_arrow' not found
+SELECT * FROM '../core/tests/data/partitioned_table_arrow' ORDER BY f0;
+
+# read avro file
+query IT
+SELECT id, CAST(string_col AS varchar) FROM 
'../../testing/data/avro/alltypes_plain.avro'
+----
+4 0
+5 1
+6 0
+7 1
+2 0
+3 1
+0 0
+1 1
+
+# dynamic query snappy avro file
+query IT
+SELECT id, CAST(string_col AS varchar) FROM 
'../../testing/data/avro/alltypes_plain.snappy.avro'
+----
+4 0
+5 1
+6 0
+7 1
+2 0
+3 1
+0 0
+1 1
+
+# query the csv file dynamically with the config of current session
+query TT
+select * from '../core/tests/data/quote.csv';
+----
+~id0~ ~value0~
+~id1~ ~value1~
+~id2~ ~value2~
+~id3~ ~value3~
+~id4~ ~value4~
+~id5~ ~value5~
+~id6~ ~value6~
+~id7~ ~value7~
+~id8~ ~value8~
+~id9~ ~value9~
+
+query TTT
+DESCRIBE '../core/tests/data/aggregate_simple.csv';
+----
+c1 Float64 YES
+c2 Float64 YES
+c3 Boolean YES
+
+query IR rowsort
+SELECT a, b FROM '../core/tests/data/2.json'
+----
+-10 -3.5
+1 -3.5
+1 0.6
+1 0.6
+1 2
+1 2
+1 2
+1 2
+100000000000000 0.6
+2 0.6
+5 -3.5
+7 -3.5
+
+query IT
+SELECT id, CAST(string_col AS varchar) FROM 
'../../parquet-testing/data/alltypes_plain.parquet';
+----
+4 0
+5 1
+6 0
+7 1
+2 0
+3 1
+0 0
+1 1
diff --git a/datafusion/sqllogictest/test_files/json.slt 
b/datafusion/sqllogictest/test_files/json.slt
index 0b9508310b..0903c24276 100644
--- a/datafusion/sqllogictest/test_files/json.slt
+++ b/datafusion/sqllogictest/test_files/json.slt
@@ -45,6 +45,11 @@ SELECT a, b FROM json_test
 5 -3.5
 7 -3.5
 
+# Ensure that local files can not be read by default (a potential security 
issue)
+# (url table is only supported when DynamicFileCatalog is enabled)
+statement error DataFusion error: Error during planning: table 
'datafusion.public.../core/tests/data/2.json' not found
+SELECT a, b FROM '../core/tests/data/2.json'
+
 query TT
 EXPLAIN SELECT count(*) from json_test
 ----
diff --git a/datafusion/sqllogictest/test_files/parquet.slt 
b/datafusion/sqllogictest/test_files/parquet.slt
index 9a7b085312..f8b163adc7 100644
--- a/datafusion/sqllogictest/test_files/parquet.slt
+++ b/datafusion/sqllogictest/test_files/parquet.slt
@@ -202,6 +202,11 @@ SELECT id, CAST(string_col AS varchar) FROM alltypes_plain
 0 0
 1 1
 
+# Ensure that local files can not be read by default (a potential security 
issue)
+# (url table is only supported when DynamicFileCatalog is enabled)
+statement error DataFusion error: Error during planning: table 
'datafusion.public.../../parquet-testing/data/alltypes_plain.parquet' not found
+SELECT id, CAST(string_col AS varchar) FROM 
'../../parquet-testing/data/alltypes_plain.parquet';
+
 # Clean up
 statement ok
 DROP TABLE alltypes_plain;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to