From 433e1147b6c99bde007bbbcbf58410bb2fb498c3 Mon Sep 17 00:00:00 2001 From: daidai Date: Thu, 2 Apr 2026 17:54:07 +0800 Subject: [PATCH] [fix](fe) Fix OceanBase JDBC catalog compatibility with newer drivers --- .../apache/doris/catalog/JdbcResource.java | 41 ++++- .../datasource/jdbc/JdbcExternalCatalog.java | 2 +- .../jdbc/client/JdbcMySQLClient.java | 158 ++++++++++++++---- .../datasource/jdbc/util/JdbcFieldSchema.java | 16 +- .../doris/catalog/JdbcResourceTest.java | 22 +++ .../jdbc/client/JdbcMySQLClientTest.java | 81 +++++++++ 6 files changed, 275 insertions(+), 45 deletions(-) create mode 100644 fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClientTest.java diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java index f1e312faebeb2a..da364051f4360b 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java +++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java @@ -52,6 +52,8 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * External JDBC Catalog resource for external table query. @@ -163,6 +165,8 @@ public class JdbcResource extends Resource { // timeout for both connection and read. 10 seconds is long enough. private static final int HTTP_TIMEOUT_MS = 10000; + private static final Pattern OCEANBASE_DRIVER_VERSION_PATTERN = + Pattern.compile("oceanbase-client-(\\d+(?:\\.\\d+){1,3})\\.jar", Pattern.CASE_INSENSITIVE); @SerializedName(value = "configs") private Map configs; @@ -185,7 +189,7 @@ public void modifyProperties(Map properties) throws DdlException for (String propertyKey : ALL_PROPERTIES) { replaceIfEffectiveValue(this.configs, propertyKey, properties.get(propertyKey)); } - this.configs.put(JDBC_URL, handleJdbcUrl(getProperty(JDBC_URL))); + this.configs.put(JDBC_URL, handleJdbcUrl(getProperty(JDBC_URL), getProperty(DRIVER_URL))); super.modifyProperties(properties); } @@ -216,7 +220,7 @@ protected void setProperties(ImmutableMap properties) throws Ddl throw new DdlException("JdbcResource Missing " + property + " in properties"); } } - this.configs.put(JDBC_URL, handleJdbcUrl(getProperty(JDBC_URL))); + this.configs.put(JDBC_URL, handleJdbcUrl(getProperty(JDBC_URL), getProperty(DRIVER_URL))); configs.put(CHECK_SUM, computeObjectChecksum(getProperty(DRIVER_URL))); } @@ -396,6 +400,10 @@ public static String parseDbType(String url) throws DdlException { } public static String handleJdbcUrl(String jdbcUrl) throws DdlException { + return handleJdbcUrl(jdbcUrl, null); + } + + public static String handleJdbcUrl(String jdbcUrl, String driverUrl) throws DdlException { // delete all space in jdbcUrl String newJdbcUrl = jdbcUrl.replaceAll(" ", ""); String dbType = parseDbType(newJdbcUrl); @@ -415,6 +423,10 @@ public static String handleJdbcUrl(String jdbcUrl) throws DdlException { if (dbType.equals(OCEANBASE)) { // set useCursorFetch to true newJdbcUrl = checkAndSetJdbcBoolParam(dbType, newJdbcUrl, "useCursorFetch", "false", "true"); + if (shouldDisableOceanBaseLegacyDatetimeCode(driverUrl)) { + newJdbcUrl = checkAndSetJdbcBoolParam(dbType, newJdbcUrl, + "useLegacyDatetimeCode", "true", "false"); + } } } if (dbType.equals(POSTGRESQL)) { @@ -429,6 +441,31 @@ public static String handleJdbcUrl(String jdbcUrl) throws DdlException { return newJdbcUrl; } + private static boolean shouldDisableOceanBaseLegacyDatetimeCode(String driverUrl) { + if (driverUrl == null || driverUrl.isEmpty()) { + return false; + } + Matcher matcher = OCEANBASE_DRIVER_VERSION_PATTERN.matcher(driverUrl); + if (!matcher.find()) { + return false; + } + return compareVersion(matcher.group(1), "2.4.15") >= 0; + } + + private static int compareVersion(String leftVersion, String rightVersion) { + String[] left = leftVersion.split("\\."); + String[] right = rightVersion.split("\\."); + int maxLength = Math.max(left.length, right.length); + for (int i = 0; i < maxLength; i++) { + int leftPart = i < left.length ? Integer.parseInt(left[i]) : 0; + int rightPart = i < right.length ? Integer.parseInt(right[i]) : 0; + if (leftPart != rightPart) { + return Integer.compare(leftPart, rightPart); + } + } + return 0; + } + /** * Check jdbcUrl param, if the param is not set, set it to the expected value. * If the param is set to an unexpected value, replace it with the expected value. diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java index a841626bcadf2f..ec1d4e17347ece 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java @@ -159,7 +159,7 @@ protected Map processCompatibleProperties(Map pr } String jdbcUrl = properties.getOrDefault(JdbcResource.JDBC_URL, ""); if (!Strings.isNullOrEmpty(jdbcUrl)) { - jdbcUrl = JdbcResource.handleJdbcUrl(jdbcUrl); + jdbcUrl = JdbcResource.handleJdbcUrl(jdbcUrl, properties.get(JdbcResource.DRIVER_URL)); properties.put(JdbcResource.JDBC_URL, jdbcUrl); } return properties; diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClient.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClient.java index ef43af25084abc..98ce6089d3d272 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClient.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClient.java @@ -18,11 +18,13 @@ package org.apache.doris.datasource.jdbc.client; import org.apache.doris.catalog.ArrayType; +import org.apache.doris.catalog.JdbcResource; import org.apache.doris.catalog.ScalarType; import org.apache.doris.catalog.Type; import org.apache.doris.common.util.Util; import org.apache.doris.datasource.jdbc.util.JdbcFieldSchema; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -35,7 +37,9 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Types; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -168,12 +172,18 @@ public List getJdbcColumnsInfo(String remoteDbName, String remo rs = getRemoteColumns(databaseMetaData, catalogName, remoteDbName, remoteTableName); Map mapFieldtoType = Maps.newHashMap(); - if (isDoris) { + if (requiresFullTypeDefinition()) { mapFieldtoType = getColumnsDataTypeUseQuery(remoteDbName, remoteTableName); } while (rs.next()) { - JdbcFieldSchema field = new JdbcFieldSchema(rs, mapFieldtoType); + JdbcFieldSchema field = new JdbcFieldSchema(rs); + String fullTypeName = mapFieldtoType.get(field.getColumnName()); + if (isDoris) { + field.setDataTypeName(Optional.ofNullable(fullTypeName)); + } else { + field.setFullDataTypeName(Optional.ofNullable(fullTypeName)); + } tableSchema.add(field); } } catch (SQLException e) { @@ -225,13 +235,16 @@ protected Type jdbcTypeToDoris(JdbcFieldSchema fieldSchema) { if (isDoris) { return dorisTypeToDoris(fieldSchema); } - // For mysql type: "INT UNSIGNED": - // fieldSchema.getDataTypeName().orElse("unknown").split(" ")[0] == "INT" - // fieldSchema.getDataTypeName().orElse("unknown").split(" ")[1] == "UNSIGNED" - String[] typeFields = fieldSchema.getDataTypeName().orElse("unknown").split(" "); - String mysqlType = typeFields[0]; + return mysqlTypeToDoris(fieldSchema, enableMappingVarbinary, enableMappingTimestampTz, convertDateToNull); + } + + @VisibleForTesting + static Type mysqlTypeToDoris(JdbcFieldSchema fieldSchema, boolean enableMappingVarbinary, + boolean enableMappingTimestampTz, boolean convertDateToNull) { + MySqlTypeDescriptor typeDescriptor = MySqlTypeDescriptor.from(fieldSchema); + String mysqlType = typeDescriptor.baseType; // For unsigned int, should extend the type. - if (typeFields.length > 1 && "UNSIGNED".equals(typeFields[1])) { + if (typeDescriptor.unsigned) { switch (mysqlType) { case "TINYINT": return Type.SMALLINT; @@ -239,13 +252,14 @@ protected Type jdbcTypeToDoris(JdbcFieldSchema fieldSchema) { case "MEDIUMINT": return Type.INT; case "INT": + case "INTEGER": return Type.BIGINT; case "BIGINT": return Type.LARGEINT; case "DECIMAL": { - int precision = fieldSchema.requiredColumnSize() + 1; - int scale = fieldSchema.requiredDecimalDigits(); - return createDecimalOrStringType(precision, scale); + int precision = getColumnLength(fieldSchema, typeDescriptor) + 1; + int scale = getDecimalScale(fieldSchema, typeDescriptor); + return createMysqlDecimalOrStringType(precision, scale); } case "DOUBLE": // As of MySQL 8.0.17, the UNSIGNED attribute is deprecated @@ -260,15 +274,20 @@ protected Type jdbcTypeToDoris(JdbcFieldSchema fieldSchema) { } } switch (mysqlType) { + case "BOOL": case "BOOLEAN": return Type.BOOLEAN; case "TINYINT": + if (fieldSchema.getDataType() == Types.BIT && getOptionalColumnLength(fieldSchema, typeDescriptor) == 1) { + return Type.BOOLEAN; + } return Type.TINYINT; case "SMALLINT": case "YEAR": return Type.SMALLINT; case "MEDIUMINT": case "INT": + case "INTEGER": return Type.INT; case "BIGINT": return Type.BIGINT; @@ -278,11 +297,7 @@ protected Type jdbcTypeToDoris(JdbcFieldSchema fieldSchema) { } return ScalarType.createDateV2Type(); case "TIMESTAMP": { - int columnSize = fieldSchema.requiredColumnSize(); - int scale = columnSize > 19 ? columnSize - 20 : 0; - if (scale > 6) { - scale = 6; - } + int scale = getDateTimeScale(fieldSchema, typeDescriptor); if (convertDateToNull) { fieldSchema.setAllowNull(true); } @@ -291,12 +306,8 @@ protected Type jdbcTypeToDoris(JdbcFieldSchema fieldSchema) { } case "DATETIME": { // mysql can support microsecond - // use columnSize to calculate the precision of timestamp/datetime - int columnSize = fieldSchema.requiredColumnSize(); - int scale = columnSize > 19 ? columnSize - 20 : 0; - if (scale > 6) { - scale = 6; - } + // use type definition when available, otherwise fall back to column size metadata + int scale = getDateTimeScale(fieldSchema, typeDescriptor); if (convertDateToNull) { fieldSchema.setAllowNull(true); } @@ -307,24 +318,24 @@ protected Type jdbcTypeToDoris(JdbcFieldSchema fieldSchema) { case "DOUBLE": return Type.DOUBLE; case "DECIMAL": { - int precision = fieldSchema.requiredColumnSize(); - int scale = fieldSchema.requiredDecimalDigits(); - return createDecimalOrStringType(precision, scale); + int precision = getColumnLength(fieldSchema, typeDescriptor); + int scale = getDecimalScale(fieldSchema, typeDescriptor); + return createMysqlDecimalOrStringType(precision, scale); } case "CHAR": - return ScalarType.createCharType(fieldSchema.requiredColumnSize()); + return ScalarType.createCharType(getColumnLength(fieldSchema, typeDescriptor)); case "VARCHAR": - return ScalarType.createVarcharType(fieldSchema.requiredColumnSize()); + return ScalarType.createVarcharType(getColumnLength(fieldSchema, typeDescriptor)); case "TINYBLOB": case "BLOB": case "MEDIUMBLOB": case "LONGBLOB": case "BINARY": case "VARBINARY": - return enableMappingVarbinary ? ScalarType.createVarbinaryType(fieldSchema.requiredColumnSize()) + return enableMappingVarbinary ? ScalarType.createVarbinaryType(getColumnLength(fieldSchema, typeDescriptor)) : ScalarType.createStringType(); case "BIT": - if (fieldSchema.requiredColumnSize() == 1) { + if (getOptionalColumnLength(fieldSchema, typeDescriptor) == 1) { return Type.BOOLEAN; } else { return ScalarType.createStringType(); @@ -344,6 +355,10 @@ protected Type jdbcTypeToDoris(JdbcFieldSchema fieldSchema) { } } + private boolean requiresFullTypeDefinition() { + return isDoris || JdbcResource.OCEANBASE.equals(dbType); + } + private boolean isConvertDatetimeToNull(JdbcClientConfig jdbcClientConfig) { // Check if the JDBC URL contains "zeroDateTimeBehavior=convertToNull" or "zeroDateTimeBehavior=convert_to_null" String jdbcUrl = jdbcClientConfig.getJdbcUrl().toLowerCase(); @@ -462,4 +477,89 @@ private Type dorisTypeToDoris(JdbcFieldSchema fieldSchema) { return Type.UNSUPPORTED; } } + + private static int getColumnLength(JdbcFieldSchema fieldSchema, MySqlTypeDescriptor typeDescriptor) { + return typeDescriptor.length.orElseGet(fieldSchema::requiredColumnSize); + } + + private static int getOptionalColumnLength(JdbcFieldSchema fieldSchema, MySqlTypeDescriptor typeDescriptor) { + return typeDescriptor.length.orElseGet(() -> fieldSchema.getColumnSize().orElse(0)); + } + + private static int getDecimalScale(JdbcFieldSchema fieldSchema, MySqlTypeDescriptor typeDescriptor) { + return typeDescriptor.scale.orElseGet(fieldSchema::requiredDecimalDigits); + } + + private static int getDateTimeScale(JdbcFieldSchema fieldSchema, MySqlTypeDescriptor typeDescriptor) { + int scale = typeDescriptor.length.orElseGet(() -> { + int columnSize = fieldSchema.getColumnSize().orElse(0); + return columnSize > 19 ? columnSize - 20 : 0; + }); + return Math.min(scale, 6); + } + + private static Type createMysqlDecimalOrStringType(int precision, int scale) { + if (precision <= ScalarType.MAX_DECIMAL128_PRECISION && precision > 0) { + return ScalarType.createDecimalV3Type(precision, scale); + } + return ScalarType.createStringType(); + } + + private static class MySqlTypeDescriptor { + private final String baseType; + private final boolean unsigned; + private final Optional length; + private final Optional scale; + + private MySqlTypeDescriptor(String baseType, boolean unsigned, Optional length, + Optional scale) { + this.baseType = baseType; + this.unsigned = unsigned; + this.length = length; + this.scale = scale; + } + + private static MySqlTypeDescriptor from(JdbcFieldSchema fieldSchema) { + String typeName = fieldSchema.getFullDataTypeName() + .orElse(fieldSchema.getDataTypeName().orElse("unknown")); + String normalized = typeName == null ? "" : typeName.trim().replaceAll("\\s+", " "); + if (normalized.isEmpty()) { + return new MySqlTypeDescriptor("UNKNOWN", false, Optional.empty(), Optional.empty()); + } + + String upperType = normalized.toUpperCase(Locale.ROOT); + boolean unsigned = upperType.contains(" UNSIGNED"); + int openParen = upperType.indexOf('('); + int firstSpace = upperType.indexOf(' '); + int endIndex = upperType.length(); + if (openParen >= 0) { + endIndex = openParen; + } else if (firstSpace >= 0) { + endIndex = firstSpace; + } + String baseType = upperType.substring(0, endIndex); + + Optional length = Optional.empty(); + Optional scale = Optional.empty(); + if (openParen >= 0) { + int closeParen = upperType.indexOf(')', openParen + 1); + if (closeParen > openParen + 1) { + String[] parameters = upperType.substring(openParen + 1, closeParen).split(","); + length = parseTypeParameter(parameters[0]); + if (parameters.length > 1) { + scale = parseTypeParameter(parameters[1]); + } + } + } + return new MySqlTypeDescriptor(baseType, unsigned, length, scale); + } + + private static Optional parseTypeParameter(String parameter) { + try { + return Optional.of(Integer.parseInt(parameter.trim())); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/util/JdbcFieldSchema.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/util/JdbcFieldSchema.java index d91b06b46c93b7..02744a9b300daf 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/util/JdbcFieldSchema.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/util/JdbcFieldSchema.java @@ -23,7 +23,6 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; -import java.util.Map; import java.util.Optional; @Data @@ -33,6 +32,8 @@ public class JdbcFieldSchema { protected int dataType; // The SQL type of the corresponding java.sql.types (Type Name) protected Optional dataTypeName; + // The native type definition returned by SHOW FULL COLUMNS, such as "decimal(18,5) unsigned". + protected Optional fullDataTypeName = Optional.empty(); // For CHAR/DATA, columnSize means the maximum number of chars. // For NUMERIC/DECIMAL, columnSize means precision. protected Optional columnSize; @@ -52,6 +53,7 @@ public JdbcFieldSchema(JdbcFieldSchema other) { this.columnName = other.columnName; this.dataType = other.dataType; this.dataTypeName = other.dataTypeName; + this.fullDataTypeName = other.fullDataTypeName; this.columnSize = other.columnSize; this.decimalDigits = other.decimalDigits; this.arrayDimensions = other.arrayDimensions; @@ -86,18 +88,6 @@ public JdbcFieldSchema(ResultSet rs, int arrayDimensions) throws SQLException { this.arrayDimensions = Optional.of(arrayDimensions); } - public JdbcFieldSchema(ResultSet rs, Map dataTypeOverrides) throws SQLException { - this.columnName = rs.getString("COLUMN_NAME"); - this.dataType = getInteger(rs, "DATA_TYPE").orElseThrow(() -> new IllegalStateException("DATA_TYPE is null")); - this.dataTypeName = Optional.ofNullable(dataTypeOverrides.getOrDefault(columnName, rs.getString("TYPE_NAME"))); - this.columnSize = getInteger(rs, "COLUMN_SIZE"); - this.decimalDigits = getInteger(rs, "DECIMAL_DIGITS"); - this.numPrecRadix = rs.getInt("NUM_PREC_RADIX"); - this.isAllowNull = rs.getInt("NULLABLE") != 0; - this.remarks = rs.getString("REMARKS"); - this.charOctetLength = rs.getInt("CHAR_OCTET_LENGTH"); - } - public JdbcFieldSchema(ResultSetMetaData metaData, int columnIndex) throws SQLException { this.columnName = metaData.getColumnName(columnIndex); this.dataType = metaData.getColumnType(columnIndex); diff --git a/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java b/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java index 2e2d9af9626094..01d085f78b4e16 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/JdbcResourceTest.java @@ -198,6 +198,28 @@ public void testHandleJdbcUrlForSqlServerWithParams() throws DdlException { Assert.assertTrue(resultUrl.contains(";")); } + @Test + public void testHandleJdbcUrlForOceanBaseNewDriver() throws DdlException { + String inputUrl = "jdbc:oceanbase://127.0.0.1:2881/doris_test"; + String driverUrl = "file:///tmp/oceanbase-client-2.4.17.jar"; + + String resultUrl = JdbcResource.handleJdbcUrl(inputUrl, driverUrl); + + Assert.assertTrue(resultUrl.contains("useCursorFetch=true")); + Assert.assertTrue(resultUrl.contains("useLegacyDatetimeCode=false")); + } + + @Test + public void testHandleJdbcUrlForOceanBaseLegacyDriver() throws DdlException { + String inputUrl = "jdbc:oceanbase://127.0.0.1:2881/doris_test"; + String driverUrl = "file:///tmp/oceanbase-client-2.4.8.jar"; + + String resultUrl = JdbcResource.handleJdbcUrl(inputUrl, driverUrl); + + Assert.assertTrue(resultUrl.contains("useCursorFetch=true")); + Assert.assertFalse(resultUrl.contains("useLegacyDatetimeCode=false")); + } + @Test public void testValidDriverUrls() { String fileUrl = "file://path/to/driver.jar"; diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClientTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClientTest.java new file mode 100644 index 00000000000000..a2e4b5f1617994 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClientTest.java @@ -0,0 +1,81 @@ +// 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.doris.datasource.jdbc.client; + +import org.apache.doris.catalog.ScalarType; +import org.apache.doris.catalog.Type; +import org.apache.doris.datasource.jdbc.util.JdbcFieldSchema; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.Types; +import java.util.Optional; + +public class JdbcMySQLClientTest { + + @Test + public void testMysqlTypeToDorisNormalizesLowerCaseMetadataType() { + JdbcFieldSchema fieldSchema = mockFieldSchema(Types.VARCHAR, "varchar", null, 10, null); + + Type type = JdbcMySQLClient.mysqlTypeToDoris(fieldSchema, false, false, false); + + Assert.assertEquals(ScalarType.createVarcharType(10), type); + } + + @Test + public void testMysqlTypeToDorisUsesOceanBaseRawUnsignedDefinition() { + JdbcFieldSchema fieldSchema = mockFieldSchema(Types.INTEGER, "int", "int(10) unsigned", 10, 0); + + Type type = JdbcMySQLClient.mysqlTypeToDoris(fieldSchema, false, false, false); + + Assert.assertEquals(Type.BIGINT, type); + } + + @Test + public void testMysqlTypeToDorisUsesOceanBaseRawTemporalScale() { + JdbcFieldSchema fieldSchema = mockFieldSchema(Types.TIMESTAMP, "timestamp", "timestamp(4)", 19, null); + + Type type = JdbcMySQLClient.mysqlTypeToDoris(fieldSchema, false, false, false); + + Assert.assertEquals(ScalarType.createDatetimeV2Type(4), type); + } + + @Test + public void testMysqlTypeToDorisPreservesOceanBaseBooleanMetadata() { + JdbcFieldSchema fieldSchema = mockFieldSchema(Types.BIT, "BIT", "tinyint(1)", 5, 0); + + Type type = JdbcMySQLClient.mysqlTypeToDoris(fieldSchema, false, false, false); + + Assert.assertEquals(Type.BOOLEAN, type); + } + + private JdbcFieldSchema mockFieldSchema(int jdbcType, String typeName, String fullTypeName, + Integer columnSize, Integer decimalDigits) { + JdbcFieldSchema fieldSchema = Mockito.mock(JdbcFieldSchema.class); + Mockito.when(fieldSchema.getDataType()).thenReturn(jdbcType); + Mockito.when(fieldSchema.getDataTypeName()).thenReturn(Optional.ofNullable(typeName)); + Mockito.when(fieldSchema.getFullDataTypeName()).thenReturn(Optional.ofNullable(fullTypeName)); + Mockito.when(fieldSchema.getColumnSize()).thenReturn(Optional.ofNullable(columnSize)); + Mockito.when(fieldSchema.getDecimalDigits()).thenReturn(Optional.ofNullable(decimalDigits)); + Mockito.when(fieldSchema.requiredColumnSize()).thenReturn(columnSize == null ? 0 : columnSize); + Mockito.when(fieldSchema.requiredDecimalDigits()).thenReturn(decimalDigits == null ? 0 : decimalDigits); + return fieldSchema; + } +}