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

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


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new c2478b915d [Cherry-pick to branch-1.3] [#11590] improvement(doris): 
upgrade type system for Doris 3.0+/4.0.x compatibility (#11763) (#11952)
c2478b915d is described below

commit c2478b915d16e57092560001d3d5a4682b4cceb6
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Jul 9 14:22:55 2026 +0800

    [Cherry-pick to branch-1.3] [#11590] improvement(doris): upgrade type 
system for Doris 3.0+/4.0.x compatibility (#11763) (#11952)
    
    **Cherry-pick Information:**
    - Original commit: 70a81aedd35eac1aa572a41e2d80dfc2245afd6a
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Signed-off-by: jiangxt2 <[email protected]>
    Co-authored-by: StormSpirit <[email protected]>
    Co-authored-by: Chang-Tong <[email protected]>
    Co-authored-by: ArtificialIdoit <[email protected]>
    Co-authored-by: cwq222 <[email protected]>
---
 .../doris/converter/DorisTypeConverter.java        | 113 +++++++++++++++++++--
 .../doris/operation/DorisTableOperations.java      |  26 +++--
 .../doris/converter/TestDorisTypeConverter.java    |  60 ++++++++++-
 .../doris/integration/test/CatalogDoris3xIT.java   |  42 ++++++++
 .../doris/integration/test/CatalogDoris4xIT.java   |  42 ++++++++
 .../doris/integration/test/CatalogDorisIT.java     |   1 +
 .../doris/operation/TestDorisTableOperations.java  |  30 +++++-
 7 files changed, 292 insertions(+), 22 deletions(-)

diff --git 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisTypeConverter.java
 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisTypeConverter.java
index 6fa342aef6..df1fd341da 100644
--- 
a/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisTypeConverter.java
+++ 
b/catalogs/catalog-jdbc-doris/src/main/java/org/apache/gravitino/catalog/doris/converter/DorisTypeConverter.java
@@ -18,7 +18,6 @@
  */
 package org.apache.gravitino.catalog.doris.converter;
 
-import java.util.Optional;
 import org.apache.gravitino.catalog.jdbc.converter.JdbcTypeConverter;
 import org.apache.gravitino.rel.types.Type;
 import org.apache.gravitino.rel.types.Types;
@@ -36,10 +35,49 @@ public class DorisTypeConverter extends JdbcTypeConverter {
   static final String DATETIME = "datetime";
   static final String CHAR = "char";
   static final String STRING = "string";
+  static final String BINARY = "binary";
+  static final String VARBINARY = "varbinary";
+  static final String JSON = "json";
+  static final String VARIANT = "variant";
+  static final String IPV4 = "ipv4";
+  static final String IPV6 = "ipv6";
+  static final String LARGEINT = "largeint";
+  static final String BITMAP = "bitmap";
+  static final String HLL = "hll";
+  static final String DATEV2 = "datev2";
+  static final String BIGINT_UNSIGNED = "bigint unsigned";
 
   @Override
   public Type toGravitino(JdbcTypeBean typeBean) {
-    switch (typeBean.getTypeName().toLowerCase()) {
+    String typeName = typeBean.getTypeName().toLowerCase();
+
+    // Extract base type name by stripping parenthesized parameters.
+    // SHOW CREATE TABLE returns full type strings like "int(11)", 
"decimal(10,2)",
+    // but the switch matches base names like "int", "decimal".
+    String baseType = typeName;
+    int parenIndex = typeName.indexOf('(');
+    if (parenIndex > 0) {
+      baseType = typeName.substring(0, parenIndex);
+    }
+
+    // Handle datetime(N) format — parse precision from type string when not 
in typeBean
+    if ("datetime".equals(baseType)) {
+      if (typeBean.getDatetimePrecision() != null) {
+        return 
Types.TimestampType.withoutTimeZone(typeBean.getDatetimePrecision());
+      }
+      if (parenIndex > 0 && typeName.endsWith(")")) {
+        try {
+          String precisionStr = typeName.substring(parenIndex + 1, 
typeName.length() - 1);
+          int precision = Integer.parseInt(precisionStr);
+          return Types.TimestampType.withoutTimeZone(precision);
+        } catch (NumberFormatException e) {
+          // Fall through to default datetime handling
+        }
+      }
+      return Types.TimestampType.withoutTimeZone();
+    }
+
+    switch (baseType) {
       case BOOLEAN:
         return Types.BooleanType.get();
       case TINYINT:
@@ -55,22 +93,71 @@ public class DorisTypeConverter extends JdbcTypeConverter {
       case DOUBLE:
         return Types.DoubleType.get();
       case DECIMAL:
-        return Types.DecimalType.of(typeBean.getColumnSize(), 
typeBean.getScale());
+        return parseTypeParamsOrExternal(
+            typeName, parenIndex, typeBean.getColumnSize(), 
typeBean.getScale());
       case DATE:
+      case DATEV2:
         return Types.DateType.get();
-      case DATETIME:
-        return Optional.ofNullable(typeBean.getDatetimePrecision())
-            .map(Types.TimestampType::withoutTimeZone)
-            .orElseGet(Types.TimestampType::withoutTimeZone);
       case CHAR:
-        return Types.FixedCharType.of(typeBean.getColumnSize());
+        return parseTypeParamsOrExternal(typeName, parenIndex, 
typeBean.getColumnSize(), null);
       case VARCHAR:
-        return Types.VarCharType.of(typeBean.getColumnSize());
+        return parseTypeParamsOrExternal(typeName, parenIndex, 
typeBean.getColumnSize(), null);
       case STRING:
       case TEXT:
         return Types.StringType.get();
+      case BINARY:
+      case VARBINARY:
+        return Types.BinaryType.get();
+        // Explicitly enumerate known Doris-specific types to signal 
intentional coverage,
+        // even though the behaviour is the same as default. BIGINT_UNSIGNED 
is reachable
+        // from SHOW CREATE TABLE parsing; JDBC getColumns() returns "BIGINT" 
for this type.
+      case BIGINT_UNSIGNED:
+      case JSON:
+      case VARIANT:
+      case IPV4:
+      case IPV6:
+      case LARGEINT:
+      case BITMAP:
+      case HLL:
+        return Types.ExternalType.of(typeName);
+      default:
+        return Types.ExternalType.of(typeName);
+    }
+  }
+
+  /**
+   * Parse type parameters from the type string or typeBean values. For 
DECIMAL, returns
+   * DecimalType(p1, p2). For CHAR/VARCHAR, returns FixedCharType/VarCharType 
with default fallback.
+   * Returns ExternalType if the type string is malformed and cannot be parsed.
+   */
+  private static Type parseTypeParamsOrExternal(
+      String typeName, int parenIndex, Integer beanParam1, Integer beanParam2) 
{
+    int p1 = beanParam1 != null ? beanParam1 : 0;
+    int p2 = beanParam2 != null ? beanParam2 : 0;
+    if (p1 == 0 && parenIndex > 0 && typeName.endsWith(")")) {
+      try {
+        String[] parts = typeName.substring(parenIndex + 1, typeName.length() 
- 1).split(",");
+        p1 = Integer.parseInt(parts[0].trim());
+        p2 = parts.length >= 2 ? Integer.parseInt(parts[1].trim()) : 0;
+      } catch (NumberFormatException e) {
+        return Types.ExternalType.of(typeName);
+      }
+    }
+
+    String baseType = parenIndex > 0 ? typeName.substring(0, parenIndex) : 
typeName;
+    switch (baseType) {
+      case DECIMAL:
+        return Types.DecimalType.of(p1, p2);
+      case CHAR:
+        // 1 = minimum valid length for CHAR; fallback when JDBC metadata and 
type string
+        // both lack length info (unlikely in practice)
+        return Types.FixedCharType.of(p1 > 0 ? p1 : 1);
+      case VARCHAR:
+        // 255 = MySQL/Doris legacy default for VARCHAR; fallback when JDBC 
metadata and
+        // type string both lack length info (unlikely in practice)
+        return Types.VarCharType.of(p1 > 0 ? p1 : 255);
       default:
-        return Types.ExternalType.of(typeBean.getTypeName());
+        return Types.ExternalType.of(typeName);
     }
   }
 
@@ -98,7 +185,7 @@ public class DorisTypeConverter extends JdbcTypeConverter {
           + ((Types.DecimalType) type).scale()
           + ")";
     } else if (type instanceof Types.DateType) {
-      return DATE;
+      return DATEV2;
     } else if (type instanceof Types.TimestampType) {
       Types.TimestampType timestampType = (Types.TimestampType) type;
       return timestampType.hasPrecisionSet()
@@ -123,6 +210,10 @@ public class DorisTypeConverter extends JdbcTypeConverter {
       return CHAR + "(" + ((Types.FixedCharType) type).length() + ")";
     } else if (type instanceof Types.StringType) {
       return STRING;
+    } else if (type instanceof Types.BinaryType) {
+      return BINARY;
+    } else if (type instanceof Types.ExternalType) {
+      return ((Types.ExternalType) type).catalogString();
     }
     throw new IllegalArgumentException(
         String.format("Couldn't convert Gravitino type %s to Doris type", 
type.simpleString()));
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 e6c774c3fe..e82e44240b 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
@@ -1036,28 +1036,36 @@ public class DorisTableOperations extends 
JdbcTableOperations {
   public Integer calculateDatetimePrecision(String typeName, int columnSize, 
int scale) {
     String upperTypeName = typeName.toUpperCase();
 
-    // Check driver version compatibility first
-    boolean isDatetimeType = "DATETIME".equals(upperTypeName);
+    // Handle datetime(N) format from SHOW CREATE TABLE first — precision is 
parsed directly
+    // from the type string and does not depend on JDBC columnSize or driver 
version.
+    if (upperTypeName.startsWith("DATETIME(") && upperTypeName.endsWith(")")) {
+      try {
+        String precisionStr =
+            upperTypeName.substring("DATETIME(".length(), 
upperTypeName.length() - 1);
+        return Integer.parseInt(precisionStr);
+      } catch (NumberFormatException e) {
+        LOG.warn("Failed to parse datetime precision from type: {}", typeName, 
e);
+        return null;
+      }
+    }
 
-    if (isDatetimeType) {
+    // For plain DATETIME, precision is derived from columnSize which depends 
on the JDBC driver.
+    // Check driver version compatibility before using columnSize-based 
calculation.
+    if ("DATETIME".equals(upperTypeName)) {
       String driverVersion = getMySQLDriverVersion();
       if (driverVersion != null && 
!isMySQLDriverVersionSupported(driverVersion)) {
         LOG.warn(
             "MySQL driver version {} is below 8.0.16, columnSize may not be 
accurate for precision calculation. "
-                + "Returning null for {} type precision. Driver version: {}",
-            driverVersion,
-            upperTypeName,
+                + "Returning null for DATETIME type precision.",
             driverVersion);
         return null;
       }
-    }
-
-    if (upperTypeName.equals("DATETIME")) {
       // DATETIME format: 'YYYY-MM-DD HH:MM:SS' (19 chars) + decimal point + 
precision
       return columnSize >= DATETIME_FORMAT_WITH_DOT.length()
           ? columnSize - DATETIME_FORMAT_WITH_DOT.length()
           : 0;
     }
+
     return null;
   }
 }
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisTypeConverter.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisTypeConverter.java
index 2b5a50d310..3958ef0e46 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisTypeConverter.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/converter/TestDorisTypeConverter.java
@@ -22,6 +22,7 @@ import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.BI
 import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.BOOLEAN;
 import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.CHAR;
 import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.DATETIME;
+import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.DATEV2;
 import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.DECIMAL;
 import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.DOUBLE;
 import static 
org.apache.gravitino.catalog.doris.converter.DorisTypeConverter.FLOAT;
@@ -54,7 +55,9 @@ public class TestDorisTypeConverter {
     checkJdbcTypeToGravitinoType(Types.LongType.get(), BIGINT, null, null, 0);
     checkJdbcTypeToGravitinoType(Types.FloatType.get(), FLOAT, null, null, 0);
     checkJdbcTypeToGravitinoType(Types.DoubleType.get(), DOUBLE, null, null, 
0);
+    // datev2 is the canonical Doris 3.0+/4.0+ date type; toGravitino also 
accepts legacy "date"
     checkJdbcTypeToGravitinoType(Types.DateType.get(), DATE, null, null, 0);
+    checkJdbcTypeToGravitinoType(Types.DateType.get(), DATEV2, null, null, 0);
     checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(0), 
DATETIME, null, null, 0);
     checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(0), 
DATETIME, 19, null, 0);
     checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(3), 
DATETIME, 23, null, 3);
@@ -66,6 +69,28 @@ public class TestDorisTypeConverter {
     checkJdbcTypeToGravitinoType(Types.StringType.get(), TEXT, null, null, 0);
     checkJdbcTypeToGravitinoType(
         Types.ExternalType.of(USER_DEFINED_TYPE), USER_DEFINED_TYPE, null, 
null, 0);
+
+    // New type mappings for Doris 3.0+ / 4.0+
+    checkJdbcTypeToGravitinoType(Types.BinaryType.get(), "binary", null, null, 
0);
+    checkJdbcTypeToGravitinoType(Types.BinaryType.get(), "varbinary", null, 
null, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of("json"), "json", null, 
null, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of("variant"), "variant", 
null, null, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of("ipv4"), "ipv4", null, 
null, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of("ipv6"), "ipv6", null, 
null, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of("largeint"), 
"largeint", null, null, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of("bitmap"), "bitmap", 
null, null, 0);
+    checkJdbcTypeToGravitinoType(Types.ExternalType.of("hll"), "hll", null, 
null, 0);
+    // Parameterized types — parenthesized part is stripped, base type matched 
directly
+    checkJdbcTypeToGravitinoType(Types.IntegerType.get(), "int(11)", 0, 0, 0);
+    checkJdbcTypeToGravitinoType(Types.LongType.get(), "bigint(20)", 0, 0, 0);
+    // Parameterized types — parameters parsed from type string when 
columnSize=0 (missing JDBC
+    // metadata)
+    checkJdbcTypeToGravitinoType(Types.DecimalType.of(10, 2), "decimal(10,2)", 
0, 0, 0);
+    checkJdbcTypeToGravitinoType(Types.DecimalType.of(18, 6), "decimal(18,6)", 
0, 0, 0);
+    checkJdbcTypeToGravitinoType(Types.VarCharType.of(100), "varchar(100)", 0, 
0, 0);
+    checkJdbcTypeToGravitinoType(Types.FixedCharType.of(32), "char(32)", 0, 0, 
0);
+    checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(3), 
"datetime(3)", 0, 0, null);
+    checkJdbcTypeToGravitinoType(Types.TimestampType.withoutTimeZone(6), 
"datetime(6)", 0, 0, null);
   }
 
   @Test
@@ -77,17 +102,50 @@ public class TestDorisTypeConverter {
     checkGravitinoTypeToJdbcType(BIGINT, Types.LongType.get());
     checkGravitinoTypeToJdbcType(FLOAT, Types.FloatType.get());
     checkGravitinoTypeToJdbcType(DOUBLE, Types.DoubleType.get());
-    checkGravitinoTypeToJdbcType(DATE, Types.DateType.get());
+    // fromGravitino: DateType → datev2 (Doris 3.0+ canonical form)
+    checkGravitinoTypeToJdbcType(DATEV2, Types.DateType.get());
     checkGravitinoTypeToJdbcType(DATETIME, 
Types.TimestampType.withoutTimeZone());
     checkGravitinoTypeToJdbcType(DECIMAL + "(10,2)", Types.DecimalType.of(10, 
2));
     checkGravitinoTypeToJdbcType(VARCHAR + "(20)", Types.VarCharType.of(20));
     checkGravitinoTypeToJdbcType(CHAR + "(20)", Types.FixedCharType.of(20));
     checkGravitinoTypeToJdbcType(STRING, Types.StringType.get());
+    checkGravitinoTypeToJdbcType("binary", Types.BinaryType.get());
     Assertions.assertThrows(
         IllegalArgumentException.class,
         () -> 
DORIS_TYPE_CONVERTER.fromGravitino(Types.UnparsedType.of(USER_DEFINED_TYPE)));
   }
 
+  @Test
+  public void testExternalTypeRoundTrip() {
+    // ExternalType round-trip: fromGravitino(ExternalType) → toGravitino
+    String[] externalTypes = {
+      "json", "variant", "ipv4", "ipv6", "largeint", "bitmap", "hll", "bigint 
unsigned"
+    };
+    for (String typeName : externalTypes) {
+      Type externalType = Types.ExternalType.of(typeName);
+      String sql = DORIS_TYPE_CONVERTER.fromGravitino(externalType);
+      Assertions.assertEquals(typeName, sql);
+      Assertions.assertEquals(
+          externalType, DORIS_TYPE_CONVERTER.toGravitino(createTypeBean(sql, 
null, null, 0)));
+    }
+  }
+
+  @Test
+  public void testMalformedTypeStringFallback() {
+    // Malformed parameterized types (e.g. "varchar(abc)") should fallback to 
ExternalType
+    // instead of throwing NumberFormatException. columnSize=null triggers 
fallback parsing
+    // from type string in parseTypeParamsOrExternal.
+    Assertions.assertEquals(
+        Types.ExternalType.of("varchar(abc)"),
+        DORIS_TYPE_CONVERTER.toGravitino(createTypeBean("varchar(abc)", null, 
null, null)));
+    Assertions.assertEquals(
+        Types.ExternalType.of("char(xyz)"),
+        DORIS_TYPE_CONVERTER.toGravitino(createTypeBean("char(xyz)", null, 
null, null)));
+    Assertions.assertEquals(
+        Types.ExternalType.of("decimal(a,b)"),
+        DORIS_TYPE_CONVERTER.toGravitino(createTypeBean("decimal(a,b)", null, 
null, null)));
+  }
+
   protected void checkGravitinoTypeToJdbcType(String jdbcTypeName, Type 
gravitinoType) {
     Assertions.assertEquals(jdbcTypeName, 
DORIS_TYPE_CONVERTER.fromGravitino(gravitinoType));
   }
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 77a6d8224b..d3f80abedf 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
@@ -23,9 +23,11 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import com.google.common.collect.Maps;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
@@ -279,4 +281,44 @@ public class CatalogDoris3xIT extends BaseIT {
     assertEquals("idx_data", t.index()[0].name());
     assertEquals(colName2, t.index()[0].fieldNames()[0][0]);
   }
+
+  @Test
+  void testExternalTypeRoundTrip() {
+    // Verify ExternalType columns survive the create → Doris 3.0 → load 
round-trip.
+    //
+    // Only "json" is tested here because the MySQL JDBC driver returns 
TYPE_NAME = "UNKNOWN"
+    // for Doris-specific types (ipv4, ipv6, variant, bitmap, hll, largeint) 
that have no
+    // standard JDBC type mapping. The toGravitino() fallback produces 
ExternalType("unknown")
+    // for those, so they cannot round-trip through the standard JDBC metadata 
path.
+    // The DDL generation (fromGravitino) and type parsing (toGravitino) for 
all these types
+    // are covered by unit tests in TestDorisTypeConverter.
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid = NameIdentifier.of(schemaName, "t_external_types");
+
+    Column[] columns =
+        ArrayUtils.addAll(
+            basicColumns(), Column.of("json_col", 
Types.ExternalType.of("json"), "json column"));
+
+    tc.createTable(
+        tid,
+        columns,
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    Table t = tc.loadTable(tid);
+    assertEquals(3, t.columns().length);
+
+    assertEquals(Types.ExternalType.of("json"), findColumn(t, 
"json_col").dataType());
+  }
+
+  private Column findColumn(Table table, String columnName) {
+    return Arrays.stream(table.columns())
+        .filter(c -> c.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/integration/test/CatalogDoris4xIT.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDoris4xIT.java
index ae38d204c6..9d64f5bbee 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
@@ -23,9 +23,11 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import com.google.common.collect.Maps;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
@@ -279,4 +281,44 @@ public class CatalogDoris4xIT extends BaseIT {
     assertEquals("idx_data", t.index()[0].name());
     assertEquals(colName2, t.index()[0].fieldNames()[0][0]);
   }
+
+  @Test
+  void testExternalTypeRoundTrip() {
+    // Verify ExternalType columns survive the create → Doris 4.0 → load 
round-trip.
+    //
+    // Only "json" is tested here because the MySQL JDBC driver returns 
TYPE_NAME = "UNKNOWN"
+    // for Doris-specific types (ipv4, ipv6, variant, bitmap, hll, largeint) 
that have no
+    // standard JDBC type mapping. The toGravitino() fallback produces 
ExternalType("unknown")
+    // for those, so they cannot round-trip through the standard JDBC metadata 
path.
+    // The DDL generation (fromGravitino) and type parsing (toGravitino) for 
all these types
+    // are covered by unit tests in TestDorisTypeConverter.
+    TableCatalog tc = catalog.asTableCatalog();
+    NameIdentifier tid = NameIdentifier.of(schemaName, "t_external_types");
+
+    Column[] columns =
+        ArrayUtils.addAll(
+            basicColumns(), Column.of("json_col", 
Types.ExternalType.of("json"), "json column"));
+
+    tc.createTable(
+        tid,
+        columns,
+        tableComment,
+        Collections.emptyMap(),
+        Transforms.EMPTY_TRANSFORM,
+        hashDist(),
+        null,
+        null);
+
+    Table t = tc.loadTable(tid);
+    assertEquals(3, t.columns().length);
+
+    assertEquals(Types.ExternalType.of("json"), findColumn(t, 
"json_col").dataType());
+  }
+
+  private Column findColumn(Table table, String columnName) {
+    return Arrays.stream(table.columns())
+        .filter(c -> c.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/integration/test/CatalogDorisIT.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/integration/test/CatalogDorisIT.java
index 7473780ee7..24a044c21f 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
@@ -88,6 +88,7 @@ import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
 @Tag("gravitino-docker-test")
+@Tag("doris-multi-version")
 public class CatalogDorisIT extends BaseIT {
 
   private static final String provider = "jdbc-doris";
diff --git 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
index ffa4ea1cfc..a503eb3c5e 100644
--- 
a/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
+++ 
b/catalogs/catalog-jdbc-doris/src/test/java/org/apache/gravitino/catalog/doris/operation/TestDorisTableOperations.java
@@ -474,9 +474,9 @@ public class TestDorisTableOperations extends TestDoris {
             Types.IntervalDayType.get(),
             Types.IntervalYearType.get(),
             Types.UUIDType.get(),
+            Types.UnionType.of(Types.IntegerType.get()),
             Types.ListType.of(Types.DateType.get(), true),
             Types.MapType.of(Types.StringType.get(), Types.IntegerType.get(), 
true),
-            Types.UnionType.of(Types.IntegerType.get()),
             Types.StructType.of(
                 Types.StructType.Field.notNullField("col_1", 
Types.IntegerType.get())));
 
@@ -718,6 +718,27 @@ public class TestDorisTableOperations extends TestDoris {
     Assertions.assertNull(
         TABLE_OPERATIONS.calculateDatetimePrecision("VARCHAR", 50, 0),
         "Non-datetime type should return 0 precision");
+
+    // DATETIME(N) format from SHOW CREATE TABLE — precision parsed from type 
string
+    Assertions.assertEquals(
+        0,
+        TABLE_OPERATIONS.calculateDatetimePrecision("DATETIME(0)", 0, 0),
+        "DATETIME(0) should return 0 precision");
+
+    Assertions.assertEquals(
+        3,
+        TABLE_OPERATIONS.calculateDatetimePrecision("DATETIME(3)", 0, 0),
+        "DATETIME(3) should return 3 precision");
+
+    Assertions.assertEquals(
+        6,
+        TABLE_OPERATIONS.calculateDatetimePrecision("DATETIME(6)", 0, 0),
+        "DATETIME(6) should return 6 precision");
+
+    // Invalid DATETIME(N) — non-numeric precision
+    Assertions.assertNull(
+        TABLE_OPERATIONS.calculateDatetimePrecision("DATETIME(x)", 0, 0),
+        "DATETIME(x) with invalid precision should return null");
   }
 
   @Test
@@ -738,5 +759,12 @@ public class TestDorisTableOperations extends TestDoris {
     Assertions.assertNull(
         operationsWithOldDriver.calculateDatetimePrecision("DATETIME", 26, 0),
         "DATETIME type should return null for unsupported driver version");
+
+    // DATETIME(N) should still work with old driver — precision comes from 
type string, not
+    // columnSize
+    Assertions.assertEquals(
+        3,
+        operationsWithOldDriver.calculateDatetimePrecision("DATETIME(3)", 0, 
0),
+        "DATETIME(3) should return 3 even with unsupported driver version");
   }
 }

Reply via email to