hudi-agent commented on code in PR #19733:
URL: https://github.com/apache/hudi/pull/19733#discussion_r3856931758


##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/AlterTableCommand.scala:
##########
@@ -208,10 +210,57 @@ case class AlterTableCommand(table: CatalogTable, 
changes: Seq[TableChange], cha
     val newTable = table.copy(
       properties = table.properties ++ properties,
       comment = 
properties.get(TableCatalog.PROP_COMMENT).orElse(table.comment))
+    updateHoodieTableConfigs(sparkSession, properties)
     catalog.alterTable(newTable)
     logInfo("table properties change finished")
   }
 
+  /**
+   * Persists Hudi table properties in hoodie.properties as well as the Spark 
catalog.
+   *
+   * The analyzer only creates this command for Hudi V2 tables, so the table 
is known to be a Hudi
+   * table. Keep non-Hudi properties catalog-only, matching Spark's normal 
ALTER TABLE behavior.
+   * Hudi's SQL aliases and datasource options are converted to the canonical 
keys stored in
+   * hoodie.properties before validation and persistence.
+   */
+  private def updateHoodieTableConfigs(sparkSession: SparkSession, properties: 
Map[String, String]): Unit = {
+    val tableConfigs = HoodieOptionConfig.mapSqlOptionsToTableConfigs(
+      HoodieOptionConfig.extractHoodieOptions(properties))
+
+    if (tableConfigs.nonEmpty) {
+      val metaClient = getMetaClient(sparkSession)
+
+      HoodieWriterUtils.validateTableConfig(
+        sparkSession,
+        tableConfigs,
+        metaClient.getTableConfig)
+
+      HoodieTableConfig.update(
+        metaClient.getStorage,
+        metaClient.getMetaPath,
+        TypedProperties.fromMap(tableConfigs.asJava))
+    }
+  }
+  private def deleteHoodieTableConfigs(sparkSession: SparkSession, 
propertyKeys: Seq[String]): Unit = {
+    val tableConfigs = HoodieOptionConfig.mapSqlOptionsToTableConfigs(
+      HoodieOptionConfig.extractHoodieOptions(propertyKeys.map(_ -> "").toMap))

Review Comment:
   🤖 nit: the `map(_ -> "").toMap` trick — using dummy empty values just to get 
key translation — is a bit surprising on first read. A short inline comment 
like `// dummy values; only the translated keys are used` would save the next 
reader a minute of head-scratching.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/AlterTableCommand.scala:
##########
@@ -208,10 +210,57 @@ case class AlterTableCommand(table: CatalogTable, 
changes: Seq[TableChange], cha
     val newTable = table.copy(
       properties = table.properties ++ properties,
       comment = 
properties.get(TableCatalog.PROP_COMMENT).orElse(table.comment))
+    updateHoodieTableConfigs(sparkSession, properties)
     catalog.alterTable(newTable)
     logInfo("table properties change finished")
   }
 
+  /**
+   * Persists Hudi table properties in hoodie.properties as well as the Spark 
catalog.
+   *
+   * The analyzer only creates this command for Hudi V2 tables, so the table 
is known to be a Hudi
+   * table. Keep non-Hudi properties catalog-only, matching Spark's normal 
ALTER TABLE behavior.
+   * Hudi's SQL aliases and datasource options are converted to the canonical 
keys stored in
+   * hoodie.properties before validation and persistence.
+   */
+  private def updateHoodieTableConfigs(sparkSession: SparkSession, properties: 
Map[String, String]): Unit = {
+    val tableConfigs = HoodieOptionConfig.mapSqlOptionsToTableConfigs(
+      HoodieOptionConfig.extractHoodieOptions(properties))
+
+    if (tableConfigs.nonEmpty) {
+      val metaClient = getMetaClient(sparkSession)
+
+      HoodieWriterUtils.validateTableConfig(
+        sparkSession,
+        tableConfigs,
+        metaClient.getTableConfig)
+
+      HoodieTableConfig.update(
+        metaClient.getStorage,
+        metaClient.getMetaPath,
+        TypedProperties.fromMap(tableConfigs.asJava))
+    }
+  }
+  private def deleteHoodieTableConfigs(sparkSession: SparkSession, 
propertyKeys: Seq[String]): Unit = {
+    val tableConfigs = HoodieOptionConfig.mapSqlOptionsToTableConfigs(
+      HoodieOptionConfig.extractHoodieOptions(propertyKeys.map(_ -> "").toMap))
+
+    if (tableConfigs.nonEmpty) {
+      val metaClient = getMetaClient(sparkSession)
+      HoodieTableConfig.delete(
+        metaClient.getStorage,
+        metaClient.getMetaPath,
+        tableConfigs.keySet.asJava)
+    }

Review Comment:
   🤖 Agree this is worth guarding. `extractHoodieOptions` passes through any 
`hoodie.*` key, so UNSET could delete immutable configs like 
`hoodie.table.recordkey.fields`, `hoodie.table.name`, or 
`hoodie.table.partition.fields` from hoodie.properties and leave the table 
unreadable. Restricting UNSET to a known-mutable set (or routing it through the 
same validation) rather than deleting arbitrary hoodie keys seems safer.



##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/AlterTableCommand.scala:
##########
@@ -208,10 +210,57 @@ case class AlterTableCommand(table: CatalogTable, 
changes: Seq[TableChange], cha
     val newTable = table.copy(
       properties = table.properties ++ properties,
       comment = 
properties.get(TableCatalog.PROP_COMMENT).orElse(table.comment))
+    updateHoodieTableConfigs(sparkSession, properties)
     catalog.alterTable(newTable)
     logInfo("table properties change finished")
   }
 
+  /**
+   * Persists Hudi table properties in hoodie.properties as well as the Spark 
catalog.
+   *
+   * The analyzer only creates this command for Hudi V2 tables, so the table 
is known to be a Hudi
+   * table. Keep non-Hudi properties catalog-only, matching Spark's normal 
ALTER TABLE behavior.
+   * Hudi's SQL aliases and datasource options are converted to the canonical 
keys stored in
+   * hoodie.properties before validation and persistence.
+   */
+  private def updateHoodieTableConfigs(sparkSession: SparkSession, properties: 
Map[String, String]): Unit = {
+    val tableConfigs = HoodieOptionConfig.mapSqlOptionsToTableConfigs(
+      HoodieOptionConfig.extractHoodieOptions(properties))

Review Comment:
   🤖 `extractHoodieOptions` keeps every key starting with `hoodie.`, and 
`mapSqlOptionsToTableConfigs` passes keys it doesn't recognize through 
unchanged, so this looks like it will persist *any* `hoodie.*` property into 
`hoodie.properties` — not just table configs. e.g. `SET 
TBLPROPERTIES('hoodie.upsert.shuffle.parallelism'='100')` or a 
`hoodie.datasource.write.*` key would get written there. Since 
`validateTableConfig` only flags value *conflicts* with existing keys (a 
brand-new key has `existingValue == null`), nothing filters these out, and 
they'd become persistent table-level defaults. Could you restrict persistence 
to actual `HoodieTableConfig` keys?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/AlterTableCommand.scala:
##########
@@ -208,10 +210,57 @@ case class AlterTableCommand(table: CatalogTable, 
changes: Seq[TableChange], cha
     val newTable = table.copy(
       properties = table.properties ++ properties,
       comment = 
properties.get(TableCatalog.PROP_COMMENT).orElse(table.comment))
+    updateHoodieTableConfigs(sparkSession, properties)
     catalog.alterTable(newTable)
     logInfo("table properties change finished")
   }
 
+  /**
+   * Persists Hudi table properties in hoodie.properties as well as the Spark 
catalog.
+   *
+   * The analyzer only creates this command for Hudi V2 tables, so the table 
is known to be a Hudi
+   * table. Keep non-Hudi properties catalog-only, matching Spark's normal 
ALTER TABLE behavior.
+   * Hudi's SQL aliases and datasource options are converted to the canonical 
keys stored in
+   * hoodie.properties before validation and persistence.
+   */
+  private def updateHoodieTableConfigs(sparkSession: SparkSession, properties: 
Map[String, String]): Unit = {
+    val tableConfigs = HoodieOptionConfig.mapSqlOptionsToTableConfigs(
+      HoodieOptionConfig.extractHoodieOptions(properties))
+
+    if (tableConfigs.nonEmpty) {
+      val metaClient = getMetaClient(sparkSession)
+
+      HoodieWriterUtils.validateTableConfig(
+        sparkSession,
+        tableConfigs,
+        metaClient.getTableConfig)
+

Review Comment:
   🤖 Confirmed from the code — `validateTableConfig` appends to `diffConfigs` 
and throws whenever an incoming key already exists in the table config with a 
different value (HoodieWriterUtils around line 301). Only a small whitelist in 
`shouldIgnoreConfig` (base file format, a few payload/merge-mode cases, empty 
database name) is exempt, so any genuine change to an already-persisted config 
would fail rather than update. Might be worth clarifying whether SET is 
intended to be create-only, or whether the mutable-config case needs a 
different path.



##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/AlterTableCommand.scala:
##########
@@ -208,10 +210,57 @@ case class AlterTableCommand(table: CatalogTable, 
changes: Seq[TableChange], cha
     val newTable = table.copy(
       properties = table.properties ++ properties,
       comment = 
properties.get(TableCatalog.PROP_COMMENT).orElse(table.comment))
+    updateHoodieTableConfigs(sparkSession, properties)

Review Comment:
   🤖 Here (and in applyPropertyUnset at line 198) hoodie.properties is mutated 
before `catalog.alterTable`. If `catalog.alterTable` throws, hoodie.properties 
has already been updated/deleted while the Spark catalog stays unchanged, 
leaving the two out of sync with no rollback — and on UNSET the delete is 
irreversible. Is there a reason to persist to hoodie.properties first rather 
than after the catalog update succeeds, or is some compensating cleanup 
expected on failure?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



-- 
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]

Reply via email to