yuqi1129 commented on code in PR #14057:
URL: https://github.com/apache/gravitino/pull/14057#discussion_r4345506107


##########
catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/operation/JdbcTableOperations.java:
##########
@@ -657,17 +657,61 @@ protected JdbcTable getOrCreateTable(
     return null != lazyLoadCreateTable ? lazyLoadCreateTable : 
load(databaseName, tableName);
   }
 
-  protected void validateUpdateColumnNullable(
+  /**
+   * Validates a nullability change and returns the column it applies to.
+   *
+   * @param change the nullability change
+   * @param table the table that contains the column
+   * @return the current column
+   */
+  protected JdbcColumn validateUpdateColumnNullable(
       TableChange.UpdateColumnNullability change, JdbcTable table) {
     if (change.fieldName().length > 1) {
       throw new UnsupportedOperationException("Nested column names are not 
supported");
     }
-    String col = change.fieldName()[0];
-    JdbcColumn column = getJdbcColumnFromTable(table, col);
-    if (!change.nullable() && column.defaultValue().equals(Literals.NULL)) {
-      throw new IllegalArgumentException(
-          "column " + col + " with null default value cannot be changed to not 
null");
+    return getJdbcColumnFromTable(table, change.fieldName()[0]);
+  }
+
+  /**
+   * Returns a copy of the column with the default value and nullability it 
has after all default
+   * value and nullability changes to it in the same alter request are 
applied. Dialects that
+   * regenerate the whole column definition for one change use it, so that the 
regenerated
+   * definition does not revert another change to the same column. A NULL 
default conflicts with NOT
+   * NULL, so it is dropped when the column ends up non-nullable.
+   *
+   * @param column the current column
+   * @param changes all changes in the alter request
+   * @return the column with the pending default value and nullability
+   */
+  protected static JdbcColumn withPendingDefaultValueAndNullability(
+      JdbcColumn column, TableChange... changes) {
+    Expression defaultValue = column.defaultValue();
+    boolean nullable = column.nullable();
+    for (TableChange change : changes) {
+      if (!(change instanceof TableChange.ColumnChange)) {
+        continue;
+      }
+      String[] fieldName = ((TableChange.ColumnChange) change).fieldName();
+      if (fieldName.length != 1 || !fieldName[0].equals(column.name())) {
+        continue;
+      }
+      if (change instanceof TableChange.UpdateColumnDefaultValue) {
+        defaultValue = ((TableChange.UpdateColumnDefaultValue) 
change).getNewDefaultValue();
+      } else if (change instanceof TableChange.UpdateColumnNullability) {
+        nullable = ((TableChange.UpdateColumnNullability) change).nullable();
+      }
+    }
+    if (!nullable && Literals.NULL.equals(defaultValue)) {
+      defaultValue = DEFAULT_VALUE_NOT_SET;
     }
+    return JdbcColumn.builder()
+        .withName(column.name())
+        .withType(column.dataType())

Review Comment:
   [Important] Other full-column definitions in the same request still use the 
original column state and can undo this nullability/default update.
   
   For an original nullable INT column with `DEFAULT NULL`, the real MySQL SQL 
generator produces this for `[updateColumnNullability(c, false), 
updateColumnType(c, Long)]`:
   ```sql
   ALTER TABLE `t`
     MODIFY COLUMN `c` int NOT NULL COMMENT 'old',
     MODIFY COLUMN `c` bigint NULL DEFAULT NULL COMMENT 'old';
   ```
   Pairing the nullability change with a comment update similarly emits a 
second definition restoring `NULL DEFAULT NULL`. Conversely, this helper copies 
the original type/comment, so a later nullability definition can revert earlier 
type/comment changes. These requests previously failed validation and are now 
accepted.
   
   I also executed the analogous ClickHouse sequence starting with `c 
Nullable(Int32)`: `MODIFY COLUMN c Int32, MODIFY COLUMN c Nullable(Int64) 
DEFAULT NULL` succeeds and leaves the column nullable.
   
   Could we build a coherent pending column state for all same-column changes 
and make every full-definition renderer use it (or emit a single final 
definition per column)? Please cover combinations/orderings with type, comment, 
and position changes, including the resulting column metadata. The MySQL 
example above is SQL-generation evidence, not a live MySQL execution result.



##########
catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/operation/JdbcTableOperations.java:
##########
@@ -657,17 +657,61 @@ protected JdbcTable getOrCreateTable(
     return null != lazyLoadCreateTable ? lazyLoadCreateTable : 
load(databaseName, tableName);
   }
 
-  protected void validateUpdateColumnNullable(
+  /**
+   * Validates a nullability change and returns the column it applies to.
+   *
+   * @param change the nullability change
+   * @param table the table that contains the column
+   * @return the current column
+   */
+  protected JdbcColumn validateUpdateColumnNullable(
       TableChange.UpdateColumnNullability change, JdbcTable table) {
     if (change.fieldName().length > 1) {
       throw new UnsupportedOperationException("Nested column names are not 
supported");
     }
-    String col = change.fieldName()[0];
-    JdbcColumn column = getJdbcColumnFromTable(table, col);
-    if (!change.nullable() && column.defaultValue().equals(Literals.NULL)) {
-      throw new IllegalArgumentException(
-          "column " + col + " with null default value cannot be changed to not 
null");
+    return getJdbcColumnFromTable(table, change.fieldName()[0]);
+  }
+
+  /**
+   * Returns a copy of the column with the default value and nullability it 
has after all default
+   * value and nullability changes to it in the same alter request are 
applied. Dialects that
+   * regenerate the whole column definition for one change use it, so that the 
regenerated
+   * definition does not revert another change to the same column. A NULL 
default conflicts with NOT
+   * NULL, so it is dropped when the column ends up non-nullable.
+   *
+   * @param column the current column
+   * @param changes all changes in the alter request
+   * @return the column with the pending default value and nullability
+   */
+  protected static JdbcColumn withPendingDefaultValueAndNullability(
+      JdbcColumn column, TableChange... changes) {
+    Expression defaultValue = column.defaultValue();
+    boolean nullable = column.nullable();
+    for (TableChange change : changes) {
+      if (!(change instanceof TableChange.ColumnChange)) {
+        continue;
+      }
+      String[] fieldName = ((TableChange.ColumnChange) change).fieldName();
+      if (fieldName.length != 1 || !fieldName[0].equals(column.name())) {
+        continue;
+      }
+      if (change instanceof TableChange.UpdateColumnDefaultValue) {
+        defaultValue = ((TableChange.UpdateColumnDefaultValue) 
change).getNewDefaultValue();
+      } else if (change instanceof TableChange.UpdateColumnNullability) {
+        nullable = ((TableChange.UpdateColumnNullability) change).nullable();
+      }
+    }
+    if (!nullable && Literals.NULL.equals(defaultValue)) {

Review Comment:
   [Important] This also silently changes an explicit request to set `DEFAULT 
NULL` into a request to remove the default, even without a nullability 
transition.
   
   Starting from `c INT NOT NULL DEFAULT 5`, call only:
   ```java
   TableChange.updateColumnDefaultValue(new String[] {"c"}, Literals.NULL)
   ```
   The real MySQL SQL generator now emits `MODIFY COLUMN c int NOT NULL ...` 
without a DEFAULT clause: this branch substitutes `DEFAULT_VALUE_NOT_SET` for 
the explicitly requested `Literals.NULL`. The request therefore removes the 
existing default instead of preserving the requested value or rejecting an 
incompatible change.
   
   Could we restrict cleanup to an inherited NULL default during the intended 
nullable-to-NOT-NULL transition, and explicitly reject an incompatible newly 
requested NULL default? Please add coverage for a standalone default update and 
for an explicit NULL default combined with NOT NULL in the same batch. 
`Literals.NULL` and `Column.DEFAULT_VALUE_NOT_SET` represent different user 
intentions.



##########
catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java:
##########
@@ -1430,20 +1431,11 @@ private String deleteIndexDefinition(
   }
 
   private String updateColumnNullabilityDefinition(
-      TableChange.UpdateColumnNullability change, JdbcTable table) {
-    validateUpdateColumnNullable(change, table);
+      TableChange.UpdateColumnNullability change, JdbcTable table, 
TableChange... changes) {
+    JdbcColumn column = validateUpdateColumnNullable(change, table);
 
     String col = change.fieldName()[0];
-    JdbcColumn column = getJdbcColumnFromTable(table, col);
-    JdbcColumn updateColumn =
-        JdbcColumn.builder()
-            .withName(col)
-            .withDefaultValue(column.defaultValue())
-            .withNullable(change.nullable())
-            .withType(column.dataType())
-            .withComment(column.comment())
-            .withAutoIncrement(column.autoIncrement())
-            .build();
+    JdbcColumn updateColumn = withPendingDefaultValueAndNullability(column, 
changes);

Review Comment:
   [Important] Omitting `DEFAULT` does not remove ClickHouse's existing default 
expression.
   
   For a column with an explicit `DEFAULT NULL`, this now generates the 
equivalent of:
   ```sql
   CREATE TABLE t (c Nullable(String) DEFAULT NULL) ENGINE = Memory;
   ALTER TABLE t MODIFY COLUMN c String;
   ```
   The ALTER fails locally on ClickHouse 26.1.1.712 with `Cannot convert NULL 
to a non-nullable type`: ClickHouse preserves the old default when a MODIFY 
supplies a type but omits the default expression. The same preservation logic 
is present in the [24.8 
source](https://github.com/ClickHouse/ClickHouse/blob/v24.8.14.39-lts/src/Storages/AlterCommands.cpp#L1273).
   
   Could we implement explicit default removal using ClickHouse's semantics and 
add a regression that executes the DDL and reloads the column? Checking that 
the generated SQL does not contain `DEFAULT` misses this failure. If removal 
requires separate statements, failure atomicity also needs consideration; on my 
local version, putting `REMOVE DEFAULT` and the type change in the same ALTER 
still fails.



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