laskoviymishka commented on code in PR #3105:
URL: https://github.com/apache/iceberg-rust/pull/3105#discussion_r3959309391


##########
crates/catalog/sql/src/catalog.rs:
##########
@@ -401,27 +392,16 @@ impl SqlCatalog {
                 "StorageFactory must be provided for SqlCatalog. Use 
`with_storage_factory` to configure it.",
             )
         })?;
-        // Forward catalog props so storage-backend keys reach the FileIO.
-        // Unrecognized keys are ignored by backends.
-        let fileio = FileIOBuilder::new(factory)
-            .with_props(config.props.clone())
-            .build();
-
         install_default_drivers();
-        let max_connections =
-            parse_pool_property(&config.props, "pool.max-connections", 
MAX_CONNECTIONS)?;
-        let idle_timeout = parse_pool_property(&config.props, 
"pool.idle-timeout", IDLE_TIMEOUT)?;
-        let test_before_acquire = parse_pool_property(
-            &config.props,
-            "pool.test-before-acquire",
-            TEST_BEFORE_ACQUIRE,
-        )?;
+        // Forward the complete property map so storage-backend keys reach 
FileIO.
+        // Unrecognized keys are ignored by backends.
+        let fileio = FileIOBuilder::new(factory).with_props(props).build();

Review Comment:
   The old `load()` stripped `uri`, `warehouse`, the bind-style keys and 
`sql.schema-version` out of the map (via those `remove()` calls) before this 
point, so they never reached FileIO or the KMS factory. Now the whole 
`merged_props` gets forwarded here and to `create_kms_client(&merged_props)` up 
in `load()`.
   
   That's a real behavior change: the DB connection string — which can carry a 
password, e.g. `postgres://user:pass@host/db` — now lands in every 
storage-backend config and in the KMS config map. A KMS factory that rejects 
unknown keys would start failing where it didn't before, and we lose the 
hygiene of not leaking the DB URI into the storage layer. Java's `JdbcCatalog` 
and PyIceberg both consume `uri`/`warehouse` internally and don't pass them 
onward.
   
   I'd add a set-difference step that filters the known catalog-internal keys 
before forwarding (pool.* can stay — those were already forwarded). And the 
comment just above ("Unrecognized keys are ignored by backends") is an 
assumption, not a guarantee — `test_storage_props_propagate_to_file_io` only 
asserts the storage keys are present, never that `uri`/`warehouse` are absent, 
so this slipped past the suite. Worth pinning that with an 
`assert_eq!(props.get("uri"), None)` while we're here. wdyt?



##########
crates/catalog/sql/src/catalog.rs:
##########
@@ -196,111 +177,120 @@ impl CatalogBuilder for SqlCatalogBuilder {
     }
 
     fn load(
-        mut self,
+        self,
         name: impl Into<String>,
         props: HashMap<String, String>,
     ) -> impl Future<Output = Result<Self::C>> + Send {
-        for (k, v) in props {
-            self.config.props.insert(k, v);
-        }
-
-        if let Some(uri) = self.config.props.remove(SQL_CATALOG_PROP_URI) {
-            self.config.uri = uri;
-        }
-        if let Some(warehouse_location) = 
self.config.props.remove(SQL_CATALOG_PROP_WAREHOUSE) {
-            self.config.warehouse_location = warehouse_location;
-        }
-
         let name = name.into();
 
-        let mut valid_sql_bind_style = true;
-
-        // Accept the preferred `sql.bind-style` key, falling back to the 
legacy `sql_bind_style`.
-        let sql_bind_style = self
-            .config
-            .props
-            .remove(SQL_CATALOG_PROP_BIND_STYLE)
-            .or_else(|| 
self.config.props.remove(SQL_CATALOG_PROP_BIND_STYLE_LEGACY));
-
-        // Validate the SQL bind style
-        if let Some(sql_bind_style) = sql_bind_style {
-            if let Ok(sql_bind_style) = 
SqlBindStyle::from_str(&sql_bind_style) {
-                self.config.sql_bind_style = sql_bind_style;
-            } else {
-                valid_sql_bind_style = false;
+        async move {
+            if name.trim().is_empty() {
+                return Err(Error::new(
+                    ErrorKind::DataInvalid,
+                    "Catalog name cannot be empty",
+                ));
             }
-        }
 
-        // Parse the requested schema version up front so invalid values fail 
fast rather than
-        // silently falling back to V0.
-        let mut valid_schema_version = true;
-        if let Some(schema_version) = 
self.config.props.remove(SQL_CATALOG_PROP_SCHEMA_VERSION) {
-            match SchemaVersion::from_str(&schema_version) {
-                Ok(schema_version) => self.config.schema_version = 
Some(schema_version),
-                Err(_) => valid_schema_version = false,
-            }
-        }
+            let mut merged_props = self.props;
+            merged_props.extend(props);
+            let catalog_properties = 
SqlCatalogProperties::from_properties(&merged_props)?;
 
-        let valid_name = !name.trim().is_empty();
+            let runtime = match self.runtime {
+                Some(rt) => rt,
+                None => Runtime::try_current()?,
+            };
+            let kms_client = match self.kms_client_factory {
+                Some(factory) => 
Some(factory.create_kms_client(&merged_props).await?),
+                None => None,
+            };
+            SqlCatalog::new(
+                name,
+                catalog_properties,
+                merged_props,
+                self.storage_factory,
+                runtime,
+                kms_client,
+            )
+            .await
+        }
+    }
+}
 
