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 99ecf448d8 [#12764] fix(doris): preserve defaults when adding columns 
(#12767)
99ecf448d8 is described below

commit 99ecf448d8eb66436a83945da0a483cf98451efd
Author: StormSpirit <[email protected]>
AuthorDate: Tue Sep 22 17:43:40 2026 +0800

    [#12764] fix(doris): preserve defaults when adding columns (#12767)
    
    ### What changes were proposed in this pull request?
    
    - Preserve string defaults when adding Doris columns, including values
    with consecutive quotes or backslashes.
    - Use single-quoted literals with doubled apostrophes for Doris versions
    before 4.0 when version-aware escaping is needed. Apply the additional
    backslash-escaping layer for Doris 3.x, while retaining the
    double-quoted literal form for Doris 4.x.
    - Detect the Doris version with `SELECT Version FROM FRONTENDS()` first,
    then fall back to `SHOW FRONTENDS` on older versions that do not support
    the table-valued function. Run the lookup only when a string default
    contains backslashes or consecutive double quotes.
    - Preserve repeated single and double quotes when converting string
    defaults from JDBC metadata; continue decoding backslash escape
    sequences.
    - Add unit and Docker IT coverage for SQL generation, metadata and
    inserted-value round-trips across Doris 1.2.x, 3.0.6.2, and 4.0.6.
    Include a limited-privilege AddColumn case for Doris 3.0.6.2.
    
    ### Why are the changes needed?
    
    The Doris catalog accepts a default value through the public AddColumn
    API but currently omits it from the generated column definition. A
    nullable column can therefore be added while silently losing its
    requested default, and Doris may reject a non-nullable column because
    the generated ADD COLUMN statement has no default.
    
    Doris versions also parse backslashes in ALTER ADD COLUMN defaults
    differently. Keeping this compatibility handling in an ADD-specific
    write path preserves the requested default without changing CREATE TABLE
    or unrelated MODIFY COLUMN write serialization. This patch separately
    normalizes the escaped string-default forms returned by Doris JDBC
    metadata during `loadTable()`.
    
    Fix: #12764
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Doris ADD COLUMN operations now preserve supported literal
    defaults, including strings with backslashes and consecutive double
    quotes. For a string default containing either condition, the connector
    resolves the FE version once per ALTER request by querying `SELECT
    Version FROM FRONTENDS()` first, then falls back to `SHOW FRONTENDS` on
    Doris versions that do not support the table-valued function. Doris
    1.2.x accounts using this fallback need permission to run `SHOW
    FRONTENDS`; on Doris 3.0.6.2, the tested limited-privilege account can
    query the version through `FRONTENDS()`. Explicit `DEFAULT NULL` remains
    distinct from an unset default at SQL generation time. No public API or
    property key is added or removed.
    
    ### How was this patch tested?
    
    - `./gradlew :catalogs:catalog-jdbc-doris:spotlessApply`
    - `./gradlew :catalogs:catalog-jdbc-doris:test -PskipITs`
    - `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    'org.apache.gravitino.catalog.doris.integration.test.CatalogDorisIT'
    -PskipDockerTests=false -PdorisMultiVersionTest`
    - `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    
'org.apache.gravitino.catalog.doris.integration.test.CatalogDoris3xIT.testAddColumnPreservesDefaultValue'
    --tests
    
'org.apache.gravitino.catalog.doris.integration.test.CatalogDoris3xIT.testAddColumnBackslashDefaultWithLimitedDorisPrivileges'
    -PskipDockerTests=false -PdorisMultiVersionTest`
    - `./gradlew :catalogs:catalog-jdbc-doris:test --tests
    
'org.apache.gravitino.catalog.doris.integration.test.CatalogDoris4xIT.testAddColumnPreservesDefaultValue'
    -PskipDockerTests=false -PdorisMultiVersionTest`
    
    All listed validations passed.
    
    ---------
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../DorisColumnDefaultValueConverter.java          |  85 +++++-
 .../doris/operation/DorisTableOperations.java      | 139 ++++++++--
 .../TestDorisColumnDefaultValueConverter.java      | 141 ++++++++++
 .../doris/integration/test/CatalogDoris3xIT.java   | 280 +++++++++++++++++++
 .../doris/integration/test/CatalogDoris4xIT.java   | 174 ++++++++++++
 .../doris/integration/test/CatalogDorisIT.java     | 198 ++++++++++++++
 .../TestDorisTableOperationsSqlGeneration.java     | 300 +++++++++++++++++++--
 7 files changed, 1264 insertions(+), 53 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 2566f226cd..0d0c8d033d 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
@@ -36,12 +36,58 @@ import 
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConvert
 import org.apache.gravitino.catalog.jdbc.converter.JdbcTypeConverter;
 import org.apache.gravitino.rel.expressions.Expression;
 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.Decimal;
+import org.apache.gravitino.rel.types.Type;
 import org.apache.gravitino.rel.types.Types;
 
 public class DorisColumnDefaultValueConverter extends 
JdbcColumnDefaultValueConverter {
 
+  /**
+   * Converts an ADD COLUMN default value to Doris SQL.
+   *
+   * @param defaultValue the Gravitino default value
+   * @param doubleEscapeBackslashes whether to add the extra backslash 
escaping required by Doris
+   *     3.x ALTER ADD COLUMN
+   * @return the Doris SQL representation
+   */
+  public String fromGravitinoForAddColumn(
+      Expression defaultValue, boolean doubleEscapeBackslashes) {
+    return fromGravitinoForAddColumn(defaultValue, doubleEscapeBackslashes, 
false);
+  }
+
+  /**
+   * Converts an ADD COLUMN default value to Doris SQL with a selected 
string-literal delimiter.
+   *
+   * @param defaultValue the Gravitino default value
+   * @param doubleEscapeBackslashes whether to add the extra backslash 
escaping required by Doris
+   *     3.x ALTER ADD COLUMN
+   * @param useSingleQuoteDelimiter whether string literals should use single 
quotes and doubled
+   *     apostrophes, as required by the legacy Doris parser for adjacent 
double quotes
+   * @return the Doris SQL representation
+   */
+  public String fromGravitinoForAddColumn(
+      Expression defaultValue, boolean doubleEscapeBackslashes, boolean 
useSingleQuoteDelimiter) {
+    if (defaultValue instanceof Literal) {
+      Literal<?> literal = (Literal<?>) defaultValue;
+      if (literal.equals(Literals.NULL)) {
+        return super.fromGravitino(defaultValue);
+      }
+      // Doris 1.2 parses literal defaults as quoted strings and uses 
MySQL-style escaping.
+      if (literal.dataType() instanceof Type.NumericType
+          || literal.dataType() instanceof Types.StringType
+          || literal.dataType() instanceof Types.VarCharType
+          || literal.dataType() instanceof Types.FixedCharType) {
+        return quoteDorisLiteral(
+            String.valueOf(literal.value()), doubleEscapeBackslashes, 
useSingleQuoteDelimiter);
+      }
+    }
+    // Doris accepts the base converter's standard SQL syntax for date/time 
literals and
+    // expressions.
+    return super.fromGravitino(defaultValue);
+  }
+
   @Override
   public Expression toGravitino(
       JdbcTypeConverter.JdbcTypeBean columnType,
@@ -90,13 +136,46 @@ public class DorisColumnDefaultValueConverter extends 
JdbcColumnDefaultValueConv
             : Literals.timestampLiteral(
                 LocalDateTime.parse(columnDefaultValue, DATE_TIME_FORMATTER));
       case JdbcTypeConverter.VARCHAR:
-        return Literals.of(columnDefaultValue, 
Types.VarCharType.of(columnType.getColumnSize()));
+        return Literals.of(
+            unescapeDorisLiteral(columnDefaultValue),
+            Types.VarCharType.of(columnType.getColumnSize()));
       case CHAR:
-        return Literals.of(columnDefaultValue, 
Types.FixedCharType.of(columnType.getColumnSize()));
+        return Literals.of(
+            unescapeDorisLiteral(columnDefaultValue),
+            Types.FixedCharType.of(columnType.getColumnSize()));
       case JdbcTypeConverter.TEXT:
-        return Literals.stringLiteral(columnDefaultValue);
+        return 
Literals.stringLiteral(unescapeDorisLiteral(columnDefaultValue));
       default:
         return UnparsedExpression.of(columnDefaultValue);
     }
   }
+
+  private static String quoteDorisLiteral(
+      String value, boolean doubleEscapeBackslashes, boolean 
useSingleQuoteDelimiter) {
+    String escapedBackslash = doubleEscapeBackslashes ? "\\\\\\\\" : "\\\\";
+    String escaped = value.replace("\\", escapedBackslash);
+    if (useSingleQuoteDelimiter) {
+      escaped = escaped.replace("'", "''");
+      return "'" + escaped + "'";
+    }
+    escaped = escaped.replace("\"", "\\\"");
+    return "\"" + escaped + "\"";
+  }
+
+  private static String unescapeDorisLiteral(String value) {
+    StringBuilder result = new StringBuilder(value.length());
+    for (int i = 0; i < value.length(); i++) {
+      char current = value.charAt(i);
+      if (current == '\\' && i + 1 < value.length()) {
+        char next = value.charAt(i + 1);
+        if (next == '\\' || next == '\'' || next == '"') {
+          result.append(next);
+          i++;
+          continue;
+        }
+      }
+      result.append(current);
+    }
+    return result.toString();
+  }
 }
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 8c25e45f56..f544dfad0d 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
@@ -55,6 +55,7 @@ import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.StringIdentifier;
+import 
org.apache.gravitino.catalog.doris.converter.DorisColumnDefaultValueConverter;
 import org.apache.gravitino.catalog.doris.utils.DorisUtils;
 import org.apache.gravitino.catalog.jdbc.JdbcColumn;
 import org.apache.gravitino.catalog.jdbc.JdbcTable;
@@ -64,6 +65,7 @@ import org.apache.gravitino.exceptions.NoSuchColumnException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.expressions.Expression;
 import org.apache.gravitino.rel.expressions.distributions.Distribution;
 import org.apache.gravitino.rel.expressions.distributions.Strategy;
 import org.apache.gravitino.rel.expressions.literals.Literal;
@@ -264,47 +266,78 @@ public class DorisTableOperations extends 
JdbcTableOperations {
     if (!hasAutoIncrement) {
       return;
     }
+    String version = getDorisVersion("AUTO_INCREMENT compatibility check");
+    if (!isVersionAtLeast(version, 2, 1, 0)) {
+      throw new UnsupportedOperationException(
+          "AUTO_INCREMENT requires Doris 2.1.0 or later. Current server 
version: " + version);
+    }
+  }
+
+  private String getDorisVersion(String purpose) {
     Preconditions.checkState(dataSource != null, "dataSource is required for 
version validation");
     String version = null;
-    // SELECT VERSION() returns the MySQL protocol version (e.g. "5.7.99"), 
not the Doris version.
-    // SHOW FRONTENDS returns the actual Doris version in the "Version" column
-    // (e.g. "doris-3.0.6.2-rc01-910c4249c5").
-    try (Connection connection = dataSource.getConnection();
-        Statement stmt = connection.createStatement();
-        ResultSet rs = stmt.executeQuery("SHOW FRONTENDS")) {
-      ResultSetMetaData meta = rs.getMetaData();
-      int versionCol = -1;
-      for (int i = 1; i <= meta.getColumnCount(); i++) {
-        if ("Version".equals(meta.getColumnLabel(i))) {
-          versionCol = i;
-          break;
+    SQLException frontendsQueryFailure = null;
+    try (Connection connection = dataSource.getConnection()) {
+      // FRONTENDS() is readable with the default information_schema SELECT 
privilege on Doris
+      // 3.0.6.2, while SHOW FRONTENDS requires ADMIN/OPERATOR there. Older 
Doris releases such as
+      // 1.2.x do not support the table-valued function, so retain SHOW 
FRONTENDS as a fallback.
+      try (Statement stmt = connection.createStatement();
+          ResultSet rs = stmt.executeQuery("SELECT Version FROM FRONTENDS()")) 
{
+        if (rs.next()) {
+          version = extractDorisVersion(rs.getString(1));
         }
+      } catch (SQLException e) {
+        frontendsQueryFailure = e;
       }
-      if (rs.next() && versionCol > 0) {
-        String versionStr = rs.getString(versionCol);
-        // Extract X.Y.Z from "doris-X.Y.Z-suffix-commit" using regex for 
robustness
-        Matcher matcher = DORIS_VERSION_PATTERN.matcher(versionStr);
-        if (matcher.find()) {
-          version = matcher.group(1);
+
+      if (version == null) {
+        try (Statement stmt = connection.createStatement();
+            ResultSet rs = stmt.executeQuery("SHOW FRONTENDS")) {
+          ResultSetMetaData meta = rs.getMetaData();
+          int versionCol = -1;
+          for (int i = 1; i <= meta.getColumnCount(); i++) {
+            if ("Version".equalsIgnoreCase(meta.getColumnLabel(i))) {
+              versionCol = i;
+              break;
+            }
+          }
+          if (rs.next() && versionCol > 0) {
+            version = extractDorisVersion(rs.getString(versionCol));
+          }
+        } catch (SQLException e) {
+          if (frontendsQueryFailure != null) {
+            e.addSuppressed(frontendsQueryFailure);
+          }
+          throw e;
         }
       }
     } catch (SQLException e) {
       throw new UnsupportedOperationException(
-          "Unable to determine Doris version for AUTO_INCREMENT compatibility 
check. "
-              + "Ensure the connection user has permission to execute SHOW 
FRONTENDS "
-              + "and the Doris FE is reachable.",
+          "Unable to determine Doris version for "
+              + purpose
+              + ". "
+              + "Ensure the connection user can query FRONTENDS() or, on older 
Doris versions, "
+              + "has permission to execute SHOW FRONTENDS, and the Doris FE is 
reachable.",
           e);
     }
     if (version == null) {
       throw new UnsupportedOperationException(
-          "Unable to determine Doris version for AUTO_INCREMENT compatibility 
check. "
-              + "Ensure the connection user has permission to execute SHOW 
FRONTENDS "
-              + "and the Doris FE is reachable.");
+          "Unable to determine Doris version for "
+              + purpose
+              + ". "
+              + "Ensure the connection user can query FRONTENDS() or, on older 
Doris versions, "
+              + "has permission to execute SHOW FRONTENDS, and the Doris FE is 
reachable.");
     }
-    if (!isVersionAtLeast(version, 2, 1, 0)) {
-      throw new UnsupportedOperationException(
-          "AUTO_INCREMENT requires Doris 2.1.0 or later. Current server 
version: " + version);
+    return version;
+  }
+
+  @Nullable
+  private static String extractDorisVersion(@Nullable String versionString) {
+    if (versionString == null) {
+      return null;
     }
+    Matcher matcher = DORIS_VERSION_PATTERN.matcher(versionString);
+    return matcher.find() ? matcher.group(1) : null;
   }
 
   @VisibleForTesting
@@ -759,6 +792,14 @@ public class DorisTableOperations extends 
JdbcTableOperations {
     TableChange.UpdateComment updateComment = null;
     List<TableChange.SetProperty> setProperties = new ArrayList<>();
     List<String> alterSql = new ArrayList<>();
+    Optional<String> addColumnDorisVersion =
+        Arrays.stream(changes)
+                .filter(TableChange.AddColumn.class::isInstance)
+                .map(TableChange.AddColumn.class::cast)
+                .map(TableChange.AddColumn::getDefaultValue)
+                
.anyMatch(DorisTableOperations::requiresVersionAwareAddColumnEscaping)
+            ? Optional.of(getDorisVersion("ADD COLUMN default literal 
compatibility check"))
+            : Optional.empty();
     for (int i = 0; i < changes.length; i++) {
       TableChange change = changes[i];
       if (change instanceof TableChange.UpdateComment) {
@@ -772,7 +813,7 @@ 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));
+        alterSql.add(addColumnFieldDefinition(addColumn, 
addColumnDorisVersion));
       } else if (change instanceof TableChange.RenameColumn) {
         throw new IllegalArgumentException("Rename column is not supported 
yet");
       } else if (change instanceof TableChange.UpdateColumnType) {
@@ -910,7 +951,8 @@ public class DorisTableOperations extends 
JdbcTableOperations {
         "MODIFY COLUMN `%s` COMMENT '%s'", col, escapeSqlLiteral(newComment, 
'\''));
   }
 
-  private String addColumnFieldDefinition(TableChange.AddColumn addColumn) {
+  private String addColumnFieldDefinition(
+      TableChange.AddColumn addColumn, Optional<String> dorisVersion) {
     String dataType = typeConverter.fromGravitino(addColumn.getDataType());
     if (addColumn.fieldName().length > 1) {
       throw new UnsupportedOperationException("Doris does not support nested 
column names.");
@@ -930,6 +972,14 @@ public class DorisTableOperations extends 
JdbcTableOperations {
     if (!addColumn.isNullable()) {
       columnDefinition.append("NOT NULL ");
     }
+
+    if (!DEFAULT_VALUE_NOT_SET.equals(addColumn.getDefaultValue())) {
+      columnDefinition
+          .append("DEFAULT ")
+          .append(serializeAddColumnDefaultValue(addColumn.getDefaultValue(), 
dorisVersion))
+          .append(SPACE);
+    }
+
     // Append comment if available
     if (StringUtils.isNotEmpty(addColumn.getComment())) {
       columnDefinition
@@ -956,6 +1006,37 @@ public class DorisTableOperations extends 
JdbcTableOperations {
     return columnDefinition.toString();
   }
 
+  private static boolean requiresVersionAwareAddColumnEscaping(Expression 
defaultValue) {
+    if (!(defaultValue instanceof Literal)) {
+      return false;
+    }
+    Object value = ((Literal<?>) defaultValue).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(
+        columnDefaultValueConverter instanceof 
DorisColumnDefaultValueConverter,
+        "DorisColumnDefaultValueConverter is required for Doris ADD COLUMN");
+    DorisColumnDefaultValueConverter converter =
+        (DorisColumnDefaultValueConverter) columnDefaultValueConverter;
+    boolean requiresDoubleEscaping =
+        dorisVersion
+            .map(
+                version ->
+                    isVersionAtLeast(version, 3, 0, 0) && 
!isVersionAtLeast(version, 4, 0, 0))
+            .orElse(false);
+    boolean useSingleQuoteDelimiter =
+        dorisVersion.map(version -> !isVersionAtLeast(version, 4, 0, 
0)).orElse(false);
+    return converter.fromGravitinoForAddColumn(
+        defaultValue, requiresDoubleEscaping, useSingleQuoteDelimiter);
+  }
+
   private String updateColumnPositionFieldDefinition(
       TableChange.UpdateColumnPosition updateColumnPosition, JdbcTable 
jdbcTable) {
     if (updateColumnPosition.fieldName().length > 1) {
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
new file mode 100644
index 0000000000..0f494cbc79
--- /dev/null
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisColumnDefaultValueConverter.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.catalog.doris.converter;
+
+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.literals.Literal;
+import org.apache.gravitino.rel.expressions.literals.Literals;
+import org.apache.gravitino.rel.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests for converting Doris column default values to SQL and Gravitino 
expressions. */
+public class TestDorisColumnDefaultValueConverter {
+
+  private static final DorisColumnDefaultValueConverter CONVERTER =
+      new DorisColumnDefaultValueConverter();
+
+  @Test
+  public void testEscapeStringLikeDefaultValues() {
+    Assertions.assertEquals(
+        "\"owner's\\\\value\"",
+        CONVERTER.fromGravitinoForAddColumn(
+            Literals.of("owner's\\value", Types.VarCharType.of(255)), false));
+    Assertions.assertEquals(
+        "\"owner's\\\\value\"",
+        CONVERTER.fromGravitinoForAddColumn(
+            Literals.of("owner's\\value", Types.FixedCharType.of(32)), false));
+    Assertions.assertEquals(
+        "\"owner's\\\\value\"",
+        
CONVERTER.fromGravitinoForAddColumn(Literals.stringLiteral("owner's\\value"), 
false));
+    Assertions.assertEquals(
+        "\"owner's \\\"value\\\"\\\\path\"",
+        CONVERTER.fromGravitinoForAddColumn(
+            Literals.of("owner's \"value\"\\path", Types.VarCharType.of(255)), 
false));
+    Assertions.assertEquals(
+        "\"\"",
+        CONVERTER.fromGravitinoForAddColumn(Literals.of("", 
Types.VarCharType.of(255)), false));
+    Assertions.assertEquals(
+        "\"   \"",
+        CONVERTER.fromGravitinoForAddColumn(Literals.of("   ", 
Types.VarCharType.of(255)), false));
+  }
+
+  @Test
+  public void testNumericDefaultUsesLegacyCompatibleQuotedSyntax() {
+    Assertions.assertEquals(
+        "\"7\"", 
CONVERTER.fromGravitinoForAddColumn(Literals.integerLiteral(7), false));
+  }
+
+  @Test
+  public void testDelegateNullAndExpressionDefaultValues() {
+    Assertions.assertEquals("NULL", 
CONVERTER.fromGravitinoForAddColumn(Literals.NULL, false));
+    Assertions.assertEquals(
+        "CURRENT_TIMESTAMP",
+        
CONVERTER.fromGravitinoForAddColumn(DEFAULT_VALUE_OF_CURRENT_TIMESTAMP, false));
+  }
+
+  @Test
+  public void testDoubleEscapeSequencesForDoris3Alter() {
+    Literal<?> defaultValue = Literals.of("owner's \"value\"\\path", 
Types.VarCharType.of(255));
+    Assertions.assertEquals(
+        "\"owner's \\\"value\\\"\\\\\\\\path\"",
+        CONVERTER.fromGravitinoForAddColumn(defaultValue, true));
+  }
+
+  @Test
+  public void testSingleQuotedLegacyLiteralPreservesAdjacentDoubleQuotes() {
+    Literal<?> defaultValue =
+        Literals.of("owner's a\"\"b \"value\"\\path", 
Types.VarCharType.of(255));
+
+    Assertions.assertEquals(
+        "'owner''s a\"\"b \"value\"\\\\path'",
+        CONVERTER.fromGravitinoForAddColumn(defaultValue, false, true));
+    Assertions.assertEquals(
+        "'owner''s a\"\"b \"value\"\\\\\\\\path'",
+        CONVERTER.fromGravitinoForAddColumn(defaultValue, true, true));
+  }
+
+  @Test
+  public void testRepeatedApostrophesRemainLiteralDuringWriteAndRead() {
+    Literal<?> defaultValue = Literals.of("owner''s", 
Types.VarCharType.of(255));
+    JdbcTypeConverter.JdbcTypeBean varcharType =
+        new JdbcTypeConverter.JdbcTypeBean(JdbcTypeConverter.VARCHAR);
+    varcharType.setColumnSize(255);
+
+    Assertions.assertEquals(
+        "'owner''''s'", CONVERTER.fromGravitinoForAddColumn(defaultValue, 
false, true));
+    Assertions.assertEquals(
+        defaultValue, CONVERTER.toGravitino(varcharType, "owner''s", false, 
false));
+  }
+
+  @Test
+  public void testUnescapeStringLikeDefaultValues() {
+    JdbcTypeConverter.JdbcTypeBean varcharType =
+        new JdbcTypeConverter.JdbcTypeBean(JdbcTypeConverter.VARCHAR);
+    varcharType.setColumnSize(255);
+    JdbcTypeConverter.JdbcTypeBean charType =
+        new JdbcTypeConverter.JdbcTypeBean(DorisTypeConverter.CHAR);
+    charType.setColumnSize(32);
+
+    Assertions.assertEquals(
+        Literals.of("owner's\\value", Types.VarCharType.of(255)),
+        CONVERTER.toGravitino(varcharType, "owner's\\\\value", false, false));
+    Assertions.assertEquals(
+        Literals.of("owner's\\value", Types.FixedCharType.of(32)),
+        CONVERTER.toGravitino(charType, "owner's\\\\value", false, false));
+    Assertions.assertEquals(
+        Literals.stringLiteral("owner's\\value"),
+        CONVERTER.toGravitino(
+            new JdbcTypeConverter.JdbcTypeBean(JdbcTypeConverter.TEXT),
+            "owner's\\\\value",
+            false,
+            false));
+    Assertions.assertEquals(
+        Literals.of("owner's \"value\"\\path", Types.VarCharType.of(255)),
+        CONVERTER.toGravitino(varcharType, "owner's \\\"value\\\"\\\\path", 
false, false));
+    Assertions.assertEquals(
+        Literals.of("owner's \"value\"\\path", Types.VarCharType.of(255)),
+        CONVERTER.toGravitino(varcharType, "owner's \"value\"\\path", false, 
false));
+    Assertions.assertEquals(
+        Literals.of("owner's a\"\"b \"value\"\\path", 
Types.VarCharType.of(255)),
+        CONVERTER.toGravitino(varcharType, "owner's a\"\"b \"value\"\\\\path", 
false, false));
+  }
+}
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 fc5e250f90..a7c73db6e4 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,286 @@ public class CatalogDoris3xIT extends BaseIT {
         .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
   }
 
+  @Test
+  void testAddColumnPreservesDefaultValue() throws SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(
+            schemaName, 
GravitinoITUtils.genRandomName("t_add_column_preserves_default"));
+    String defaultedColumnName = "defaulted_col";
+    String nullableDefaultColumnName = "nullable_default_col";
+    String requestedDefaultValue = "owner's a\"\"b \"value\"\\path";
+
+    tc.createTable(
+        tid,
+        basicColumns(),
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    tc.alterTable(
+        tid,
+        TableChange.addColumn(
+            new String[] {defaultedColumnName},
+            Types.VarCharType.of(255),
+            "defaulted column",
+            TableChange.ColumnPosition.defaultPos(),
+            false,
+            false,
+            Literals.of(requestedDefaultValue, Types.VarCharType.of(255))));
+
+    String diagnosticQualifiedTableName = schemaName + "." + tid.name();
+    String[] rawDefaults = new String[3];
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              try (Connection connection =
+                      DriverManager.getConnection(
+                          jdbcUrl, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+                  Statement statement = connection.createStatement();
+                  ResultSet columns =
+                      connection
+                          .getMetaData()
+                          .getColumns(schemaName, null, tid.name(), 
defaultedColumnName)) {
+                String jdbcColumnDefault = null;
+                while (columns.next()) {
+                  if 
(defaultedColumnName.equals(columns.getString("COLUMN_NAME"))) {
+                    jdbcColumnDefault = columns.getString("COLUMN_DEF");
+                    break;
+                  }
+                }
+                assertNotNull(jdbcColumnDefault, "JDBC COLUMN_DEF was not 
returned");
+                rawDefaults[0] = jdbcColumnDefault;
+
+                try (ResultSet showColumns =
+                    statement.executeQuery(
+                        "SHOW FULL COLUMNS FROM " + 
diagnosticQualifiedTableName)) {
+                  while (showColumns.next()) {
+                    if 
(defaultedColumnName.equals(showColumns.getString("Field"))) {
+                      rawDefaults[1] = showColumns.getString("Default");
+                      break;
+                    }
+                  }
+                }
+                assertNotNull(
+                    rawDefaults[1], "SHOW FULL COLUMNS did not return the 
string default");
+
+                String infoSchemaQuery =
+                    String.format(
+                        "SELECT COLUMN_DEFAULT FROM information_schema.columns 
"
+                            + "WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' 
"
+                            + "AND COLUMN_NAME = '%s'",
+                        schemaName, tid.name(), defaultedColumnName);
+                try (ResultSet infoSchema = 
statement.executeQuery(infoSchemaQuery)) {
+                  assertTrue(infoSchema.next(), "information_schema.columns 
row was not returned");
+                  rawDefaults[2] = infoSchema.getString("COLUMN_DEFAULT");
+                }
+              }
+            });
+
+    String metadataDiagnostic =
+        String.format(
+            "JDBC COLUMN_DEF=[%s], SHOW FULL COLUMNS Default=[%s], "
+                + "information_schema.COLUMN_DEFAULT=[%s]",
+            rawDefaults[0], rawDefaults[1], rawDefaults[2]);
+    DorisContainer.LOG.info("Doris default metadata diagnostic: {}", 
metadataDiagnostic);
+    String[] insertedDefaultValue = new String[1];
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format(
+              "INSERT INTO %s (%s, %s) VALUES (102, 'data')",
+              diagnosticQualifiedTableName, colName1, colName2));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format(
+                  "SELECT %s FROM %s WHERE %s = 102",
+                  defaultedColumnName, diagnosticQualifiedTableName, 
colName1))) {
+        assertTrue(resultSet.next());
+        insertedDefaultValue[0] = resultSet.getString(1);
+        assertFalse(resultSet.next());
+      }
+    }
+    assertEquals(
+        requestedDefaultValue,
+        insertedDefaultValue[0],
+        metadataDiagnostic + "; inserted value=[" + insertedDefaultValue[0] + 
"]");
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column addedColumn = findColumn(tc.loadTable(tid), 
defaultedColumnName);
+              assertEquals(Types.VarCharType.of(255), addedColumn.dataType());
+              assertFalse(addedColumn.nullable());
+              assertEquals("defaulted column", addedColumn.comment());
+              assertEquals(
+                  Literals.of(requestedDefaultValue, 
Types.VarCharType.of(255)),
+                  addedColumn.defaultValue(),
+                  metadataDiagnostic + "; inserted value=[" + 
insertedDefaultValue[0] + "]");
+            });
+
+    tc.alterTable(
+        tid,
+        TableChange.addColumn(
+            new String[] {nullableDefaultColumnName},
+            Types.IntegerType.get(),
+            "nullable default column",
+            TableChange.ColumnPosition.defaultPos(),
+            true,
+            false,
+            Literals.integerLiteral(9)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column addedColumn = findColumn(tc.loadTable(tid), 
nullableDefaultColumnName);
+              assertEquals(Types.IntegerType.get(), addedColumn.dataType());
+              assertTrue(addedColumn.nullable());
+              assertEquals("nullable default column", addedColumn.comment());
+              assertEquals(Literals.integerLiteral(9), 
addedColumn.defaultValue());
+            });
+
+    String qualifiedTableName = String.format("`%s`.`%s`", schemaName, 
tid.name());
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format(
+              "INSERT INTO %s (`%s`, `%s`) VALUES (101, 'data')",
+              qualifiedTableName, colName1, colName2));
+
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format(
+                  "SELECT `%s`, `%s` FROM %s WHERE `%s` = 101",
+                  defaultedColumnName, nullableDefaultColumnName, 
qualifiedTableName, colName1))) {
+        assertTrue(resultSet.next());
+        assertEquals(requestedDefaultValue, resultSet.getString(1));
+        assertEquals(9, resultSet.getInt(2));
+        assertFalse(resultSet.wasNull());
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  void testAddColumnBackslashDefaultWithLimitedDorisPrivileges() throws 
SQLException {
+    TableCatalog rootTableCatalog = catalog.asTableCatalog();
+    NameIdentifier tableIdentifier =
+        NameIdentifier.of(
+            schemaName, 
GravitinoITUtils.genRandomName("t_add_column_limited_privileges"));
+    String defaultedColumnName = "defaulted_col";
+    String requestedDefaultValue = "owner's a\"\"b \"value\"\\path";
+    String userName = 
GravitinoITUtils.genRandomName("doris_add_column_user").replace('-', '_');
+    String userPassword = userName + "_test_password";
+    String limitedCatalogName = 
GravitinoITUtils.genRandomName("doris_limited_catalog");
+    Catalog limitedCatalog = null;
+    boolean userCreated = false;
+
+    rootTableCatalog.createTable(
+        tableIdentifier,
+        basicColumns(),
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    try {
+      try (Connection connection =
+              DriverManager.getConnection(
+                  jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+          Statement statement = connection.createStatement()) {
+        statement.execute(
+            String.format("CREATE USER '%s'@'%%' IDENTIFIED BY '%s'", 
userName, userPassword));
+        userCreated = true;
+        statement.execute(
+            String.format(
+                "GRANT SELECT_PRIV, ALTER_PRIV ON `%s`.* TO '%s'@'%%'", 
schemaName, userName));
+      }
+
+      Map<String, String> limitedCatalogProperties = Maps.newHashMap();
+      limitedCatalogProperties.put(JdbcConfig.JDBC_URL.getKey(), jdbcUrl);
+      limitedCatalogProperties.put(JdbcConfig.JDBC_DRIVER.getKey(), 
DRIVER_CLASS_NAME);
+      limitedCatalogProperties.put(JdbcConfig.USERNAME.getKey(), userName);
+      limitedCatalogProperties.put(JdbcConfig.PASSWORD.getKey(), userPassword);
+      limitedCatalog =
+          metalake.createCatalog(
+              limitedCatalogName,
+              Catalog.Type.RELATIONAL,
+              PROVIDER,
+              "Doris catalog with limited table privileges",
+              limitedCatalogProperties);
+
+      limitedCatalog
+          .asTableCatalog()
+          .alterTable(
+              tableIdentifier,
+              TableChange.addColumn(
+                  new String[] {defaultedColumnName},
+                  Types.VarCharType.of(255),
+                  Literals.of(requestedDefaultValue, 
Types.VarCharType.of(255))));
+
+      Awaitility.await()
+          .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+          .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+          .untilAsserted(
+              () ->
+                  assertEquals(
+                      Literals.of(requestedDefaultValue, 
Types.VarCharType.of(255)),
+                      findColumn(rootTableCatalog.loadTable(tableIdentifier), 
defaultedColumnName)
+                          .defaultValue()));
+
+      String qualifiedTableName = String.format("`%s`.`%s`", schemaName, 
tableIdentifier.name());
+      try (Connection connection =
+              DriverManager.getConnection(
+                  jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+          Statement statement = connection.createStatement()) {
+        statement.executeUpdate(
+            String.format(
+                "INSERT INTO %s (`%s`, `%s`) VALUES (301, 'limited')",
+                qualifiedTableName, colName1, colName2));
+        try (ResultSet resultSet =
+            statement.executeQuery(
+                String.format(
+                    "SELECT `%s` FROM %s WHERE `%s` = 301",
+                    defaultedColumnName, qualifiedTableName, colName1))) {
+          assertTrue(resultSet.next());
+          assertEquals(requestedDefaultValue, resultSet.getString(1));
+          assertFalse(resultSet.next());
+        }
+      }
+    } finally {
+      try {
+        if (limitedCatalog != null) {
+          metalake.dropCatalog(limitedCatalogName, true);
+        }
+      } finally {
+        if (userCreated) {
+          try (Connection connection =
+                  DriverManager.getConnection(
+                      jdbcUrl, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+              Statement statement = connection.createStatement()) {
+            statement.execute(String.format("DROP USER '%s'@'%%'", userName));
+          }
+        }
+      }
+    }
+  }
+
   @Test
   void testDeleteMissingIndexIfExists() {
     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 c54154e511..dff696f912 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,180 @@ public class CatalogDoris4xIT extends BaseIT {
         .untilAsserted(() -> assertEquals(0, 
tc.loadTable(tid).index().length));
   }
 
+  @Test
+  void testAddColumnPreservesDefaultValue() throws SQLException {
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid =
+        NameIdentifier.of(
+            schemaName, 
GravitinoITUtils.genRandomName("t_add_column_preserves_default"));
+    String defaultedColumnName = "defaulted_col";
+    String nullableDefaultColumnName = "nullable_default_col";
+    String requestedDefaultValue = "owner's a\"\"b \"value\"\\path";
+
+    tc.createTable(
+        tid,
+        basicColumns(),
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    tc.alterTable(
+        tid,
+        TableChange.addColumn(
+            new String[] {defaultedColumnName},
+            Types.VarCharType.of(255),
+            "defaulted column",
+            TableChange.ColumnPosition.defaultPos(),
+            false,
+            false,
+            Literals.of(requestedDefaultValue, Types.VarCharType.of(255))));
+
+    String diagnosticQualifiedTableName = schemaName + "." + tid.name();
+    String[] rawDefaults = new String[3];
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              try (Connection connection =
+                      DriverManager.getConnection(
+                          jdbcUrl, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+                  Statement statement = connection.createStatement();
+                  ResultSet columns =
+                      connection
+                          .getMetaData()
+                          .getColumns(schemaName, null, tid.name(), 
defaultedColumnName)) {
+                String jdbcColumnDefault = null;
+                while (columns.next()) {
+                  if 
(defaultedColumnName.equals(columns.getString("COLUMN_NAME"))) {
+                    jdbcColumnDefault = columns.getString("COLUMN_DEF");
+                    break;
+                  }
+                }
+                assertNotNull(jdbcColumnDefault, "JDBC COLUMN_DEF was not 
returned");
+                rawDefaults[0] = jdbcColumnDefault;
+
+                try (ResultSet showColumns =
+                    statement.executeQuery(
+                        "SHOW FULL COLUMNS FROM " + 
diagnosticQualifiedTableName)) {
+                  while (showColumns.next()) {
+                    if 
(defaultedColumnName.equals(showColumns.getString("Field"))) {
+                      rawDefaults[1] = showColumns.getString("Default");
+                      break;
+                    }
+                  }
+                }
+                assertNotNull(
+                    rawDefaults[1], "SHOW FULL COLUMNS did not return the 
string default");
+
+                String infoSchemaQuery =
+                    String.format(
+                        "SELECT COLUMN_DEFAULT FROM information_schema.columns 
"
+                            + "WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' 
"
+                            + "AND COLUMN_NAME = '%s'",
+                        schemaName, tid.name(), defaultedColumnName);
+                try (ResultSet infoSchema = 
statement.executeQuery(infoSchemaQuery)) {
+                  assertTrue(infoSchema.next(), "information_schema.columns 
row was not returned");
+                  rawDefaults[2] = infoSchema.getString("COLUMN_DEFAULT");
+                }
+              }
+            });
+
+    String metadataDiagnostic =
+        String.format(
+            "JDBC COLUMN_DEF=[%s], SHOW FULL COLUMNS Default=[%s], "
+                + "information_schema.COLUMN_DEFAULT=[%s]",
+            rawDefaults[0], rawDefaults[1], rawDefaults[2]);
+    DorisContainer.LOG.info("Doris default metadata diagnostic: {}", 
metadataDiagnostic);
+    String[] insertedDefaultValue = new String[1];
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format(
+              "INSERT INTO %s (%s, %s) VALUES (102, 'data')",
+              diagnosticQualifiedTableName, colName1, colName2));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format(
+                  "SELECT %s FROM %s WHERE %s = 102",
+                  defaultedColumnName, diagnosticQualifiedTableName, 
colName1))) {
+        assertTrue(resultSet.next());
+        insertedDefaultValue[0] = resultSet.getString(1);
+        assertFalse(resultSet.next());
+      }
+    }
+    assertEquals(
+        requestedDefaultValue,
+        insertedDefaultValue[0],
+        metadataDiagnostic + "; inserted value=[" + insertedDefaultValue[0] + 
"]");
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column addedColumn = findColumn(tc.loadTable(tid), 
defaultedColumnName);
+              assertEquals(Types.VarCharType.of(255), addedColumn.dataType());
+              assertFalse(addedColumn.nullable());
+              assertEquals("defaulted column", addedColumn.comment());
+              assertEquals(
+                  Literals.of(requestedDefaultValue, 
Types.VarCharType.of(255)),
+                  addedColumn.defaultValue(),
+                  metadataDiagnostic + "; inserted value=[" + 
insertedDefaultValue[0] + "]");
+            });
+
+    tc.alterTable(
+        tid,
+        TableChange.addColumn(
+            new String[] {nullableDefaultColumnName},
+            Types.IntegerType.get(),
+            "nullable default column",
+            TableChange.ColumnPosition.defaultPos(),
+            true,
+            false,
+            Literals.integerLiteral(9)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column addedColumn = findColumn(tc.loadTable(tid), 
nullableDefaultColumnName);
+              assertEquals(Types.IntegerType.get(), addedColumn.dataType());
+              assertTrue(addedColumn.nullable());
+              assertEquals("nullable default column", addedColumn.comment());
+              assertEquals(Literals.integerLiteral(9), 
addedColumn.defaultValue());
+            });
+
+    String qualifiedTableName = String.format("`%s`.`%s`", schemaName, 
tid.name());
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format(
+              "INSERT INTO %s (`%s`, `%s`) VALUES (101, 'data')",
+              qualifiedTableName, colName1, colName2));
+
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format(
+                  "SELECT `%s`, `%s` FROM %s WHERE `%s` = 101",
+                  defaultedColumnName, nullableDefaultColumnName, 
qualifiedTableName, colName1))) {
+        assertTrue(resultSet.next());
+        assertEquals(requestedDefaultValue, resultSet.getString(1));
+        assertEquals(9, resultSet.getInt(2));
+        assertFalse(resultSet.wasNull());
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
   @Test
   void testDeleteMissingIndexIfExists() {
     TableCatalog tc = catalog.asTableCatalog();
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDorisIT.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDorisIT.java
index 8b9af56ca2..7dd97e9f5b 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDorisIT.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDorisIT.java
@@ -36,6 +36,8 @@ import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.Arrays;
@@ -720,6 +722,195 @@ public class CatalogDorisIT extends BaseIT {
                     "true", 
tableCatalog.loadTable(tableIdentifier).properties().get("in_memory")));
   }
 
+  @Test
+  void testAddColumnPreservesDefaultValue() throws SQLException {
+    String defaultedColumnName = "defaulted_col";
+    String nullableDefaultColumnName = "nullable_default_col";
+    String requestedDefaultValue = "owner's a\"\"b \"value\"\\path";
+    NameIdentifier tableIdentifier =
+        NameIdentifier.of(
+            schemaName, 
GravitinoITUtils.genRandomName("test_add_column_preserves_default"));
+    TableCatalog tableCatalog = catalog.asTableCatalog();
+    tableCatalog.createTable(
+        tableIdentifier,
+        createColumns(),
+        table_comment,
+        createTableProperties(),
+        Transforms.EMPTY_TRANSFORM,
+        createDistribution(),
+        null,
+        Indexes.EMPTY_INDEXES);
+
+    tableCatalog.alterTable(
+        tableIdentifier,
+        TableChange.addColumn(
+            new String[] {defaultedColumnName},
+            Types.VarCharType.of(255),
+            "defaulted column",
+            TableChange.ColumnPosition.defaultPos(),
+            false,
+            false,
+            Literals.of(requestedDefaultValue, Types.VarCharType.of(255))));
+
+    String diagnosticQualifiedTableName = schemaName + "." + 
tableIdentifier.name();
+    String[] rawDefaults = new String[3];
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              try (Connection connection =
+                      DriverManager.getConnection(
+                          jdbcUrl, DorisContainer.USER_NAME, 
DorisContainer.PASSWORD);
+                  Statement statement = connection.createStatement();
+                  ResultSet columns =
+                      connection
+                          .getMetaData()
+                          .getColumns(
+                              schemaName, null, tableIdentifier.name(), 
defaultedColumnName)) {
+                String jdbcColumnDefault = null;
+                while (columns.next()) {
+                  if 
(defaultedColumnName.equals(columns.getString("COLUMN_NAME"))) {
+                    jdbcColumnDefault = columns.getString("COLUMN_DEF");
+                    break;
+                  }
+                }
+                Assertions.assertNotNull(jdbcColumnDefault, "JDBC COLUMN_DEF 
was not returned");
+                rawDefaults[0] = jdbcColumnDefault;
+
+                try (ResultSet showColumns =
+                    statement.executeQuery(
+                        "SHOW FULL COLUMNS FROM " + 
diagnosticQualifiedTableName)) {
+                  while (showColumns.next()) {
+                    if 
(defaultedColumnName.equals(showColumns.getString("Field"))) {
+                      rawDefaults[1] = showColumns.getString("Default");
+                      break;
+                    }
+                  }
+                }
+                Assertions.assertNotNull(
+                    rawDefaults[1], "SHOW FULL COLUMNS did not return the 
string default");
+
+                String infoSchemaQuery =
+                    String.format(
+                        "SELECT COLUMN_DEFAULT FROM information_schema.columns 
"
+                            + "WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' 
"
+                            + "AND COLUMN_NAME = '%s'",
+                        schemaName, tableIdentifier.name(), 
defaultedColumnName);
+                try (ResultSet infoSchema = 
statement.executeQuery(infoSchemaQuery)) {
+                  Assertions.assertTrue(
+                      infoSchema.next(), "information_schema.columns row was 
not returned");
+                  rawDefaults[2] = infoSchema.getString("COLUMN_DEFAULT");
+                }
+              }
+            });
+
+    String metadataDiagnostic =
+        String.format(
+            "JDBC COLUMN_DEF=[%s], SHOW FULL COLUMNS Default=[%s], "
+                + "information_schema.COLUMN_DEFAULT=[%s]",
+            rawDefaults[0], rawDefaults[1], rawDefaults[2]);
+    DorisContainer.LOG.info("Doris default metadata diagnostic: {}", 
metadataDiagnostic);
+    String[] insertedDefaultValue = new String[1];
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format(
+              "INSERT INTO %s (%s, %s, %s, %s) " + "VALUES (102, 'data-1', 
'data-2', '2024-01-01')",
+              diagnosticQualifiedTableName,
+              DORIS_COL_NAME1,
+              DORIS_COL_NAME2,
+              DORIS_COL_NAME3,
+              DORIS_COL_NAME4));
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format(
+                  "SELECT %s FROM %s WHERE %s = 102",
+                  defaultedColumnName, diagnosticQualifiedTableName, 
DORIS_COL_NAME1))) {
+        Assertions.assertTrue(resultSet.next());
+        insertedDefaultValue[0] = resultSet.getString(1);
+        Assertions.assertFalse(resultSet.next());
+      }
+    }
+    Assertions.assertEquals(
+        requestedDefaultValue,
+        insertedDefaultValue[0],
+        metadataDiagnostic + "; inserted value=[" + insertedDefaultValue[0] + 
"]");
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column addedColumn =
+                  findColumn(tableCatalog.loadTable(tableIdentifier), 
defaultedColumnName);
+              Assertions.assertEquals(Types.VarCharType.of(255), 
addedColumn.dataType());
+              Assertions.assertFalse(addedColumn.nullable());
+              Assertions.assertEquals("defaulted column", 
addedColumn.comment());
+              Assertions.assertEquals(
+                  Literals.of(requestedDefaultValue, 
Types.VarCharType.of(255)),
+                  addedColumn.defaultValue(),
+                  metadataDiagnostic + "; inserted value=[" + 
insertedDefaultValue[0] + "]");
+            });
+
+    tableCatalog.alterTable(
+        tableIdentifier,
+        TableChange.addColumn(
+            new String[] {nullableDefaultColumnName},
+            Types.IntegerType.get(),
+            "nullable default column",
+            TableChange.ColumnPosition.defaultPos(),
+            true,
+            false,
+            Literals.integerLiteral(9)));
+
+    Awaitility.await()
+        .atMost(MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(WAIT_INTERVAL_IN_SECONDS, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              Column addedColumn =
+                  findColumn(tableCatalog.loadTable(tableIdentifier), 
nullableDefaultColumnName);
+              Assertions.assertEquals(Types.IntegerType.get(), 
addedColumn.dataType());
+              Assertions.assertTrue(addedColumn.nullable());
+              Assertions.assertEquals("nullable default column", 
addedColumn.comment());
+              Assertions.assertEquals(Literals.integerLiteral(9), 
addedColumn.defaultValue());
+            });
+
+    String qualifiedTableName = String.format("`%s`.`%s`", schemaName, 
tableIdentifier.name());
+    try (Connection connection =
+            DriverManager.getConnection(
+                jdbcUrl, DorisContainer.USER_NAME, DorisContainer.PASSWORD);
+        Statement statement = connection.createStatement()) {
+      statement.executeUpdate(
+          String.format(
+              "INSERT INTO %s (`%s`, `%s`, `%s`, `%s`) "
+                  + "VALUES (101, 'data-1', 'data-2', '2024-01-01')",
+              qualifiedTableName,
+              DORIS_COL_NAME1,
+              DORIS_COL_NAME2,
+              DORIS_COL_NAME3,
+              DORIS_COL_NAME4));
+
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              String.format(
+                  "SELECT `%s`, `%s` FROM %s WHERE `%s` = 101",
+                  defaultedColumnName,
+                  nullableDefaultColumnName,
+                  qualifiedTableName,
+                  DORIS_COL_NAME1))) {
+        Assertions.assertTrue(resultSet.next());
+        Assertions.assertEquals(requestedDefaultValue, resultSet.getString(1));
+        Assertions.assertEquals(9, resultSet.getInt(2));
+        Assertions.assertFalse(resultSet.wasNull());
+        Assertions.assertFalse(resultSet.next());
+      }
+    }
+  }
+
   @Test
   void testDorisIndex() {
     String tableName = GravitinoITUtils.genRandomName("test_add_index");
@@ -1355,4 +1546,11 @@ public class CatalogDorisIT extends BaseIT {
     assertPartition(Partitions.list("p1", p1Values, Collections.emptyMap()), 
partitions.get("p1"));
     assertPartition(Partitions.list("p2", p2Values, Collections.emptyMap()), 
partitions.get("p2"));
   }
+
+  private Column findColumn(Table table, String columnName) {
+    return Arrays.stream(table.columns())
+        .filter(column -> column.name().equals(columnName))
+        .findFirst()
+        .orElseThrow(() -> new AssertionError("Column not found: " + 
columnName));
+  }
 }
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 164e9bb6b1..cf8fe7f781 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
@@ -20,6 +20,8 @@ package org.apache.gravitino.catalog.doris.operation;
 
 import static 
org.apache.gravitino.catalog.doris.DorisTablePropertiesMetadata.REPLICATION_ALLOCATION;
 import static 
org.apache.gravitino.catalog.doris.DorisTablePropertiesMetadata.REPLICATION_FACTOR;
+import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
+import static 
org.apache.gravitino.rel.Column.DEFAULT_VALUE_OF_CURRENT_TIMESTAMP;
 
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -33,15 +35,16 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import javax.sql.DataSource;
+import 
org.apache.gravitino.catalog.doris.converter.DorisColumnDefaultValueConverter;
 import org.apache.gravitino.catalog.doris.converter.DorisTypeConverter;
 import org.apache.gravitino.catalog.jdbc.JdbcColumn;
 import org.apache.gravitino.catalog.jdbc.JdbcTable;
-import 
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
 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.distributions.Distribution;
 import org.apache.gravitino.rel.expressions.distributions.Distributions;
+import org.apache.gravitino.rel.expressions.literals.Literal;
 import org.apache.gravitino.rel.expressions.literals.Literals;
 import org.apache.gravitino.rel.expressions.transforms.Transforms;
 import org.apache.gravitino.rel.indexes.Index;
@@ -58,26 +61,15 @@ public class TestDorisTableOperationsSqlGeneration {
         
JdbcTable.builder().withName("test_table").withIndexes(Indexes.EMPTY_INDEXES).build();
 
     public TestableDorisTableOperations() {
+      this("doris-3.0.6.2-rc01-910c4249c5");
+    }
+
+    public TestableDorisTableOperations(String dorisVersion) {
       super.exceptionMapper = new JdbcExceptionConverter();
       super.typeConverter = new DorisTypeConverter();
-      super.columnDefaultValueConverter = new 
JdbcColumnDefaultValueConverter();
+      super.columnDefaultValueConverter = new 
DorisColumnDefaultValueConverter();
       try {
-        // Set up a mock DataSource for validateAutoIncrementVersion
-        // Uses SHOW FRONTENDS to get the actual Doris version (not MySQL 
protocol version)
-        DataSource mockDataSource = Mockito.mock(DataSource.class);
-        Connection mockConnection = Mockito.mock(Connection.class);
-        Statement mockStatement = Mockito.mock(Statement.class);
-        ResultSet mockResultSet = Mockito.mock(ResultSet.class);
-        ResultSetMetaData mockMetaData = Mockito.mock(ResultSetMetaData.class);
-        
Mockito.when(mockDataSource.getConnection()).thenReturn(mockConnection);
-        
Mockito.when(mockConnection.createStatement()).thenReturn(mockStatement);
-        Mockito.when(mockStatement.executeQuery("SHOW 
FRONTENDS")).thenReturn(mockResultSet);
-        Mockito.when(mockResultSet.getMetaData()).thenReturn(mockMetaData);
-        Mockito.when(mockMetaData.getColumnCount()).thenReturn(1);
-        Mockito.when(mockMetaData.getColumnLabel(1)).thenReturn("Version");
-        Mockito.when(mockResultSet.next()).thenReturn(true);
-        
Mockito.when(mockResultSet.getString(1)).thenReturn("doris-3.0.6.2-rc01-910c4249c5");
-        super.dataSource = mockDataSource;
+        super.dataSource = mockVersionDataSource(dorisVersion);
       } catch (Exception e) {
         throw new RuntimeException(e);
       }
@@ -156,10 +148,10 @@ public class TestDorisTableOperationsSqlGeneration {
         .appendNecessaryProperties(Mockito.anyMap());
 
     String sql = mockOps.createTableSql(tableName, new JdbcColumn[] {col1}, 
distribution);
-    JdbcColumnDefaultValueConverter converter = new 
JdbcColumnDefaultValueConverter();
+    DorisColumnDefaultValueConverter converter = new 
DorisColumnDefaultValueConverter();
     Assertions.assertTrue(
         sql.contains("DEFAULT " + 
converter.fromGravitino(col1.defaultValue())),
-        "Should contain DEFAULT '' but was: " + sql);
+        "Should contain an empty DEFAULT value but was: " + sql);
   }
 
   @Test
@@ -182,7 +174,7 @@ public class TestDorisTableOperationsSqlGeneration {
         .appendNecessaryProperties(Mockito.anyMap());
 
     String sql = mockOps.createTableSql(tableName, new JdbcColumn[] {col1}, 
distribution);
-    JdbcColumnDefaultValueConverter converter = new 
JdbcColumnDefaultValueConverter();
+    DorisColumnDefaultValueConverter converter = new 
DorisColumnDefaultValueConverter();
     Assertions.assertTrue(
         sql.contains("DEFAULT " + 
converter.fromGravitino(col1.defaultValue())),
         "Should contain DEFAULT value but was: " + sql);
@@ -234,6 +226,258 @@ public class TestDorisTableOperationsSqlGeneration {
     Assertions.assertTrue(alterSql.contains("COMMENT 'owner\\\\''s 
\"comment\"; --'"), alterSql);
   }
 
+  @Test
+  public void testAddColumnDefaultValuesInGeneratedSql() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+
+    String numericDefaultSql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(
+                new String[] {"col2"},
+                Types.IntegerType.get(),
+                "comment",
+                TableChange.ColumnPosition.after("col1"),
+                false,
+                false,
+                Literals.integerLiteral(7)));
+    String nullDefaultSql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(
+                new String[] {"col2"},
+                Types.IntegerType.get(),
+                null,
+                TableChange.ColumnPosition.defaultPos(),
+                true,
+                false,
+                Literals.NULL));
+    String currentTimestampSql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(
+                new String[] {"created_at"},
+                Types.TimestampType.withoutTimeZone(),
+                DEFAULT_VALUE_OF_CURRENT_TIMESTAMP));
+    String unsetDefaultSql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(
+                new String[] {"col2"},
+                Types.IntegerType.get(),
+                null,
+                TableChange.ColumnPosition.defaultPos(),
+                true,
+                false,
+                DEFAULT_VALUE_NOT_SET));
+
+    Assertions.assertEquals(
+        "ALTER TABLE `test_table`\n"
+            + "ADD COLUMN `col2` int NOT NULL DEFAULT \"7\" COMMENT 'comment' 
AFTER `col1`;",
+        numericDefaultSql);
+    Assertions.assertEquals(
+        "ALTER TABLE `test_table`\nADD COLUMN `col2` int DEFAULT NULL ;", 
nullDefaultSql);
+    Assertions.assertEquals(
+        "ALTER TABLE `test_table`\n"
+            + "ADD COLUMN `created_at` datetime DEFAULT CURRENT_TIMESTAMP ;",
+        currentTimestampSql);
+    Assertions.assertEquals("ALTER TABLE `test_table`\nADD COLUMN `col2` int 
;", unsetDefaultSql);
+  }
+
+  @Test
+  public void testAddColumnEscapesStringDefaultValue() {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    Literal<?> defaultValue = Literals.of("owner's \"value\"\\path", 
Types.VarCharType.of(255));
+
+    String sql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(new String[] {"col2"}, 
Types.VarCharType.of(255), defaultValue));
+
+    Assertions.assertEquals(
+        "ALTER TABLE `test_table`\n"
+            + "ADD COLUMN `col2` varchar(255) DEFAULT "
+            + new DorisColumnDefaultValueConverter()
+                .fromGravitinoForAddColumn(defaultValue, true, true)
+            + " ;",
+        sql);
+  }
+
+  @Test
+  public void testAddColumnKeepsStandardBackslashEscapingOutsideDoris3() {
+    for (String version : new String[] {"doris-1.2.2-release", 
"doris-4.0.6-release-a851eab4"}) {
+      TestableDorisTableOperations ops = new 
TestableDorisTableOperations(version);
+      Literal<?> defaultValue = Literals.of("owner's \"value\"\\path", 
Types.VarCharType.of(255));
+      String sql =
+          ops.alterTableSql(
+              "test_table",
+              TableChange.addColumn(
+                  new String[] {"col2"}, Types.VarCharType.of(255), 
defaultValue));
+
+      boolean useSingleQuoteDelimiter = version.startsWith("doris-1.2.");
+      Assertions.assertEquals(
+          "ALTER TABLE `test_table`\n"
+              + "ADD COLUMN `col2` varchar(255) DEFAULT "
+              + new DorisColumnDefaultValueConverter()
+                  .fromGravitinoForAddColumn(defaultValue, false, 
useSingleQuoteDelimiter)
+              + " ;",
+          sql);
+    }
+  }
+
+  @Test
+  public void testAddColumnDoesNotQueryVersionWithoutBackslashes() throws 
Exception {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource unavailableDataSource = Mockito.mock(DataSource.class);
+    Mockito.when(unavailableDataSource.getConnection())
+        .thenThrow(new SQLException("Version query should not run"));
+    ops.setDataSource(unavailableDataSource);
+
+    String sql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(
+                new String[] {"col2"},
+                Types.VarCharType.of(255),
+                Literals.of("owner's value", Types.VarCharType.of(255))));
+
+    Assertions.assertTrue(sql.contains("DEFAULT \"owner's value\""), sql);
+  }
+
+  @Test
+  public void testAddColumnQueriesVersionForAdjacentDoubleQuotes() throws 
Exception {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource versionDataSource = 
mockVersionDataSource("doris-1.2.7-1-Unknown");
+    ops.setDataSource(versionDataSource);
+
+    String sql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(
+                new String[] {"col2"},
+                Types.VarCharType.of(255),
+                Literals.of("before\"\"after", Types.VarCharType.of(255))));
+
+    Assertions.assertTrue(sql.contains("DEFAULT 'before\"\"after'"), sql);
+    Mockito.verify(versionDataSource).getConnection();
+  }
+
+  @Test
+  public void testAddColumnsQueryVersionOncePerAlterRequest() throws Exception 
{
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource versionDataSource = 
mockVersionDataSource("doris-3.0.6.2-rc01-910c4249c5");
+    ops.setDataSource(versionDataSource);
+
+    ops.alterTableSql(
+        "test_table",
+        TableChange.addColumn(
+            new String[] {"col2"},
+            Types.VarCharType.of(255),
+            Literals.of("first\\value", Types.VarCharType.of(255))),
+        TableChange.addColumn(
+            new String[] {"col3"},
+            Types.VarCharType.of(255),
+            Literals.of("second\\value", Types.VarCharType.of(255))));
+
+    Mockito.verify(versionDataSource, Mockito.times(1)).getConnection();
+  }
+
+  @Test
+  public void 
testAddColumnFallsBackToShowFrontendsWhenTableFunctionIsUnavailable()
+      throws Exception {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource dataSource = Mockito.mock(DataSource.class);
+    Connection connection = Mockito.mock(Connection.class);
+    Statement statement = Mockito.mock(Statement.class);
+    ResultSet resultSet = Mockito.mock(ResultSet.class);
+    ResultSetMetaData metadata = Mockito.mock(ResultSetMetaData.class);
+    Mockito.when(dataSource.getConnection()).thenReturn(connection);
+    Mockito.when(connection.createStatement()).thenReturn(statement);
+    Mockito.when(statement.executeQuery("SELECT Version FROM FRONTENDS()"))
+        .thenThrow(new SQLException("FRONTENDS table-valued function is 
unavailable"));
+    Mockito.when(statement.executeQuery("SHOW 
FRONTENDS")).thenReturn(resultSet);
+    Mockito.when(resultSet.getMetaData()).thenReturn(metadata);
+    Mockito.when(metadata.getColumnCount()).thenReturn(1);
+    Mockito.when(metadata.getColumnLabel(1)).thenReturn("Version");
+    Mockito.when(resultSet.next()).thenReturn(true);
+    Mockito.when(resultSet.getString(1)).thenReturn("doris-1.2.7-1-Unknown");
+    ops.setDataSource(dataSource);
+
+    String sql =
+        ops.alterTableSql(
+            "test_table",
+            TableChange.addColumn(
+                new String[] {"col2"},
+                Types.VarCharType.of(255),
+                Literals.of("value\\path", Types.VarCharType.of(255))));
+
+    Assertions.assertTrue(
+        sql.contains(
+            new DorisColumnDefaultValueConverter()
+                .fromGravitinoForAddColumn(
+                    Literals.of("value\\path", Types.VarCharType.of(255)), 
false, true)),
+        sql);
+    Mockito.verify(statement).executeQuery("SELECT Version FROM FRONTENDS()");
+    Mockito.verify(statement).executeQuery("SHOW FRONTENDS");
+  }
+
+  @Test
+  public void testAddColumnFailsClosedWhenBothVersionQueriesFail() throws 
Exception {
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource dataSource = Mockito.mock(DataSource.class);
+    Connection connection = Mockito.mock(Connection.class);
+    Statement statement = Mockito.mock(Statement.class);
+    SQLException frontendsFailure = new SQLException("FRONTENDS() denied");
+    SQLException showFailure = new SQLException("SHOW FRONTENDS denied");
+    Mockito.when(dataSource.getConnection()).thenReturn(connection);
+    Mockito.when(connection.createStatement()).thenReturn(statement);
+    Mockito.when(statement.executeQuery("SELECT Version FROM FRONTENDS()"))
+        .thenThrow(frontendsFailure);
+    Mockito.when(statement.executeQuery("SHOW 
FRONTENDS")).thenThrow(showFailure);
+    ops.setDataSource(dataSource);
+
+    UnsupportedOperationException exception =
+        Assertions.assertThrows(
+            UnsupportedOperationException.class,
+            () ->
+                ops.alterTableSql(
+                    "test_table",
+                    TableChange.addColumn(
+                        new String[] {"col2"},
+                        Types.VarCharType.of(255),
+                        Literals.of("value\\path", 
Types.VarCharType.of(255)))));
+
+    Assertions.assertTrue(
+        exception.getMessage().contains("ADD COLUMN default literal 
compatibility check"),
+        exception.getMessage());
+    Assertions.assertSame(showFailure, exception.getCause());
+    Assertions.assertArrayEquals(new Throwable[] {frontendsFailure}, 
showFailure.getSuppressed());
+  }
+
+  @Test
+  public void testAddColumnFailsClosedWhenVersionQueryFails() throws Exception 
{
+    TestableDorisTableOperations ops = new TestableDorisTableOperations();
+    DataSource unavailableDataSource = Mockito.mock(DataSource.class);
+    Mockito.when(unavailableDataSource.getConnection())
+        .thenThrow(new SQLException("SHOW FRONTENDS denied"));
+    ops.setDataSource(unavailableDataSource);
+
+    UnsupportedOperationException exception =
+        Assertions.assertThrows(
+            UnsupportedOperationException.class,
+            () ->
+                ops.alterTableSql(
+                    "test_table",
+                    TableChange.addColumn(
+                        new String[] {"col2"},
+                        Types.VarCharType.of(255),
+                        Literals.of("owner's\\value", 
Types.VarCharType.of(255)))));
+
+    Assertions.assertTrue(
+        exception.getMessage().contains("ADD COLUMN default literal 
compatibility check"),
+        exception.getMessage());
+  }
+
   @Test
   public void testCreateTableWithPrimaryKeyIndex() {
     TestableDorisTableOperations ops = new TestableDorisTableOperations();
@@ -1011,4 +1255,18 @@ public class TestDorisTableOperationsSqlGeneration {
 
     return dataSource;
   }
+
+  private static DataSource mockVersionDataSource(String dorisVersion) throws 
Exception {
+    // FRONTENDS() exposes the Doris version instead of the MySQL protocol 
version.
+    DataSource dataSource = Mockito.mock(DataSource.class);
+    Connection connection = Mockito.mock(Connection.class);
+    Statement statement = Mockito.mock(Statement.class);
+    ResultSet resultSet = Mockito.mock(ResultSet.class);
+    Mockito.when(dataSource.getConnection()).thenReturn(connection);
+    Mockito.when(connection.createStatement()).thenReturn(statement);
+    Mockito.when(statement.executeQuery("SELECT Version FROM 
FRONTENDS()")).thenReturn(resultSet);
+    Mockito.when(resultSet.next()).thenReturn(true);
+    Mockito.when(resultSet.getString(1)).thenReturn(dorisVersion);
+    return dataSource;
+  }
 }

Reply via email to