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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-3102-ba849a6ed3030c237e46be3e1d39e67cebf035a7
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git

commit b3470c8463c281c6e00ea9eee402881bb6c6d1fd
Author: blackmwk <[email protected]>
AuthorDate: Thu Oct 15 18:12:58 2026 +0000

    refactor(glue): derive catalog configuration from properties (#3102)
    
    * refactor(glue): derive catalog configuration from properties
    
    Use the shared property derive for Glue catalog settings while preserving 
warehouse validation and forwarding AWS and storage properties.
    
    Tracks 
[apache/iceberg-rust#3096](https://github.com/apache/iceberg-rust/issues/3096).
    
    Generated-by: Codex
    
    * refactor(glue): simplify catalog builder and tests
    
    Derive Default for GlueCatalogBuilder, use the conventional tests module 
name, and remove the FileIO forwarding test requested in review.
    
    Generated-by: Codex
---
 Cargo.lock                         |   1 +
 crates/catalog/glue/Cargo.toml     |   1 +
 crates/catalog/glue/public-api.txt |   2 +-
 crates/catalog/glue/src/catalog.rs | 187 ++++++++++++++++++++-----------------
 4 files changed, 106 insertions(+), 85 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index b98bd2da4..3bb94e339 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2779,6 +2779,7 @@ dependencies = [
  "aws-config",
  "aws-sdk-glue",
  "iceberg",
+ "iceberg-property-macro",
  "iceberg-storage-opendal",
  "iceberg_test_utils",
  "serde_json",
diff --git a/crates/catalog/glue/Cargo.toml b/crates/catalog/glue/Cargo.toml
index decb78134..ac912bd40 100644
--- a/crates/catalog/glue/Cargo.toml
+++ b/crates/catalog/glue/Cargo.toml
@@ -35,6 +35,7 @@ async-trait = { workspace = true }
 aws-config = { workspace = true }
 aws-sdk-glue = { workspace = true }
 iceberg = { workspace = true }
+iceberg-property-macro = { workspace = true }
 iceberg-storage-opendal = { workspace = true, features = ["opendal-s3"] }
 serde_json = { workspace = true }
 tokio = { workspace = true }
diff --git a/crates/catalog/glue/public-api.txt 
b/crates/catalog/glue/public-api.txt
index 902daf635..eafce2054 100644
--- a/crates/catalog/glue/public-api.txt
+++ b/crates/catalog/glue/public-api.txt
@@ -23,7 +23,7 @@ pub fn 
iceberg_catalog_glue::GlueCatalog::update_namespace<'life0, 'life1, 'asyn
 pub fn iceberg_catalog_glue::GlueCatalog::update_table<'life0, 
'async_trait>(&'life0 self, commit: iceberg::catalog::TableCommit) -> 
core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = 
iceberg::error::Result<iceberg::table::Table>> + core::marker::Send + 
'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
 pub struct iceberg_catalog_glue::GlueCatalogBuilder
 impl core::default::Default for iceberg_catalog_glue::GlueCatalogBuilder
-pub fn iceberg_catalog_glue::GlueCatalogBuilder::default() -> Self
+pub fn iceberg_catalog_glue::GlueCatalogBuilder::default() -> 
iceberg_catalog_glue::GlueCatalogBuilder
 impl core::fmt::Debug for iceberg_catalog_glue::GlueCatalogBuilder
 pub fn iceberg_catalog_glue::GlueCatalogBuilder::fmt(&self, f: &mut 
core::fmt::Formatter<'_>) -> core::fmt::Result
 impl iceberg::catalog::CatalogBuilder for 
iceberg_catalog_glue::GlueCatalogBuilder
diff --git a/crates/catalog/glue/src/catalog.rs 
b/crates/catalog/glue/src/catalog.rs
index 721fbb548..06e8b8400 100644
--- a/crates/catalog/glue/src/catalog.rs
+++ b/crates/catalog/glue/src/catalog.rs
@@ -36,6 +36,7 @@ use iceberg::{
     Catalog, CatalogBuilder, Error, ErrorKind, MetadataLocation, Namespace, 
NamespaceIdent, Result,
     Runtime, TableCommit, TableCreation, TableIdent,
 };
+use iceberg_property_macro::Properties;
 use iceberg_storage_opendal::OpenDalStorageFactory;
 
 use crate::error::{from_aws_build_error, from_aws_sdk_error};
@@ -55,31 +56,13 @@ pub const GLUE_CATALOG_PROP_CATALOG_ID: &str = "catalog_id";
 pub const GLUE_CATALOG_PROP_WAREHOUSE: &str = "warehouse";
 
 /// Builder for [`GlueCatalog`].
-#[derive(Debug)]
+#[derive(Debug, Default)]
 pub struct GlueCatalogBuilder {
-    config: GlueCatalogConfig,
     storage_factory: Option<Arc<dyn StorageFactory>>,
     kms_client_factory: Option<Arc<dyn KmsClientFactory>>,
     runtime: Option<Runtime>,
 }
 
-impl Default for GlueCatalogBuilder {
-    fn default() -> Self {
-        Self {
-            config: GlueCatalogConfig {
-                name: None,
-                uri: None,
-                catalog_id: None,
-                warehouse: "".to_string(),
-                props: HashMap::new(),
-            },
-            storage_factory: None,
-            kms_client_factory: None,
-            runtime: None,
-        }
-    }
-}
-
 impl CatalogBuilder for GlueCatalogBuilder {
     type C = GlueCatalog;
 
@@ -99,45 +82,15 @@ impl CatalogBuilder for GlueCatalogBuilder {
     }
 
     fn load(
-        mut self,
+        self,
         name: impl Into<String>,
         props: HashMap<String, String>,
     ) -> impl Future<Output = Result<Self::C>> + Send {
-        self.config.name = Some(name.into());
-
-        if props.contains_key(GLUE_CATALOG_PROP_URI) {
-            self.config.uri = props.get(GLUE_CATALOG_PROP_URI).cloned()
-        }
-
-        if props.contains_key(GLUE_CATALOG_PROP_CATALOG_ID) {
-            self.config.catalog_id = 
props.get(GLUE_CATALOG_PROP_CATALOG_ID).cloned()
-        }
-
-        if props.contains_key(GLUE_CATALOG_PROP_WAREHOUSE) {
-            self.config.warehouse = props
-                .get(GLUE_CATALOG_PROP_WAREHOUSE)
-                .cloned()
-                .unwrap_or_default();
-        }
-
-        // Collect other remaining properties
-        self.config.props = props
-            .into_iter()
-            .filter(|(k, _)| {
-                k != GLUE_CATALOG_PROP_URI
-                    && k != GLUE_CATALOG_PROP_CATALOG_ID
-                    && k != GLUE_CATALOG_PROP_WAREHOUSE
-            })
-            .collect();
+        let name = name.into();
 
         async move {
-            if self.config.name.is_none() {
-                return Err(Error::new(
-                    ErrorKind::DataInvalid,
-                    "Catalog name is required",
-                ));
-            }
-            if self.config.warehouse.is_empty() {
+            let catalog_properties = 
GlueCatalogProperties::from_properties(&props)?;
+            if catalog_properties.warehouse.is_empty() {
                 return Err(Error::new(
                     ErrorKind::DataInvalid,
                     "Catalog warehouse is required",
@@ -149,29 +102,38 @@ impl CatalogBuilder for GlueCatalogBuilder {
                 None => Runtime::try_current()?,
             };
             let kms_client = match self.kms_client_factory {
-                Some(factory) => 
Some(factory.create_kms_client(&self.config.props).await?),
+                Some(factory) => 
Some(factory.create_kms_client(&props).await?),
                 None => None,
             };
-            GlueCatalog::new(self.config, self.storage_factory, runtime, 
kms_client).await
+            GlueCatalog::new(
+                name,
+                catalog_properties,
+                props,
+                self.storage_factory,
+                runtime,
+                kms_client,
+            )
+            .await
         }
     }
 }
 
-#[derive(Debug)]
-/// Glue Catalog configuration
-pub(crate) struct GlueCatalogConfig {
-    name: Option<String>,
+#[derive(Debug, Properties)]
+pub(crate) struct GlueCatalogProperties {
+    #[property(key = GLUE_CATALOG_PROP_URI, default = None)]
     uri: Option<String>,
+    #[property(key = GLUE_CATALOG_PROP_CATALOG_ID, default = None)]
     catalog_id: Option<String>,
+    #[property(key = GLUE_CATALOG_PROP_WAREHOUSE, default = "")]
     warehouse: String,
-    props: HashMap<String, String>,
 }
 
 struct GlueClient(aws_sdk_glue::Client);
 
 /// Glue Catalog
 pub struct GlueCatalog {
-    config: GlueCatalogConfig,
+    name: String,
+    properties: GlueCatalogProperties,
     client: GlueClient,
     file_io: FileIO,
     runtime: Runtime,
@@ -181,7 +143,8 @@ pub struct GlueCatalog {
 impl Debug for GlueCatalog {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("GlueCatalog")
-            .field("config", &self.config)
+            .field("name", &self.name)
+            .field("properties", &self.properties)
             .finish_non_exhaustive()
     }
 }
@@ -189,13 +152,15 @@ impl Debug for GlueCatalog {
 impl GlueCatalog {
     /// Create a new glue catalog
     async fn new(
-        config: GlueCatalogConfig,
+        name: String,
+        properties: GlueCatalogProperties,
+        props: HashMap<String, String>,
         storage_factory: Option<Arc<dyn StorageFactory>>,
         runtime: Runtime,
         kms_client: Option<Arc<dyn KeyManagementClient>>,
     ) -> Result<Self> {
-        let sdk_config = create_sdk_config(&config.props, 
config.uri.as_ref()).await;
-        let mut file_io_props = config.props.clone();
+        let sdk_config = create_sdk_config(&props, 
properties.uri.as_ref()).await;
+        let mut file_io_props = props;
         if !file_io_props.contains_key(S3_ACCESS_KEY_ID)
             && let Some(access_key_id) = file_io_props.get(AWS_ACCESS_KEY_ID)
         {
@@ -220,7 +185,7 @@ impl GlueCatalog {
             file_io_props.insert(S3_SESSION_TOKEN.to_string(), 
session_token.to_string());
         }
         if !file_io_props.contains_key(S3_ENDPOINT)
-            && let Some(aws_endpoint) = config.uri.as_ref()
+            && let Some(aws_endpoint) = properties.uri.as_ref()
         {
             file_io_props.insert(S3_ENDPOINT.to_string(), 
aws_endpoint.to_string());
         }
@@ -238,7 +203,8 @@ impl GlueCatalog {
             .build();
 
         Ok(GlueCatalog {
-            config,
+            name,
+            properties,
             client: GlueClient(client),
             file_io,
             runtime,
@@ -275,7 +241,7 @@ impl GlueCatalog {
             .get_table()
             .database_name(&db_name)
             .name(table_name);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         let glue_table_output = 
builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -333,7 +299,7 @@ impl Catalog for GlueCatalog {
                 Some(token) => self.client.0.get_databases().next_token(token),
                 None => self.client.0.get_databases(),
             };
-            let builder = with_catalog_id!(builder, self.config);
+            let builder = with_catalog_id!(builder, self.properties);
             let resp = builder.send().await.map_err(from_aws_sdk_error)?;
 
             let dbs: Vec<NamespaceIdent> = resp
@@ -381,7 +347,7 @@ impl Catalog for GlueCatalog {
         let db_input = convert_to_database(namespace, &properties)?;
 
         let builder = self.client.0.create_database().database_input(db_input);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -402,7 +368,7 @@ impl Catalog for GlueCatalog {
         let db_name = validate_namespace(namespace)?;
 
         let builder = self.client.0.get_database().name(&db_name);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         let resp = builder.send().await.map_err(|err| {
             if err
@@ -446,7 +412,7 @@ impl Catalog for GlueCatalog {
         let db_name = validate_namespace(namespace)?;
 
         let builder = self.client.0.get_database().name(&db_name);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         let resp = builder.send().await;
 
@@ -496,7 +462,7 @@ impl Catalog for GlueCatalog {
             .update_database()
             .name(&db_name)
             .database_input(db_input);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -533,7 +499,7 @@ impl Catalog for GlueCatalog {
             .get_tables()
             .database_name(&db_name)
             .max_results(1);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
         let resp = builder.send().await.map_err(from_aws_sdk_error)?;
 
         if !resp.table_list().is_empty() {
@@ -544,7 +510,7 @@ impl Catalog for GlueCatalog {
         }
 
         let builder = self.client.0.delete_database().name(db_name);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -579,7 +545,7 @@ impl Catalog for GlueCatalog {
                     .next_token(token),
                 None => self.client.0.get_tables().database_name(&db_name),
             };
-            let builder = with_catalog_id!(builder, self.config);
+            let builder = with_catalog_id!(builder, self.properties);
             let resp = builder.send().await.map_err(from_aws_sdk_error)?;
 
             let tables: Vec<_> = resp
@@ -623,7 +589,7 @@ impl Catalog for GlueCatalog {
         if creation.location.is_none() {
             let ns = self.get_namespace(namespace).await?;
             let location =
-                get_default_table_location(&ns, &db_name, &table_name, 
&self.config.warehouse);
+                get_default_table_location(&ns, &db_name, &table_name, 
&self.properties.warehouse);
             creation.location = Some(location);
         }
         let metadata = TableMetadataBuilder::from_table_creation(creation)?
@@ -648,7 +614,7 @@ impl Catalog for GlueCatalog {
             .create_table()
             .database_name(&db_name)
             .table_input(glue_table);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -701,7 +667,7 @@ impl Catalog for GlueCatalog {
             .delete_table()
             .database_name(&db_name)
             .name(table_name);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -731,7 +697,7 @@ impl Catalog for GlueCatalog {
             .get_table()
             .database_name(&db_name)
             .name(table_name);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         let resp = builder.send().await;
 
@@ -769,7 +735,7 @@ impl Catalog for GlueCatalog {
             .get_table()
             .database_name(&src_db_name)
             .name(src_table_name);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         let glue_table_output = 
builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -796,7 +762,7 @@ impl Catalog for GlueCatalog {
                     .create_table()
                     .database_name(&dest_db_name)
                     .table_input(rename_table_input);
-                let builder = with_catalog_id!(builder, self.config);
+                let builder = with_catalog_id!(builder, self.properties);
 
                 builder.send().await.map_err(from_aws_sdk_error)?;
 
@@ -864,7 +830,7 @@ impl Catalog for GlueCatalog {
             .create_table()
             .database_name(&db_name)
             .table_input(table_input);
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
 
         builder.send().await.map_err(|e| {
             let error = e.into_service_error();
@@ -935,7 +901,7 @@ impl Catalog for GlueCatalog {
             builder = builder.version_id(version_id);
         }
 
-        let builder = with_catalog_id!(builder, self.config);
+        let builder = with_catalog_id!(builder, self.properties);
         let _ = builder.send().await.map_err(|e| {
             let error = e.into_service_error();
             match error {
@@ -959,3 +925,56 @@ impl Catalog for GlueCatalog {
         Ok(staged_table)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_catalog_properties() {
+        let properties = 
GlueCatalogProperties::from_properties(&HashMap::from([
+            (
+                GLUE_CATALOG_PROP_URI.to_string(),
+                "http://localhost".to_string(),
+            ),
+            (
+                GLUE_CATALOG_PROP_CATALOG_ID.to_string(),
+                "catalog".to_string(),
+            ),
+            (
+                GLUE_CATALOG_PROP_WAREHOUSE.to_string(),
+                "s3://warehouse".to_string(),
+            ),
+        ]))
+        .unwrap();
+
+        assert_eq!(properties.uri.as_deref(), Some("http://localhost";));
+        assert_eq!(properties.catalog_id.as_deref(), Some("catalog"));
+        assert_eq!(properties.warehouse, "s3://warehouse");
+    }
+
+    #[test]
+    fn test_catalog_properties_defaults() {
+        let properties = 
GlueCatalogProperties::from_properties(&HashMap::new()).unwrap();
+
+        assert_eq!(properties.uri, None);
+        assert_eq!(properties.catalog_id, None);
+        assert_eq!(properties.warehouse, "");
+    }
+
+    #[tokio::test]
+    async fn test_load_rejects_missing_or_empty_warehouse() {
+        for props in [
+            HashMap::new(),
+            HashMap::from([(GLUE_CATALOG_PROP_WAREHOUSE.to_string(), 
String::new())]),
+        ] {
+            let error = GlueCatalogBuilder::default()
+                .load("glue", props)
+                .await
+                .unwrap_err();
+
+            assert_eq!(error.kind(), ErrorKind::DataInvalid);
+            assert_eq!(error.message(), "Catalog warehouse is required");
+        }
+    }
+}

Reply via email to