-        async move {
-            if !valid_name {
-                Err(Error::new(
-                    ErrorKind::DataInvalid,
-                    "Catalog name cannot be empty",
-                ))
-            } else if !valid_sql_bind_style {
-                Err(Error::new(
+fn parse_sql_bind_style(
+    properties: &HashMap<String, String>,
+    key: &str,
+    additional_keys: &[&str],
+    default: SqlBindStyle,
+) -> Result<SqlBindStyle> {
+    properties
+        .get(key)
+        .or_else(|| additional_keys.iter().find_map(|key| 
properties.get(*key)))
+        .map_or(Ok(default), |value| {
+            SqlBindStyle::from_str(value).map_err(|_| {

Review Comment:
   On the legacy-key path this names the wrong key — the first `format!` arg is 
hardcoded to `SQL_CATALOG_PROP_BIND_STYLE`, so an invalid `sql_bind_style` 
value produces an error mentioning `sql.bind-style` instead of the key the user 
actually set. I'd use the `key` parameter here. (The macro also wraps 
`with_context("property", …)` with the canonical key, so the whole context 
chain points at the wrong one.)



##########
crates/catalog/sql/src/catalog.rs:
##########
@@ -1047,7 +1026,7 @@ impl Catalog for SqlCatalog {
                 None => {
                     format!(
                         "{}/{}",
-                        self.warehouse_location.clone(),
+                        self.properties.warehouse_location.clone(),

Review Comment:
   `format!` borrows its args, so this `.clone()` allocates a `String` that's 
dropped immediately — `&self.properties.warehouse_location` is enough. Carried 
over from the old code, but might as well drop it while we're here.



##########
crates/catalog/sql/src/catalog.rs:
##########
@@ -150,7 +128,10 @@ impl SqlCatalogBuilder {
     /// If `SQL_CATALOG_PROP_BIND_STYLE` has a value set in `props` during 
`SqlCatalogBuilder::load`,
     /// that value takes precedence, and the value specified by this method 
will not be used.
     pub fn sql_bind_style(mut self, sql_bind_style: SqlBindStyle) -> Self {
-        self.config.sql_bind_style = sql_bind_style;
+        self.props.insert(

Review Comment:
   Heads up on a precedence flip. Before, `.sql_bind_style(X)` wrote to a typed 
field, and during `load()` a load-time legacy `sql_bind_style` key (when the 
preferred key was absent) overwrote it — so the load-time legacy key beat the 
builder method. Now `.sql_bind_style(X)` writes the preferred `sql.bind-style` 
key into `props`, load-time props get `extend`ed on top, and 
`parse_sql_bind_style` prefers `sql.bind-style` over the legacy key — so the 
builder value wins instead.
   
   Concretely `.sql_bind_style(DollarNumeric).load("cat", {"sql_bind_style": 
"QMark"})` used to yield `QMark` and now yields `DollarNumeric`, silently.
   
   Since the legacy key is deprecated this may well be intentional — if so I'd 
just call it out in the `sql_bind_style()` doc comment (it currently only 
mentions the preferred key taking precedence). Either way I'd add a test with 
both the preferred and legacy keys set to different values, plus one for the 
builder-vs-load-legacy case, so the contract is pinned. Is the flip intended?



##########
crates/catalog/sql/src/catalog.rs:
##########
@@ -77,62 +78,38 @@ static MAX_CONNECTIONS: u32 = 10; // Default the SQL pool 
to 10 connections if n
 static IDLE_TIMEOUT: u64 = 10; // Default the maximum idle timeout per 
connection to 10s before it is closed
 static TEST_BEFORE_ACQUIRE: bool = true; // Default the health-check of each 
connection to enabled prior to returning
 
-fn parse_pool_property<T>(
-    props: &HashMap<String, String>,
-    property: &'static str,
-    default: T,
-) -> Result<T>
+fn parse_pool_property<T>(value: &str) -> Result<T>

Review Comment:
   The old signature attached `.with_context("property", property)` in here; 
the new one drops the key and relies on the macro re-adding it at the call 
site. That's fine as long as it's only ever called by the macro, but nothing 
marks it as macro-only — called directly it'd produce errors with no property 
key. A one-line doc comment noting the convention (or a `parse_pool_value` 
rename) would save the next person a footgun.



##########
crates/catalog/sql/src/catalog.rs:
##########
@@ -196,111 +177,120 @@ impl CatalogBuilder for SqlCatalogBuilder {
     }
 
     fn load(
-        mut self,
+        self,
         name: impl Into<String>,
         props: HashMap<String, String>,
     ) -> impl Future<Output = Result<Self::C>> + Send {
-        for (k, v) in props {
-            self.config.props.insert(k, v);
-        }
-
-        if let Some(uri) = self.config.props.remove(SQL_CATALOG_PROP_URI) {
-            self.config.uri = uri;
-        }
-        if let Some(warehouse_location) = 
self.config.props.remove(SQL_CATALOG_PROP_WAREHOUSE) {
-            self.config.warehouse_location = warehouse_location;
-        }
-
         let name = name.into();
 
-        let mut valid_sql_bind_style = true;
-
-        // Accept the preferred `sql.bind-style` key, falling back to the 
legacy `sql_bind_style`.
-        let sql_bind_style = self
-            .config
-            .props
-            .remove(SQL_CATALOG_PROP_BIND_STYLE)
-            .or_else(|| 
self.config.props.remove(SQL_CATALOG_PROP_BIND_STYLE_LEGACY));
-
-        // Validate the SQL bind style
-        if let Some(sql_bind_style) = sql_bind_style {
-            if let Ok(sql_bind_style) = 
SqlBindStyle::from_str(&sql_bind_style) {
-                self.config.sql_bind_style = sql_bind_style;
-            } else {
-                valid_sql_bind_style = false;
+        async move {
+            if name.trim().is_empty() {
+                return Err(Error::new(
+                    ErrorKind::DataInvalid,
+                    "Catalog name cannot be empty",
+                ));
             }
-        }
 
-        // Parse the requested schema version up front so invalid values fail 
fast rather than
-        // silently falling back to V0.
-        let mut valid_schema_version = true;
-        if let Some(schema_version) = 
self.config.props.remove(SQL_CATALOG_PROP_SCHEMA_VERSION) {
-            match SchemaVersion::from_str(&schema_version) {
-                Ok(schema_version) => self.config.schema_version = 
Some(schema_version),
-                Err(_) => valid_schema_version = false,
-            }
-        }
+            let mut merged_props = self.props;
+            merged_props.extend(props);
+            let catalog_properties = 
SqlCatalogProperties::from_properties(&merged_props)?;
 
-        let valid_name = !name.trim().is_empty();
+            let runtime = match self.runtime {
+                Some(rt) => rt,
+                None => Runtime::try_current()?,
+            };
+            let kms_client = match self.kms_client_factory {
+                Some(factory) => 
Some(factory.create_kms_client(&merged_props).await?),
+                None => None,
+            };
+            SqlCatalog::new(
+                name,
+                catalog_properties,
+                merged_props,
+                self.storage_factory,
+                runtime,
+                kms_client,
+            )
+            .await
+        }
+    }
+}
 
-        async move {
-            if !valid_name {
-                Err(Error::new(
-                    ErrorKind::DataInvalid,
-                    "Catalog name cannot be empty",
-                ))
-            } else if !valid_sql_bind_style {
-                Err(Error::new(
+fn parse_sql_bind_style(
+    properties: &HashMap<String, String>,
+    key: &str,
+    additional_keys: &[&str],
+    default: SqlBindStyle,
+) -> Result<SqlBindStyle> {
+    properties
+        .get(key)
+        .or_else(|| additional_keys.iter().find_map(|key| 
properties.get(*key)))

Review Comment:
   Small readability thing — the closure param `key` shadows the outer `key: 
&str`. Harmless, but renaming it to `alt_key` (or `k`) makes the fallback 
lookup easier to follow.



##########
crates/catalog/sql/src/catalog.rs:
##########
@@ -310,10 +300,9 @@ struct SqlCatalogConfig {
 /// Catalogs can opt-in to automatic migration by configuring the 
`sql.schema-version` catalog property.
 pub struct SqlCatalog {
     name: String,
+    properties: SqlCatalogProperties,

Review Comment:
   `SqlCatalog` now holds the whole `SqlCatalogProperties`, but `uri`, the pool 
fields and `schema_version` are all construction-only — the pool's already open 
and the URI's consumed by the time we store this. It also leaves us with two 
schema-version fields (`properties.schema_version`, the requested one, and the 
struct's own `schema_version`, the resolved runtime one), which is easy to mix 
up. I'd consume `properties` inside `new()` and store just what runtime needs 
(`warehouse_location` + `sql_bind_style`) — keeps the typed-parse win without 
the dual state. Not blocking. wdyt?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to