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

yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 7a04352992 [#12740] fix(doris): Preserve default value on column type 
change (#12747)
7a04352992 is described below

commit 7a043529925254bec1283256da10346b13b585f9
Author: StormSpirit <[email protected]>
AuthorDate: Wed Sep 30 08:51:18 2026 +0800

    [#12740] fix(doris): Preserve default value on column type change (#12747)
    
    ### What changes were proposed in this pull request?
    
    This pull request preserves the loaded Doris column default when
    `TableChange.UpdateColumnType` builds the replacement column definition.
    It also adds focused SQL-generation coverage for string, numeric,
    `CURRENT_TIMESTAMP`, explicit `DEFAULT NULL`, unset defaults, preserved
    column attributes, and nested-column rejection.
    
    The integration coverage verifies successful default-preserving type
    changes on Doris 3.0.6.2 and 4.0.6, and verifies on Doris 4.0.6 that an
    unsupported `VARCHAR` to `BOOLEAN` conversion is still rejected without
    changing the column type or default.
    
    This pull request is opened as a draft so it can be reviewed in parallel
    with a related Doris default-literal converter change. Before this draft
    is marked ready, it will be rebased onto that change, its string and
    numeric SQL expectations will be aligned with the merged Doris quoting
    rules, and an exact quote/backslash default-value case will be added and
    verified.
    
    ### Why are the changes needed?
    
    The Doris catalog currently replaces the existing default with
    `DEFAULT_VALUE_NOT_SET` when generating a `MODIFY COLUMN` statement for
    `UpdateColumnType`. Doris treats `MODIFY COLUMN` as a complete column
    definition and rejects the operation with `Can not change default value`
    when the submitted definition omits an existing default.
    
    Preserving the loaded default makes the generated definition faithful to
    the unchanged column attributes while leaving type and default
    compatibility validation to Doris.
    
    Fix: #12740
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. A supported Doris column type change can now preserve an existing
    default value instead of failing because the generated `MODIFY COLUMN`
    definition omits that default. This pull request does not change any
    public API or property key.
    
    ### How was this patch tested?
    
    - `./gradlew :catalogs:catalog-jdbc-doris:spotlessCheck --console=plain
    --no-daemon`
    - `./gradlew rat --console=plain --no-daemon`
    - `./gradlew :catalogs:catalog-jdbc-doris:test -PskipITs --console=plain
    --no-daemon`
    - `./gradlew :catalogs:catalog-jdbc-doris:build -x test --console=plain
    --no-daemon`
    - Local static precheck covering negative-test consistency, string-slice
    safety, switch-case coverage, stale-comment references, orphan-test
    detection, commit-message validation, regex validation, cross-module
    impact, and unintended-file detection: all checks passed.
    - Doris 3.0.6.2: `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    
'org.apache.gravitino.catalog.doris.integration.test.CatalogDoris3xIT.testAlterColumnTypePreservesDefaultValue'
    -PskipDockerTests=false -PdorisMultiVersionTest --console=plain
    --no-daemon`
    - Doris 4.0.6: `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    
'org.apache.gravitino.catalog.doris.integration.test.CatalogDoris4xIT.testAlterColumnTypePreservesDefaultValue'
    -PskipDockerTests=false -PdorisMultiVersionTest --console=plain
    --no-daemon`
    - Doris 4.0.6: `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    
'org.apache.gravitino.catalog.doris.integration.test.CatalogDoris4xIT.testAlterColumnTypeRejectsInvalidConversionWithDefaultValue'
    -PskipDockerTests=false -PdorisMultiVersionTest --console=plain
    --no-daemon`
    
    All listed checks and focused tests passed. The post-commit static
    precheck completed all nine checks successfully.
    
    ---------
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../DorisColumnDefaultValueConverter.java          |  49 +++++
 .../doris/operation/DorisTableOperations.java      |  82 +++++++-
 .../TestDorisColumnDefaultValueConverter.java      |  40 ++++
 .../doris/integration/test/CatalogDoris3xIT.java   | 234 +++++++++++++++++++++
 .../doris/integration/test/CatalogDoris4xIT.java   | 213 +++++++++++++++++++
 .../TestDorisTableOperationsSqlGeneration.java     | 209 ++++++++++++++++++
 6 files changed, 821 insertions(+), 6 deletions(-)

diff --git 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisColumnDefaultValueConverter.java
 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisColumnDefaultValueConverter.java
index 0d0c8d033d..243211f1ef 100644
--- 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisColumnDefaultValueConverter.java
+++ 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisColumnDefaultValueConverter.java
@@ -32,6 +32,7 @@ import static 
org.apache.gravitino.rel.Column.DEFAULT_VALUE_OF_CURRENT_TIMESTAMP
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import javax.annotation.Nullable;
 import 
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
 import org.apache.gravitino.catalog.jdbc.converter.JdbcTypeConverter;
 import org.apache.gravitino.rel.expressions.Expression;
@@ -88,6 +89,46 @@ public class DorisColumnDefaultValueConverter extends 
JdbcColumnDefaultValueConv
     return super.fromGravitino(defaultValue);
   }
 
+  /**
+   * Converts a loaded default value for a Doris MODIFY COLUMN definition.
+   *
+   * <p>String literals containing quote or backslash characters need 
Doris-specific escaping.
+   * Unparsed expressions come from native Doris metadata and are passed 
through unchanged.
+   *
+   * @param defaultValue the loaded Gravitino default value
+   * @param doubleEscapeBackslashes whether Doris 3.x requires an additional 
backslash-escaping
+   *     layer
+   * @param tripleEscapeQuotes whether Doris 3.x MODIFY COLUMN requires three 
backslashes before an
+   *     embedded double quote
+   * @return the Doris SQL representation, or {@code null} when the default is 
unset
+   */
+  @Nullable
+  public String fromGravitinoForModifyColumn(
+      Expression defaultValue, boolean doubleEscapeBackslashes, boolean 
tripleEscapeQuotes) {
+    if (DEFAULT_VALUE_NOT_SET.equals(defaultValue)) {
+      return null;
+    }
+
+    if (defaultValue instanceof UnparsedExpression) {
+      return ((UnparsedExpression) defaultValue).unparsedExpression();
+    }
+
+    if (defaultValue instanceof Literal) {
+      Literal<?> literal = (Literal<?>) defaultValue;
+      if (literal.value() != null
+          && (literal.dataType() instanceof Types.StringType
+              || literal.dataType() instanceof Types.VarCharType
+              || literal.dataType() instanceof Types.FixedCharType)) {
+        String value = String.valueOf(literal.value());
+        if (value.indexOf('\\') >= 0 || value.indexOf('\'') >= 0 || 
value.indexOf('"') >= 0) {
+          return quoteDorisModifyLiteral(value, doubleEscapeBackslashes, 
tripleEscapeQuotes);
+        }
+      }
+    }
+
+    return super.fromGravitino(defaultValue);
+  }
+
   @Override
   public Expression toGravitino(
       JdbcTypeConverter.JdbcTypeBean columnType,
@@ -162,6 +203,14 @@ public class DorisColumnDefaultValueConverter extends 
JdbcColumnDefaultValueConv
     return "\"" + escaped + "\"";
   }
 
+  private static String quoteDorisModifyLiteral(
+      String value, boolean doubleEscapeBackslashes, boolean 
tripleEscapeQuotes) {
+    String escapedBackslash = doubleEscapeBackslashes ? "\\".repeat(4) : 
"\\".repeat(2);
+    String escapedQuote = "\\".repeat(tripleEscapeQuotes ? 3 : 1) + "\"";
+    String escaped = value.replace("\\", escapedBackslash).replace("\"", 
escapedQuote);
+    return "\"" + escaped + "\"";
+  }
+
   private static String unescapeDorisLiteral(String value) {
     StringBuilder result = new StringBuilder(value.length());
     for (int i = 0; i < value.length(); i++) {
diff --git 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
index fb2cb54211..ef0ce30e62 100644
--- 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
+++ 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/operation/DorisTableOperations.java
@@ -78,6 +78,7 @@ import org.apache.gravitino.rel.indexes.Index;
 import org.apache.gravitino.rel.indexes.Indexes;
 import org.apache.gravitino.rel.partitions.ListPartition;
 import org.apache.gravitino.rel.partitions.RangePartition;
+import org.apache.gravitino.rel.types.Types;
 import org.apache.gravitino.utils.ExceptionMessages;
 
 /** Table operations for Apache Doris. */
@@ -840,7 +841,7 @@ public class DorisTableOperations extends 
JdbcTableOperations {
     TableChange.UpdateComment updateComment = null;
     List<TableChange.SetProperty> setProperties = new ArrayList<>();
     List<String> alterSql = new ArrayList<>();
-    Optional<String> addColumnDorisVersion =
+    Optional<String> alterColumnDorisVersion =
         Arrays.stream(changes)
                 .filter(TableChange.AddColumn.class::isInstance)
                 .map(TableChange.AddColumn.class::cast)
@@ -861,13 +862,23 @@ public class DorisTableOperations extends 
JdbcTableOperations {
       } else if (change instanceof TableChange.AddColumn) {
         TableChange.AddColumn addColumn = (TableChange.AddColumn) change;
         lazyLoadTable = getOrCreateTable(databaseName, tableName, 
lazyLoadTable);
-        alterSql.add(addColumnFieldDefinition(addColumn, 
addColumnDorisVersion));
+        alterSql.add(addColumnFieldDefinition(addColumn, 
alterColumnDorisVersion));
       } else if (change instanceof TableChange.RenameColumn) {
         throw new IllegalArgumentException("Rename column is not supported 
yet");
       } else if (change instanceof TableChange.UpdateColumnType) {
         lazyLoadTable = getOrCreateTable(databaseName, tableName, 
lazyLoadTable);
         TableChange.UpdateColumnType updateColumnType = 
(TableChange.UpdateColumnType) change;
-        alterSql.add(updateColumnTypeFieldDefinition(updateColumnType, 
lazyLoadTable));
+        if (alterColumnDorisVersion.isEmpty() && 
updateColumnType.fieldName().length == 1) {
+          JdbcColumn currentColumn =
+              getJdbcColumnFromTable(lazyLoadTable, 
updateColumnType.fieldName()[0]);
+          if (requiresVersionAwareModifyEscaping(currentColumn)) {
+            alterColumnDorisVersion =
+                Optional.of(getDorisVersion("MODIFY COLUMN default literal 
compatibility check"));
+          }
+        }
+        alterSql.add(
+            updateColumnTypeFieldDefinition(
+                updateColumnType, lazyLoadTable, alterColumnDorisVersion));
       } else if (change instanceof TableChange.UpdateColumnComment) {
         TableChange.UpdateColumnComment updateColumnComment =
             (TableChange.UpdateColumnComment) change;
@@ -1066,6 +1077,25 @@ public class DorisTableOperations extends 
JdbcTableOperations {
     return stringValue.contains("\\") || stringValue.contains("\"\"");
   }
 
+  private static boolean requiresVersionAwareModifyEscaping(Column column) {
+    if (!(column.defaultValue() instanceof Literal)) {
+      return false;
+    }
+
+    Literal<?> literal = (Literal<?>) column.defaultValue();
+    if (!(literal.dataType() instanceof Types.StringType
+        || literal.dataType() instanceof Types.VarCharType
+        || literal.dataType() instanceof Types.FixedCharType)) {
+      return false;
+    }
+    Object value = literal.value();
+    if (value == null) {
+      return false;
+    }
+    String stringValue = String.valueOf(value);
+    return stringValue.contains("\\") || stringValue.contains("\"");
+  }
+
   private String serializeAddColumnDefaultValue(
       Expression defaultValue, Optional<String> dorisVersion) {
     Preconditions.checkState(
@@ -1136,7 +1166,9 @@ public class DorisTableOperations extends 
JdbcTableOperations {
   }
 
   private String updateColumnTypeFieldDefinition(
-      TableChange.UpdateColumnType updateColumnType, JdbcTable jdbcTable) {
+      TableChange.UpdateColumnType updateColumnType,
+      JdbcTable jdbcTable,
+      Optional<String> dorisVersion) {
     if (updateColumnType.fieldName().length > 1) {
       throw new UnsupportedOperationException("Doris does not support nested 
column names.");
     }
@@ -1148,11 +1180,49 @@ public class DorisTableOperations extends 
JdbcTableOperations {
             .withName(col)
             .withType(updateColumnType.getNewDataType())
             .withComment(column.comment())
-            .withDefaultValue(DEFAULT_VALUE_NOT_SET)
+            .withDefaultValue(column.defaultValue())
             .withNullable(column.nullable())
             .withAutoIncrement(column.autoIncrement())
             .build();
-    return appendColumnDefinition(newColumn, sqlBuilder).toString();
+    return appendColumnDefinitionForModify(newColumn, sqlBuilder, 
dorisVersion).toString();
+  }
+
+  private StringBuilder appendColumnDefinitionForModify(
+      JdbcColumn column, StringBuilder sqlBuilder, Optional<String> 
dorisVersion) {
+    
sqlBuilder.append(SPACE).append(typeConverter.fromGravitino(column.dataType())).append(SPACE);
+
+    if (column.nullable()) {
+      sqlBuilder.append("NULL ");
+    } else {
+      sqlBuilder.append("NOT NULL ");
+    }
+
+    if (!DEFAULT_VALUE_NOT_SET.equals(column.defaultValue())) {
+      Preconditions.checkState(
+          columnDefaultValueConverter instanceof 
DorisColumnDefaultValueConverter,
+          "DorisColumnDefaultValueConverter is required for Doris MODIFY 
COLUMN");
+      DorisColumnDefaultValueConverter converter =
+          (DorisColumnDefaultValueConverter) columnDefaultValueConverter;
+      boolean isDoris3x =
+          dorisVersion
+              .map(
+                  version ->
+                      isVersionAtLeast(version, 3, 0, 0) && 
!isVersionAtLeast(version, 4, 0, 0))
+              .orElse(false);
+      String defaultValue =
+          converter.fromGravitinoForModifyColumn(column.defaultValue(), 
isDoris3x, isDoris3x);
+      Preconditions.checkState(defaultValue != null, "Doris default value must 
not be null");
+      sqlBuilder.append("DEFAULT ").append(defaultValue).append(SPACE);
+    }
+
+    if (column.autoIncrement()) {
+      sqlBuilder.append(DORIS_AUTO_INCREMENT).append(" ");
+    }
+
+    if (StringUtils.isNotEmpty(column.comment())) {
+      sqlBuilder.append("COMMENT '").append(escapeSqlLiteral(column.comment(), 
'\'')).append("' ");
+    }
+    return sqlBuilder;
   }
 
   private StringBuilder appendColumnDefinition(JdbcColumn column, 
StringBuilder sqlBuilder) {
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisColumnDefaultValueConverter.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisColumnDefaultValueConverter.java
index 0f494cbc79..34ff4f4c70 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisColumnDefaultValueConverter.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisColumnDefaultValueConverter.java
@@ -18,9 +18,11 @@
  */
 package org.apache.gravitino.catalog.doris.converter;
 
+import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
 import static 
org.apache.gravitino.rel.Column.DEFAULT_VALUE_OF_CURRENT_TIMESTAMP;
 
 import org.apache.gravitino.catalog.jdbc.converter.JdbcTypeConverter;
+import org.apache.gravitino.rel.expressions.UnparsedExpression;
 import org.apache.gravitino.rel.expressions.literals.Literal;
 import org.apache.gravitino.rel.expressions.literals.Literals;
 import org.apache.gravitino.rel.types.Types;
@@ -138,4 +140,42 @@ public class TestDorisColumnDefaultValueConverter {
         Literals.of("owner's a\"\"b \"value\"\\path", 
Types.VarCharType.of(255)),
         CONVERTER.toGravitino(varcharType, "owner's a\"\"b \"value\"\\\\path", 
false, false));
   }
+
+  /** Verifies loaded literal and native expression defaults use MODIFY COLUMN 
SQL forms. */
+  @Test
+  public void testModifyColumnSerializesLoadedDefaultValues() {
+    Assertions.assertNull(
+        CONVERTER.fromGravitinoForModifyColumn(DEFAULT_VALUE_NOT_SET, false, 
false));
+    Assertions.assertEquals(
+        "NULL", CONVERTER.fromGravitinoForModifyColumn(Literals.NULL, false, 
false));
+    Assertions.assertEquals(
+        "7", 
CONVERTER.fromGravitinoForModifyColumn(Literals.integerLiteral(7), false, 
false));
+    Assertions.assertEquals(
+        "CURRENT_TIMESTAMP",
+        
CONVERTER.fromGravitinoForModifyColumn(DEFAULT_VALUE_OF_CURRENT_TIMESTAMP, 
false, false));
+    Assertions.assertEquals(
+        "CURRENT_DATE",
+        CONVERTER.fromGravitinoForModifyColumn(
+            UnparsedExpression.of("CURRENT_DATE"), false, false));
+
+    Assertions.assertEquals(
+        "\"owner's \\\"value\\\"\\\\path\"",
+        CONVERTER.fromGravitinoForModifyColumn(
+            Literals.of("owner's \"value\"\\path", Types.VarCharType.of(255)), 
false, false));
+  }
+
+  /** Verifies adjacent backslash and quote escaping for Doris 3.x and 4.x. */
+  @Test
+  public void testModifyColumnEscapesAdjacentBackslashAndQuoteByDorisVersion() 
{
+    String defaultValue = "prefix" + "\\" + "\"" + "suffix";
+
+    Assertions.assertEquals(
+        "\"" + "prefix" + "\\".repeat(3) + "\"suffix\"",
+        CONVERTER.fromGravitinoForModifyColumn(
+            Literals.of(defaultValue, Types.VarCharType.of(255)), false, 
false));
+    Assertions.assertEquals(
+        "\"" + "prefix" + "\\".repeat(7) + "\"suffix\"",
+        CONVERTER.fromGravitinoForModifyColumn(
+            Literals.of(defaultValue, Types.VarCharType.of(255)), true, true));
+  }
 }
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
index a7c73db6e4..e45aeaff81 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris3xIT.java
@@ -309,6 +309,240 @@ public class CatalogDoris3xIT extends BaseIT {
         .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
   }
 
+  @Test
+  void testAlterColumnTypePreservesDefaultValue() throws SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    String tableName = 
GravitinoITUtils.genRandomName("t_alter_type_preserves_default");
+    NameIdentifier tid = NameIdentifier.of(schemaName, tableName);
+    String distributionColumnName = "dist_key";
+    String valueColumnName = "agg_value";
+    String createSql =
+        String.format(
+            "CREATE TABLE %s ("
+                + "%s INT NOT NULL DEFAULT \"7\", "
+                + "%s BIGINT NOT NULL, "
+                + "%s BIGINT SUM DEFAULT \"0\""
+                + ") AGGREGATE KEY(%s, %s) "
+                + "DISTRIBUTED BY HASH(%s) BUCKETS 1 "
+                + "PROPERTIES (\"replication_num\" = \"1\")",
+            tableName,
+            colName1,
+            distributionColumnName,
+            valueColumnName,
+            colName1,
+            distributionColumnName,
+            distributionColumnName);
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(createSql);
+    }
+
+    Column originalColumn = findColumn(tc.loadTable(tid), colName1);
+    assertEquals(Types.IntegerType.get(), originalColumn.dataType());
+    assertEquals(Literals.integerLiteral(7), originalColumn.defaultValue());
+
+    tc.alterTable(tid, TableChange.updateColumnType(new String[] {colName1}, 
Types.LongType.get()));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column updatedColumn = findColumn(tc.loadTable(tid), colName1);
+              assertEquals(Types.LongType.get(), updatedColumn.dataType());
+              assertEquals(Literals.longLiteral(7L), 
updatedColumn.defaultValue());
+            });
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format(
+              "INSERT INTO %s (%s, %s) VALUES (101, 0)",
+              tableName, distributionColumnName, valueColumnName));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format(
+                  "SELECT %s FROM %s WHERE %s = 101",
+                  colName1, tableName, distributionColumnName))) {
+        assertTrue(resultSet.next());
+        assertEquals(7L, resultSet.getLong(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  void testAlterColumnTypePreservesQuotedAndBackslashDefaultValue() throws 
SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    String tableName = 
GravitinoITUtils.genRandomName("t_alter_type_special_default");
+    NameIdentifier tid = NameIdentifier.of(schemaName, tableName);
+    String logicalDefault = "owner's \"value\"\\\\path";
+    String sqlDefault = "owner's \\\"value\\\"" + "\\\\\\\\" + "path";
+    String createSql =
+        String.format(
+            "CREATE TABLE %s ("
+                + "col_pk BIGINT NOT NULL, "
+                + "col_data VARCHAR(32) NOT NULL DEFAULT \"%s\") "
+                + "DUPLICATE KEY(col_pk) "
+                + "DISTRIBUTED BY HASH(col_pk) BUCKETS 1 "
+                + "PROPERTIES (\"replication_num\" = \"1\")",
+            tableName, sqlDefault);
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(createSql);
+    }
+
+    assertEquals(
+        Literals.of(logicalDefault, Types.VarCharType.of(32)),
+        findColumn(tc.loadTable(tid), "col_data").defaultValue());
+    tc.alterTable(
+        tid, TableChange.updateColumnType(new String[] {"col_data"}, 
Types.VarCharType.of(64)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column updatedColumn = findColumn(tc.loadTable(tid), "col_data");
+              assertEquals(Types.VarCharType.of(64), updatedColumn.dataType());
+              assertEquals(
+                  Literals.of(logicalDefault, Types.VarCharType.of(64)),
+                  updatedColumn.defaultValue());
+            });
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(String.format("INSERT INTO %s (col_pk) VALUES 
(1)", tableName));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format("SELECT col_data FROM %s WHERE col_pk = 1", 
tableName))) {
+        assertTrue(resultSet.next());
+        assertEquals(logicalDefault, resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  void testAlterColumnTypePreservesBackslashOnlyDefaultValue() throws 
SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    String tableName = 
GravitinoITUtils.genRandomName("t_alter_type_backslash_default");
+    NameIdentifier tid = NameIdentifier.of(schemaName, tableName);
+    String logicalDefault = "owner's\\value";
+    String sqlDefault = "\"" + logicalDefault.replace("\\", "\\\\") + "\"";
+    String createSql =
+        String.format(
+            "CREATE TABLE %s ("
+                + "col_pk BIGINT NOT NULL, "
+                + "col_data VARCHAR(32) NOT NULL DEFAULT %s) "
+                + "DUPLICATE KEY(col_pk) "
+                + "DISTRIBUTED BY HASH(col_pk) BUCKETS 1 "
+                + "PROPERTIES (\"replication_num\" = \"1\")",
+            tableName, sqlDefault);
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(createSql);
+    }
+
+    assertEquals(
+        Literals.of(logicalDefault, Types.VarCharType.of(32)),
+        findColumn(tc.loadTable(tid), "col_data").defaultValue());
+    tc.alterTable(
+        tid, TableChange.updateColumnType(new String[] {"col_data"}, 
Types.VarCharType.of(64)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column updatedColumn = findColumn(tc.loadTable(tid), "col_data");
+              assertEquals(Types.VarCharType.of(64), updatedColumn.dataType());
+              assertEquals(
+                  Literals.of(logicalDefault, Types.VarCharType.of(64)),
+                  updatedColumn.defaultValue());
+            });
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(String.format("INSERT INTO %s (col_pk) VALUES 
(1)", tableName));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format("SELECT col_data FROM %s WHERE col_pk = 1", 
tableName))) {
+        assertTrue(resultSet.next());
+        assertEquals(logicalDefault, resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  void testAlterColumnTypePreservesAdjacentBackslashQuoteDefaultValue() throws 
SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    String tableName = 
GravitinoITUtils.genRandomName("t_alter_type_adjacent_default");
+    NameIdentifier tid = NameIdentifier.of(schemaName, tableName);
+    String logicalDefault = "prefix" + "\\" + "\"" + "suffix";
+    String sqlDefault = "\"" + logicalDefault.replace("\\", 
"\\\\").replace("\"", "\\\"") + "\"";
+    String createSql =
+        String.format(
+            "CREATE TABLE %s ("
+                + "col_pk BIGINT NOT NULL, "
+                + "col_data VARCHAR(32) NOT NULL DEFAULT %s) "
+                + "DUPLICATE KEY(col_pk) "
+                + "DISTRIBUTED BY HASH(col_pk) BUCKETS 1 "
+                + "PROPERTIES (\"replication_num\" = \"1\")",
+            tableName, sqlDefault);
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(createSql);
+    }
+
+    assertEquals(
+        Literals.of(logicalDefault, Types.VarCharType.of(32)),
+        findColumn(tc.loadTable(tid), "col_data").defaultValue());
+    tc.alterTable(
+        tid, TableChange.updateColumnType(new String[] {"col_data"}, 
Types.VarCharType.of(64)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column updatedColumn = findColumn(tc.loadTable(tid), "col_data");
+              assertEquals(Types.VarCharType.of(64), updatedColumn.dataType());
+              assertEquals(
+                  Literals.of(logicalDefault, Types.VarCharType.of(64)),
+                  updatedColumn.defaultValue());
+            });
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(String.format("INSERT INTO %s (col_pk) VALUES 
(1)", tableName));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format("SELECT col_data FROM %s WHERE col_pk = 1", 
tableName))) {
+        assertTrue(resultSet.next());
+        assertEquals(logicalDefault, resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
   @Test
   void testAddColumnPreservesDefaultValue() throws SQLException {
     TableCatalog tc = catalog.asTableCatalog();
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
index dff696f912..3badfd22d3 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
@@ -345,6 +345,219 @@ public class CatalogDoris4xIT extends BaseIT {
         .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
   }
 
+  @Test
+  void testAlterColumnTypePreservesDefaultValue() throws SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(
+            schemaName, 
GravitinoITUtils.genRandomName("t_alter_type_preserves_default"));
+    Column defaultedColumn =
+        Column.of(
+            colName2,
+            Types.VarCharType.of(10),
+            "defaulted column",
+            false,
+            false,
+            Literals.of("seed", Types.VarCharType.of(10)));
+
+    tc.createTable(
+        tid,
+        new Column[] {
+          Column.of(colName1, Types.LongType.get(), "pk", false, false, null), 
defaultedColumn
+        },
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    tc.alterTable(
+        tid, TableChange.updateColumnType(new String[] {colName2}, 
Types.VarCharType.of(20)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column updatedColumn = findColumn(tc.loadTable(tid), colName2);
+              assertEquals(Types.VarCharType.of(20), updatedColumn.dataType());
+              assertEquals(
+                  Literals.of("seed", Types.VarCharType.of(20)), 
updatedColumn.defaultValue());
+            });
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format("INSERT INTO %s (%s) VALUES (1)", tid.name(), 
colName1));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format("SELECT %s FROM %s WHERE %s = 1", colName2, 
tid.name(), colName1))) {
+        assertTrue(resultSet.next());
+        assertEquals("seed", resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  void testAlterColumnTypePreservesQuotedAndBackslashDefaultValue() throws 
SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    String tableName = 
GravitinoITUtils.genRandomName("t_alter_type_special_default");
+    NameIdentifier tid = NameIdentifier.of(schemaName, tableName);
+    String logicalDefault = "owner's \"value\"\\path";
+    String sqlDefault = "owner's \\\"value\\\"" + "\\\\" + "path";
+    String createSql =
+        String.format(
+            "CREATE TABLE %s ("
+                + "col_pk BIGINT NOT NULL, "
+                + "col_data VARCHAR(32) NOT NULL DEFAULT \"%s\") "
+                + "DUPLICATE KEY(col_pk) "
+                + "DISTRIBUTED BY HASH(col_pk) BUCKETS 1 "
+                + "PROPERTIES (\"replication_num\" = \"1\")",
+            tableName, sqlDefault);
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(createSql);
+    }
+
+    assertEquals(
+        Literals.of(logicalDefault, Types.VarCharType.of(32)),
+        findColumn(tc.loadTable(tid), "col_data").defaultValue());
+    tc.alterTable(
+        tid, TableChange.updateColumnType(new String[] {"col_data"}, 
Types.VarCharType.of(64)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column updatedColumn = findColumn(tc.loadTable(tid), "col_data");
+              assertEquals(Types.VarCharType.of(64), updatedColumn.dataType());
+              assertEquals(
+                  Literals.of(logicalDefault, Types.VarCharType.of(64)),
+                  updatedColumn.defaultValue());
+            });
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(String.format("INSERT INTO %s (col_pk) VALUES 
(1)", tableName));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format("SELECT col_data FROM %s WHERE col_pk = 1", 
tableName))) {
+        assertTrue(resultSet.next());
+        assertEquals(logicalDefault, resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  void testAlterColumnTypePreservesAdjacentBackslashQuoteDefaultValue() throws 
SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    String tableName = 
GravitinoITUtils.genRandomName("t_alter_type_adjacent_default");
+    NameIdentifier tid = NameIdentifier.of(schemaName, tableName);
+    String logicalDefault = "prefix" + "\\" + "\"" + "suffix";
+    String sqlDefault = "\"" + logicalDefault.replace("\\", 
"\\\\").replace("\"", "\\\"") + "\"";
+    String createSql =
+        String.format(
+            "CREATE TABLE %s ("
+                + "col_pk BIGINT NOT NULL, "
+                + "col_data VARCHAR(32) NOT NULL DEFAULT %s) "
+                + "DUPLICATE KEY(col_pk) "
+                + "DISTRIBUTED BY HASH(col_pk) BUCKETS 1 "
+                + "PROPERTIES (\"replication_num\" = \"1\")",
+            tableName, sqlDefault);
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(createSql);
+    }
+
+    assertEquals(
+        Literals.of(logicalDefault, Types.VarCharType.of(32)),
+        findColumn(tc.loadTable(tid), "col_data").defaultValue());
+    tc.alterTable(
+        tid, TableChange.updateColumnType(new String[] {"col_data"}, 
Types.VarCharType.of(64)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column updatedColumn = findColumn(tc.loadTable(tid), "col_data");
+              assertEquals(Types.VarCharType.of(64), updatedColumn.dataType());
+              assertEquals(
+                  Literals.of(logicalDefault, Types.VarCharType.of(64)),
+                  updatedColumn.defaultValue());
+            });
+
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl + schemaName, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(String.format("INSERT INTO %s (col_pk) VALUES 
(1)", tableName));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format("SELECT col_data FROM %s WHERE col_pk = 1", 
tableName))) {
+        assertTrue(resultSet.next());
+        assertEquals(logicalDefault, resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  void testAlterColumnTypeRejectsInvalidConversionWithDefaultValue() {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(
+            schemaName, 
GravitinoITUtils.genRandomName("t_alter_type_rejects_invalid"));
+    Column defaultedColumn =
+        Column.of(
+            colName2,
+            Types.VarCharType.of(10),
+            "defaulted column",
+            false,
+            false,
+            Literals.of("true", Types.VarCharType.of(10)));
+
+    tc.createTable(
+        tid,
+        new Column[] {
+          Column.of(colName1, Types.LongType.get(), "pk", false, false, null), 
defaultedColumn
+        },
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    RuntimeException exception =
+        assertThrows(
+            RuntimeException.class,
+            () ->
+                tc.alterTable(
+                    tid,
+                    TableChange.updateColumnType(
+                        new String[] {colName2}, Types.BooleanType.get())));
+
+    assertTrue(
+        exception.getMessage().contains("Can not change VARCHAR to BOOLEAN"),
+        exception.getMessage());
+    Column unchangedColumn = findColumn(tc.loadTable(tid), colName2);
+    assertEquals(Types.VarCharType.of(10), unchangedColumn.dataType());
+    assertEquals(Literals.of("true", Types.VarCharType.of(10)), 
unchangedColumn.defaultValue());
+  }
+
   @Test
   void testAddColumnPreservesDefaultValue() throws SQLException {
     TableCatalog tc = catalog.asTableCatalog();
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
index 464797da76..86eea613d9 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperationsSqlGeneration.java
@@ -43,6 +43,7 @@ import org.apache.gravitino.catalog.jdbc.JdbcTable;
 import org.apache.gravitino.catalog.jdbc.converter.JdbcExceptionConverter;
 import org.apache.gravitino.rel.TableChange;
 import org.apache.gravitino.rel.expressions.NamedReference;
+import org.apache.gravitino.rel.expressions.UnparsedExpression;
 import org.apache.gravitino.rel.expressions.distributions.Distribution;
 import org.apache.gravitino.rel.expressions.distributions.Distributions;
 import org.apache.gravitino.rel.expressions.literals.Literal;
@@ -50,6 +51,7 @@ import org.apache.gravitino.rel.expressions.literals.Literals;
 import org.apache.gravitino.rel.expressions.transforms.Transforms;
 import org.apache.gravitino.rel.indexes.Index;
 import org.apache.gravitino.rel.indexes.Indexes;
+import org.apache.gravitino.rel.types.Type;
 import org.apache.gravitino.rel.types.Types;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -107,6 +109,23 @@ public class TestDorisTableOperationsSqlGeneration {
       return generateAlterTableSql("database", tableName, changes);
     }
 
+    /**
+     * Generates SQL for updating the given column type.
+     *
+     * @param column the current JDBC column
+     * @param newType the requested Gravitino type
+     * @return the generated ALTER TABLE SQL
+     */
+    public String updateColumnTypeSql(JdbcColumn column, Type newType) {
+      setTableForAlter(
+          JdbcTable.builder()
+              .withName("test_table")
+              .withColumns(new JdbcColumn[] {column})
+              .build());
+      return alterTableSql(
+          "test_table", TableChange.updateColumnType(new String[] 
{column.name()}, newType));
+    }
+
     void setTableForAlter(JdbcTable table) {
       this.tableForAlter = table;
     }
@@ -135,6 +154,196 @@ public class TestDorisTableOperationsSqlGeneration {
     }
   }
 
+  /** Verifies string defaults and unchanged column attributes are preserved. 
*/
+  @Test
+  public void testUpdateColumnTypePreservesStringDefaultAndColumnAttributes() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn column =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.VarCharType.of(10))
+            .withComment("comment")
+            .withNullable(true)
+            .withDefaultValue(Literals.of("seed", Types.VarCharType.of(10)))
+            .build();
+
+    Assertions.assertEquals(
+        "ALTER TABLE `test_table`\n"
+            + "MODIFY COLUMN `col1` varchar(20) NULL DEFAULT 'seed' COMMENT 
'comment' ;",
+        ops.updateColumnTypeSql(column, Types.VarCharType.of(20)));
+  }
+
+  /** Verifies numeric and current-timestamp defaults are preserved. */
+  @Test
+  public void 
testUpdateColumnTypePreservesNumericAndCurrentTimestampDefaults() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn numericColumn =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.IntegerType.get())
+            .withNullable(false)
+            .withDefaultValue(Literals.integerLiteral(7))
+            .build();
+    JdbcColumn timestampColumn =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.TimestampType.withoutTimeZone())
+            .withNullable(false)
+            .withDefaultValue(DEFAULT_VALUE_OF_CURRENT_TIMESTAMP)
+            .build();
+
+    Assertions.assertEquals(
+        "ALTER TABLE `test_table`\nMODIFY COLUMN `col1` bigint NOT NULL 
DEFAULT 7 ;",
+        ops.updateColumnTypeSql(numericColumn, Types.LongType.get()));
+    Assertions.assertEquals(
+        "ALTER TABLE `test_table`\n"
+            + "MODIFY COLUMN `col1` datetime(6) NOT NULL DEFAULT 
CURRENT_TIMESTAMP ;",
+        ops.updateColumnTypeSql(timestampColumn, 
Types.TimestampType.withoutTimeZone(6)));
+  }
+
+  /** Verifies quoted string defaults use Doris MODIFY COLUMN escaping. */
+  @Test
+  public void testUpdateColumnTypeUsesDorisModifyEscapingForQuotedDefaults() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    Literal<?> defaultValue = Literals.of("owner's \"value\"\\path", 
Types.VarCharType.of(32));
+    JdbcColumn column =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.VarCharType.of(32))
+            .withNullable(false)
+            .withDefaultValue(defaultValue)
+            .build();
+
+    String sql = ops.updateColumnTypeSql(column, Types.VarCharType.of(64));
+
+    Assertions.assertTrue(
+        sql.contains(
+            "DEFAULT "
+                + new DorisColumnDefaultValueConverter()
+                    .fromGravitinoForModifyColumn(defaultValue, true, true)),
+        sql);
+  }
+
+  /** Verifies native unparsed defaults pass through without interpretation. */
+  @Test
+  public void testUpdateColumnTypePassesThroughUnparsedDefault() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn column =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.DateType.get())
+            .withNullable(false)
+            .withDefaultValue(UnparsedExpression.of("CURRENT_DATE"))
+            .build();
+
+    Assertions.assertTrue(
+        ops.updateColumnTypeSql(column, 
Types.DateType.get()).contains("DEFAULT CURRENT_DATE"),
+        "Native default expression should pass through unchanged");
+  }
+
+  /** Verifies explicit NULL and an unset default remain distinct in generated 
SQL. */
+  @Test
+  public void testUpdateColumnTypePreservesNullAndUnsetDefaultDistinction() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    JdbcColumn nullDefault =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.IntegerType.get())
+            .withNullable(true)
+            .withDefaultValue(Literals.NULL)
+            .build();
+    JdbcColumn unsetDefault =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.IntegerType.get())
+            .withNullable(true)
+            .withDefaultValue(DEFAULT_VALUE_NOT_SET)
+            .build();
+
+    Assertions.assertTrue(
+        ops.updateColumnTypeSql(nullDefault, 
Types.LongType.get()).contains("NULL DEFAULT NULL ;"));
+    Assertions.assertTrue(
+        ops.updateColumnTypeSql(unsetDefault, 
Types.LongType.get()).contains("bigint NULL ;"));
+  }
+
+  /** Verifies nested column names remain unsupported for type changes. */
+  @Test
+  public void testUpdateColumnTypeRejectsNestedColumnName() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+
+    UnsupportedOperationException exception =
+        Assertions.assertThrows(
+            UnsupportedOperationException.class,
+            () ->
+                ops.alterTableSql(
+                    "test_table",
+                    TableChange.updateColumnType(
+                        new String[] {"parent", "child"}, 
Types.LongType.get())));
+
+    Assertions.assertEquals("Doris does not support nested column names.", 
exception.getMessage());
+  }
+
+  /** Verifies version lookup is cached across type changes in one ALTER 
request. */
+  @Test
+  public void testUpdateColumnTypeQueriesVersionOnceForAlterRequest() throws 
Exception {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource versionDataSource = 
mockVersionDataSource("doris-3.0.6.2-rc01-910c4249c5");
+    ops.setDataSource(versionDataSource);
+    JdbcColumn firstColumn =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.VarCharType.of(32))
+            .withDefaultValue(Literals.of("first\\value", 
Types.VarCharType.of(32)))
+            .build();
+    JdbcColumn secondColumn =
+        JdbcColumn.builder()
+            .withName("col2")
+            .withType(Types.VarCharType.of(32))
+            .withDefaultValue(Literals.of("second\"value", 
Types.VarCharType.of(32)))
+            .build();
+    ops.setTableForAlter(
+        JdbcTable.builder()
+            .withName("test_table")
+            .withColumns(new JdbcColumn[] {firstColumn, secondColumn})
+            .build());
+
+    ops.alterTableSql(
+        "test_table",
+        TableChange.updateColumnType(new String[] {"col1"}, 
Types.VarCharType.of(64)),
+        TableChange.updateColumnType(new String[] {"col2"}, 
Types.VarCharType.of(64)));
+
+    Mockito.verify(versionDataSource, Mockito.times(1)).getConnection();
+  }
+
+  /** Verifies ADD and MODIFY changes share one version lookup in an ALTER 
request. */
+  @Test
+  public void testAddAndUpdateColumnTypeShareVersionLookup() throws Exception {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource versionDataSource = 
mockVersionDataSource("doris-3.0.6.2-rc01-910c4249c5");
+    ops.setDataSource(versionDataSource);
+    JdbcColumn existingColumn =
+        JdbcColumn.builder()
+            .withName("col1")
+            .withType(Types.VarCharType.of(32))
+            .withDefaultValue(Literals.of("existing\\value", 
Types.VarCharType.of(32)))
+            .build();
+    ops.setTableForAlter(
+        JdbcTable.builder()
+            .withName("test_table")
+            .withColumns(new JdbcColumn[] {existingColumn})
+            .build());
+
+    ops.alterTableSql(
+        "test_table",
+        TableChange.addColumn(
+            new String[] {"col2"},
+            Types.VarCharType.of(32),
+            Literals.of("added\\value", Types.VarCharType.of(32))),
+        TableChange.updateColumnType(new String[] {"col1"}, 
Types.VarCharType.of(64)));
+
+    Mockito.verify(versionDataSource, Mockito.times(1)).getConnection();
+  }
+
   /**
    * Verifies DROP returns false for missing-table errors from different Doris 
versions.
    *

Reply via email to