-
Notifications
You must be signed in to change notification settings - Fork 548
Datafusion session integration #3000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DerGut
wants to merge
12
commits into
apache:main
Choose a base branch
from
DerGut:datafusion-session-integration
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
1a8714d
refactor(datafusion): rename catalog provider modules
DerGut a824a15
Introduce the SessionContextResolver trait
DerGut 63ee826
Introduce the CatalogAccess abstraction
DerGut 612a2bd
Use CatalogAccess in providers and forward session were available
DerGut f543270
Add new constructor for session-aware providers
DerGut 373e365
Match IcebergCommitExec::new's visibility
DerGut 9abe204
Store fallback_context and reuse it
DerGut 394baf0
SessionCatalog to Catalog adapter
DerGut c361fe5
Keep SessionBoundCatalog private
DerGut 7ed1307
Factor out catalog access with shared session
DerGut 1bfcded
Add example
DerGut 39b7808
Add tests
DerGut File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,303 @@ | ||
| // 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. | ||
|
|
||
| //! Connects a session-aware Iceberg catalog to DataFusion. | ||
| //! | ||
| //! Run with: | ||
| //! | ||
| //! ```text | ||
| //! cargo run -p iceberg-examples --example datafusion-session-catalog | ||
| //! ``` | ||
| //! | ||
| //! The adapter at the bottom only makes the example self-contained. Applications | ||
| //! should pass their own `SessionCatalog` implementation to the provider. | ||
|
|
||
| use std::collections::HashMap; | ||
| use std::sync::Arc; | ||
|
|
||
| use async_trait::async_trait; | ||
| use datafusion::catalog::Session as DataFusionSession; | ||
| use datafusion::error::{DataFusionError, Result as DataFusionResult}; | ||
| use datafusion::prelude::{SessionConfig, SessionContext as DataFusionSessionContext}; | ||
| use iceberg::memory::{MEMORY_CATALOG_WAREHOUSE, MemoryCatalog, MemoryCatalogBuilder}; | ||
| use iceberg::spec::{NestedField, PrimitiveType, Schema, Type}; | ||
| use iceberg::table::Table; | ||
| use iceberg::{ | ||
| Catalog, CatalogBuilder, Namespace, NamespaceIdent, Result, SessionCatalog, | ||
| SessionContext as IcebergSessionContext, TableCommit, TableCreation, TableIdent, | ||
| }; | ||
| use iceberg_datafusion::{IcebergCatalogProvider, SessionContextResolver}; | ||
|
|
||
| /// User metadata stored as an application-specific DataFusion extension. | ||
| #[derive(Debug)] | ||
| struct UserContext { | ||
| name: String, | ||
| } | ||
|
|
||
| /// Maps the application's DataFusion user context to an Iceberg session. | ||
| #[derive(Debug)] | ||
| struct UserSessionContextResolver; | ||
|
|
||
| impl SessionContextResolver for UserSessionContextResolver { | ||
| fn resolve(&self, session: &dyn DataFusionSession) -> DataFusionResult<IcebergSessionContext> { | ||
| let user = session | ||
| .config() | ||
| .get_extension::<UserContext>() | ||
| .ok_or_else(|| { | ||
| DataFusionError::Configuration( | ||
| "the DataFusion session has no UserContext extension".to_string(), | ||
| ) | ||
| })?; | ||
|
|
||
| Ok(IcebergSessionContext::builder() | ||
| // Reusing the DataFusion session ID gives the catalog a stable key | ||
| // for session-scoped caches. | ||
| .session_id(session.session_id().to_string()) | ||
| .identity(user.name.to_string()) | ||
| .build()) | ||
| } | ||
| } | ||
|
|
||
| #[tokio::main] | ||
| async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { | ||
| let catalog = | ||
| init_catalog_with_table(TableIdent::from_strs(["datafusion", "example"])?).await?; | ||
|
|
||
| let session_catalog = Arc::new(ExampleSessionCatalog::new(catalog)); | ||
| // Provider construction discovers namespaces and tables with one stable, | ||
| // anonymous fallback context. Session-dependent catalogs must make that | ||
| // discovery set available to the fallback context. | ||
| let provider = IcebergCatalogProvider::try_new_with_session_catalog( | ||
| session_catalog, | ||
| Arc::new(UserSessionContextResolver), | ||
| ) | ||
| .await?; | ||
|
|
||
| let config = SessionConfig::new().with_extension(Arc::new(UserContext { | ||
| name: "user123".to_string(), | ||
| })); | ||
| let datafusion = DataFusionSessionContext::new_with_config(config); | ||
| datafusion.register_catalog("iceberg", Arc::new(provider)); | ||
|
|
||
| // Planning the scan invokes the resolver and forwards its Iceberg context | ||
| // to the session catalog's `load_table` operation. | ||
| datafusion | ||
| .sql("SELECT COUNT(*) AS event_count FROM iceberg.datafusion.example") | ||
| .await? | ||
| .show() | ||
| .await?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// A small session-aware wrapper around the in-memory catalog used by this | ||
| /// standalone example. | ||
| /// | ||
| /// Real session catalogs can use the context for authorization, credentials, | ||
| /// configuration, and caching. This wrapper logs it, then delegates catalog | ||
| /// operations so the example needs no external service. | ||
| #[derive(Debug)] | ||
| struct ExampleSessionCatalog { | ||
| inner: MemoryCatalog, | ||
| } | ||
|
|
||
| impl ExampleSessionCatalog { | ||
| fn new(inner: MemoryCatalog) -> Self { | ||
| Self { inner } | ||
| } | ||
|
|
||
| fn log_context(context: &IcebergSessionContext, operation: &str) { | ||
| let identity = context.identity().unwrap_or("<anonymous>"); | ||
| println!( | ||
| "{operation}: session_id={}, identity={identity}", | ||
| context.session_id() | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl SessionCatalog for ExampleSessionCatalog { | ||
| async fn list_namespaces( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| parent: Option<&NamespaceIdent>, | ||
| ) -> Result<Vec<NamespaceIdent>> { | ||
| Self::log_context(context, "list_namespaces"); | ||
| self.inner.list_namespaces(parent).await | ||
| } | ||
|
|
||
| async fn create_namespace( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| namespace: &NamespaceIdent, | ||
| properties: HashMap<String, String>, | ||
| ) -> Result<Namespace> { | ||
| Self::log_context(context, "create_namespace"); | ||
| self.inner.create_namespace(namespace, properties).await | ||
| } | ||
|
|
||
| async fn get_namespace( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| namespace: &NamespaceIdent, | ||
| ) -> Result<Namespace> { | ||
| Self::log_context(context, "get_namespace"); | ||
| self.inner.get_namespace(namespace).await | ||
| } | ||
|
|
||
| async fn namespace_exists( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| namespace: &NamespaceIdent, | ||
| ) -> Result<bool> { | ||
| Self::log_context(context, "namespace_exists"); | ||
| self.inner.namespace_exists(namespace).await | ||
| } | ||
|
|
||
| async fn update_namespace( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| namespace: &NamespaceIdent, | ||
| properties: HashMap<String, String>, | ||
| ) -> Result<()> { | ||
| Self::log_context(context, "update_namespace"); | ||
| self.inner.update_namespace(namespace, properties).await | ||
| } | ||
|
|
||
| async fn drop_namespace( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| namespace: &NamespaceIdent, | ||
| ) -> Result<()> { | ||
| Self::log_context(context, "drop_namespace"); | ||
| self.inner.drop_namespace(namespace).await | ||
| } | ||
|
|
||
| async fn list_tables( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| namespace: &NamespaceIdent, | ||
| ) -> Result<Vec<TableIdent>> { | ||
| Self::log_context(context, "list_tables"); | ||
| self.inner.list_tables(namespace).await | ||
| } | ||
|
|
||
| async fn create_table( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| namespace: &NamespaceIdent, | ||
| creation: TableCreation, | ||
| ) -> Result<Table> { | ||
| Self::log_context(context, "create_table"); | ||
| self.inner.create_table(namespace, creation).await | ||
| } | ||
|
|
||
| async fn load_table( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| table: &TableIdent, | ||
| ) -> Result<Table> { | ||
| Self::log_context(context, "load_table"); | ||
| self.inner.load_table(table).await | ||
| } | ||
|
|
||
| async fn drop_table(&self, context: &IcebergSessionContext, table: &TableIdent) -> Result<()> { | ||
| Self::log_context(context, "drop_table"); | ||
| self.inner.drop_table(table).await | ||
| } | ||
|
|
||
| async fn purge_table(&self, context: &IcebergSessionContext, table: &TableIdent) -> Result<()> { | ||
| Self::log_context(context, "purge_table"); | ||
| self.inner.purge_table(table).await | ||
| } | ||
|
|
||
| async fn table_exists( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| table: &TableIdent, | ||
| ) -> Result<bool> { | ||
| Self::log_context(context, "table_exists"); | ||
| self.inner.table_exists(table).await | ||
| } | ||
|
|
||
| async fn rename_table( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| src: &TableIdent, | ||
| dest: &TableIdent, | ||
| ) -> Result<()> { | ||
| Self::log_context(context, "rename_table"); | ||
| self.inner.rename_table(src, dest).await | ||
| } | ||
|
|
||
| async fn register_table( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| table: &TableIdent, | ||
| metadata_location: String, | ||
| ) -> Result<Table> { | ||
| Self::log_context(context, "register_table"); | ||
| self.inner.register_table(table, metadata_location).await | ||
| } | ||
|
|
||
| async fn update_table( | ||
| &self, | ||
| context: &IcebergSessionContext, | ||
| commit: TableCommit, | ||
| ) -> Result<Table> { | ||
| Self::log_context(context, "update_table"); | ||
| self.inner.update_table(commit).await | ||
| } | ||
| } | ||
|
|
||
| async fn init_catalog_with_table(table_ident: TableIdent) -> Result<MemoryCatalog> { | ||
| let catalog = MemoryCatalogBuilder::default() | ||
| .load( | ||
| "memory", | ||
| HashMap::from([( | ||
| MEMORY_CATALOG_WAREHOUSE.to_string(), | ||
| "memory://session-catalog-example".to_string(), | ||
| )]), | ||
| ) | ||
| .await?; | ||
|
|
||
| catalog | ||
| .create_namespace(table_ident.namespace(), HashMap::new()) | ||
| .await?; | ||
| catalog | ||
| .create_table( | ||
| table_ident.namespace(), | ||
| TableCreation::builder() | ||
| .name(table_ident.name().to_string()) | ||
| .schema( | ||
| Schema::builder() | ||
| .with_fields(vec![ | ||
| NestedField::required( | ||
| 1, | ||
| "event_id", | ||
| Type::Primitive(PrimitiveType::Long), | ||
| ) | ||
| .into(), | ||
| ]) | ||
| .build()?, | ||
| ) | ||
| .build(), | ||
| ) | ||
| .await?; | ||
|
|
||
| Ok(catalog) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's an opportunity of reducing a lot of the convolution by using DataFusion
ConfigOptionextensions. For example:With this, we could automatically map
IcebergOptionsto the relevant fields ofIcebergSessionContextinside this project, without exposing this detail to users.From a public API standpoint, this crate would just offer this
IcebergOptionsas a native DataFusionConfigOptionsimplementation, and under the hood this can be wired up internally to anIcebergSessionContext.This is the most DataFusion native way of threading custom config across the callstack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would indeed help the complexity of the integration implementation a lot. But this also means that users now have to set Iceberg-specific extension options in addition to their possibly already existing custom extension options, right?
Definitely good to know that this is the DataFusion-way of doing things, this is exactly the context I was missing, thanks! 🙇
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have the same question, I thought the reason why we don't want to have a default context resolving logic is that configs are highly customized by users and cannot be easily mapped to a default set of iceberg options: https://github.com/apache/iceberg-rust/pull/3000/changes/BASE..39b780895f494e61164c59d92bbe1ced19b6f83d#r3799689323