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");
}
}