This is an automated email from the ASF dual-hosted git repository.
JNSimba pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new ab043a433ce [fix](fe) Normalize MySQL JDBC URLs for streaming jobs
(#65647)
ab043a433ce is described below
commit ab043a433cef0246bd81fbe76011a83b9a13d17b
Author: wudi <[email protected]>
AuthorDate: Wed Jul 22 16:41:31 2026 +0800
[fix](fe) Normalize MySQL JDBC URLs for streaming jobs (#65647)
### What problem does this PR solve?
MySQL Connector/J interprets TINYINT(1) as BOOLEAN and
YEAR as a date unless JDBC URL options override those defaults.
Streaming jobs did not normalize the URL consistently across the FROM
MYSQL and cdc_stream TVF paths, which could change automatically
discovered Doris column types and YEAR zero values. This change adds a
shared streaming JDBC URL normalizer and applies it before metadata
discovery and CDC reads. MySQL streaming URLs now use
yearIsDateType=false, tinyInt1isBit=false, useUnicode=true, and
characterEncoding=utf-8. PostgreSQL URLs remain unchanged, and
rewriteBatchedStatements is intentionally not added because streaming
ingestion writes through Stream Load.
---
.../insert/streaming/StreamingInsertJob.java | 17 ++-
.../streaming/StreamingJdbcUrlNormalizer.java | 83 ++++++++++++++
.../offset/jdbc/JdbcTvfSourceOffsetProvider.java | 3 +
.../CdcStreamTableValuedFunction.java | 10 +-
.../streaming/StreamingJdbcUrlNormalizerTest.java | 78 +++++++++++++
.../jdbc/JdbcTvfSourceOffsetProviderTest.java | 46 ++++++++
.../CdcStreamTableValuedFunctionTest.java | 15 +++
.../source/reader/mysql/MySqlSourceReader.java | 8 ++
.../source/reader/mysql/MySqlYearConverter.java | 70 ++++++++++++
.../source/reader/mysql/MySqlSourceReaderTest.java | 29 +++++
.../reader/mysql/MySqlYearConverterTest.java | 75 ++++++++++++
.../cdc/test_streaming_mysql_job_all_type.out | 9 +-
.../test_streaming_mysql_job_integer_boundary.out | 33 +++---
.../cdc/tvf/test_cdc_stream_tvf_mysql.out | 13 +--
...ing_job_cdc_stream_mysql_jdbc_type_defaults.out | 14 +++
.../cdc/test_streaming_mysql_job_all_type.groovy | 4 +-
..._streaming_mysql_job_charset_and_strings.groovy | 6 +-
.../test_streaming_mysql_job_create_alter.groovy | 37 +-----
...est_streaming_mysql_job_integer_boundary.groovy | 41 +++----
.../test_streaming_mysql_job_special_offset.groovy | 11 +-
.../cdc/tvf/test_cdc_stream_tvf_mysql.groovy | 11 +-
..._job_cdc_stream_mysql_jdbc_type_defaults.groovy | 126 +++++++++++++++++++++
22 files changed, 633 insertions(+), 106 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
index b22ccea0b19..28b684a7e03 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
@@ -297,11 +297,12 @@ public class StreamingInsertJob extends
AbstractJob<StreamingJobSchedulerTask, M
private List<String> createTableIfNotExists() throws Exception {
List<String> syncTbls = new ArrayList<>();
+ Map<String, String> effectiveSourceProperties =
buildConvertedSourceProperties(sourceProperties);
// Key: source table name (PG/MySQL); Value: CreateTableCommand for
the Doris target table.
// The two names differ when "table.<src>.target_table" is configured.
LinkedHashMap<String, CreateTableCommand> createTblCmds =
StreamingJobUtils.generateCreateTableCmds(targetDb,
- dataSourceType, sourceProperties, targetProperties);
+ dataSourceType, effectiveSourceProperties,
targetProperties);
Database db =
Env.getCurrentEnv().getInternalCatalog().getDbNullable(targetDb);
Preconditions.checkNotNull(db, "target database %s does not exist",
targetDb);
for (Map.Entry<String, CreateTableCommand> entry :
createTblCmds.entrySet()) {
@@ -536,7 +537,7 @@ public class StreamingInsertJob extends
AbstractJob<StreamingJobSchedulerTask, M
Map<String, String> mergedSourceProperties = new
HashMap<>(this.sourceProperties);
mergedSourceProperties.putAll(alterJobCommand.getSourceProperties());
Map<String, String> newConvertedSourceProperties =
- StreamingJobUtils.convertCertFile(getDbId(),
mergedSourceProperties);
+ buildConvertedSourceProperties(mergedSourceProperties);
this.sourceProperties = mergedSourceProperties;
this.convertedSourceProperties = newConvertedSourceProperties;
logParts.add("source properties: " +
alterJobCommand.getSourceProperties());
@@ -677,11 +678,21 @@ public class StreamingInsertJob extends
AbstractJob<StreamingJobSchedulerTask, M
private Map<String, String> getConvertedSourceProperties() throws
JobException {
if (convertedSourceProperties == null) {
- this.convertedSourceProperties =
StreamingJobUtils.convertCertFile(getDbId(), sourceProperties);
+ this.convertedSourceProperties =
buildConvertedSourceProperties(sourceProperties);
}
return convertedSourceProperties;
}
+ private Map<String, String> buildConvertedSourceProperties(Map<String,
String> inputProperties)
+ throws JobException {
+ Map<String, String> convertedProperties =
+ StreamingJobUtils.convertCertFile(getDbId(), inputProperties);
+ convertedProperties.put(DataSourceConfigKeys.JDBC_URL,
+ StreamingJdbcUrlNormalizer.normalize(dataSourceType,
+
convertedProperties.get(DataSourceConfigKeys.JDBC_URL)));
+ return convertedProperties;
+ }
+
private Map<String, String> getOriginTvfProps() {
if (originTvfProps == null) {
this.originTvfProps = getCurrentTvf().getProperties().getMap();
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingJdbcUrlNormalizer.java
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingJdbcUrlNormalizer.java
new file mode 100644
index 00000000000..f13d41687df
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingJdbcUrlNormalizer.java
@@ -0,0 +1,83 @@
+// 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.job.extensions.insert.streaming;
+
+import org.apache.doris.job.common.DataSourceType;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Normalizes JDBC URLs before streaming ingestion uses them for metadata
discovery and CDC reads.
+ * Database-specific rules are kept here so every streaming entry point
applies the same read-side
+ * semantics while leaving unrelated JDBC Catalog write optimizations out of
scope.
+ */
+public final class StreamingJdbcUrlNormalizer {
+
+ private StreamingJdbcUrlNormalizer() {
+ }
+
+ public static String normalize(DataSourceType sourceType, String jdbcUrl) {
+ switch (sourceType) {
+ case MYSQL:
+ return normalizeMysql(jdbcUrl);
+ case POSTGRES:
+ return jdbcUrl;
+ default:
+ throw new IllegalArgumentException("Unsupported data source
type: " + sourceType);
+ }
+ }
+
+ private static String normalizeMysql(String jdbcUrl) {
+ String normalizedUrl = jdbcUrl.replace(" ", "");
+ Set<String> params = getParams(normalizedUrl);
+ StringBuilder result = new StringBuilder(normalizedUrl);
+ setDefaultParam(result, params, "yearIsDateType", "false");
+ setDefaultParam(result, params, "tinyInt1isBit", "false");
+ setDefaultParam(result, params, "useUnicode", "true");
+ setDefaultParam(result, params, "characterEncoding", "utf-8");
+ return result.toString();
+ }
+
+ private static void setDefaultParam(StringBuilder jdbcUrl, Set<String>
params, String param, String value) {
+ if (params.contains(param)) {
+ return;
+ }
+ char lastChar = jdbcUrl.charAt(jdbcUrl.length() - 1);
+ if (lastChar != '?' && lastChar != '&') {
+ jdbcUrl.append(jdbcUrl.indexOf("?") < 0 ? '?' : '&');
+ }
+ jdbcUrl.append(param).append('=').append(value);
+ }
+
+ private static Set<String> getParams(String jdbcUrl) {
+ Set<String> params = new HashSet<>();
+ int queryIndex = jdbcUrl.indexOf('?');
+ if (queryIndex < 0) {
+ return params;
+ }
+ for (String pair : jdbcUrl.substring(queryIndex + 1).split("&")) {
+ int equalsIndex = pair.indexOf('=');
+ String name = equalsIndex < 0 ? pair : pair.substring(0,
equalsIndex);
+ if (!name.isEmpty()) {
+ params.add(name);
+ }
+ }
+ return params;
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcTvfSourceOffsetProvider.java
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcTvfSourceOffsetProvider.java
index 2376d3bd65b..0e5bb8fb753 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcTvfSourceOffsetProvider.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcTvfSourceOffsetProvider.java
@@ -27,6 +27,7 @@ import org.apache.doris.job.cdc.split.SnapshotSplit;
import org.apache.doris.job.common.DataSourceType;
import org.apache.doris.job.exception.JobException;
import org.apache.doris.job.extensions.insert.streaming.StreamingInsertJob;
+import
org.apache.doris.job.extensions.insert.streaming.StreamingJdbcUrlNormalizer;
import org.apache.doris.job.offset.Offset;
import org.apache.doris.job.util.StreamingJobUtils;
import org.apache.doris.nereids.analyzer.UnboundTVFRelation;
@@ -105,6 +106,8 @@ public class JdbcTvfSourceOffsetProvider extends
JdbcSourceOffsetProvider {
// Populate default slot/pub into sourceProperties so cleanMeta ->
/api/close
// carries the resolved names for cdcclient ownership-based cleanup.
Map<String, String> effective = new HashMap<>(originTvfProps);
+ effective.put(DataSourceConfigKeys.JDBC_URL,
StreamingJdbcUrlNormalizer.normalize(
+ resolvedType, effective.get(DataSourceConfigKeys.JDBC_URL)));
StreamingJobUtils.populateDefaultSourceProperties(resolvedType,
effective, String.valueOf(jobId));
// Always refresh fields that may be updated via ALTER JOB (e.g.
credentials, parallelism).
this.sourceProperties = effective;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
index 6dec7cd6bef..e4323cef82a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
@@ -27,6 +27,7 @@ import org.apache.doris.job.cdc.DataSourceConfigKeys;
import org.apache.doris.job.cdc.request.FetchRecordRequest;
import org.apache.doris.job.common.DataSourceType;
import
org.apache.doris.job.extensions.insert.streaming.DataSourceConfigValidator;
+import
org.apache.doris.job.extensions.insert.streaming.StreamingJdbcUrlNormalizer;
import org.apache.doris.job.util.StreamingJobUtils;
import org.apache.doris.thrift.TBrokerFileStatus;
import org.apache.doris.thrift.TFileType;
@@ -68,14 +69,17 @@ public class CdcStreamTableValuedFunction extends
ExternalFileTableValuedFunctio
copyProps.remove(INCLUDE_DELETE_SIGN);
copyProps.put("format", "json");
+ DataSourceType sourceType = DataSourceType.valueOf(
+ copyProps.get(DataSourceConfigKeys.TYPE).toUpperCase());
+ copyProps.put(DataSourceConfigKeys.JDBC_URL,
StreamingJdbcUrlNormalizer.normalize(
+ sourceType, copyProps.get(DataSourceConfigKeys.JDBC_URL)));
+
// Standalone TVF: random jobId. TVF-in-job: job.id injected by
rewriteTvfParams.
String jobId = copyProps.computeIfAbsent(JOB_ID_KEY,
k -> UUID.randomUUID().toString().replace("-", ""));
// Default PG slot/pub so cdcclient auto-creates per-job resources
- StreamingJobUtils.populateDefaultSourceProperties(
-
DataSourceType.valueOf(copyProps.get(DataSourceConfigKeys.TYPE).toUpperCase()),
- copyProps, jobId);
+ StreamingJobUtils.populateDefaultSourceProperties(sourceType,
copyProps, jobId);
super.parseCommonProperties(copyProps);
this.processedParams.put(ENABLE_CDC_CLIENT_KEY, "true");
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingJdbcUrlNormalizerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingJdbcUrlNormalizerTest.java
new file mode 100644
index 00000000000..bfdc7ec695d
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingJdbcUrlNormalizerTest.java
@@ -0,0 +1,78 @@
+// 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.job.extensions.insert.streaming;
+
+import org.apache.doris.job.common.DataSourceType;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class StreamingJdbcUrlNormalizerTest {
+
+ @Test
+ public void testNormalizeMysqlJdbcUrl() {
+ String jdbcUrl = StreamingJdbcUrlNormalizer.normalize(
+ DataSourceType.MYSQL, "jdbc:mysql://127.0.0.1:3306/test");
+
+
Assert.assertEquals("jdbc:mysql://127.0.0.1:3306/test?yearIsDateType=false"
+ +
"&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf-8",
+ jdbcUrl);
+ Assert.assertFalse(jdbcUrl.contains("rewriteBatchedStatements"));
+ }
+
+ @Test
+ public void testNormalizeMysqlJdbcUrlPreservesExplicitValues() {
+ String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/test?tinyInt1isBit=true"
+ +
"&yearIsDateType=true&useUnicode=false&characterEncoding=GBK";
+
+ Assert.assertEquals(jdbcUrl,
+ StreamingJdbcUrlNormalizer.normalize(DataSourceType.MYSQL,
jdbcUrl));
+
+ String partialJdbcUrl =
"jdbc:mysql://127.0.0.1:3306/test?yearIsDateType=true";
+ Assert.assertEquals(partialJdbcUrl + "&tinyInt1isBit=false"
+ + "&useUnicode=true&characterEncoding=utf-8",
+ StreamingJdbcUrlNormalizer.normalize(DataSourceType.MYSQL,
partialJdbcUrl));
+ }
+
+ @Test
+ public void testNormalizeMysqlJdbcUrlMatchesExactParameterNames() {
+ String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/test?YEARISDATETYPE=true"
+ + "&custom=characterEncoding=utf-8";
+
+ Assert.assertEquals(jdbcUrl +
"&yearIsDateType=false&tinyInt1isBit=false"
+ + "&useUnicode=true&characterEncoding=utf-8",
+ StreamingJdbcUrlNormalizer.normalize(DataSourceType.MYSQL,
jdbcUrl));
+ }
+
+ @Test
+ public void testNormalizeMysqlJdbcUrlIsIdempotent() {
+ String jdbcUrl =
"jdbc:mysql://127.0.0.1:3306/test?yearIsDateType=false"
+ +
"&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf-8";
+
+ Assert.assertEquals(jdbcUrl,
+ StreamingJdbcUrlNormalizer.normalize(DataSourceType.MYSQL,
jdbcUrl));
+ }
+
+ @Test
+ public void testNormalizePostgresJdbcUrlDoesNotChange() {
+ String jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/test";
+
+ Assert.assertEquals(jdbcUrl,
+ StreamingJdbcUrlNormalizer.normalize(DataSourceType.POSTGRES,
jdbcUrl));
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/job/offset/jdbc/JdbcTvfSourceOffsetProviderTest.java
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/jdbc/JdbcTvfSourceOffsetProviderTest.java
new file mode 100644
index 00000000000..09f336fc442
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/jdbc/JdbcTvfSourceOffsetProviderTest.java
@@ -0,0 +1,46 @@
+// 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.job.offset.jdbc;
+
+import org.apache.doris.job.cdc.DataSourceConfigKeys;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class JdbcTvfSourceOffsetProviderTest {
+
+ @Test
+ public void testEnsureInitializedNormalizesMysqlJdbcUrl() throws Exception
{
+ JdbcTvfSourceOffsetProvider provider = new
JdbcTvfSourceOffsetProvider();
+ Map<String, String> properties = new HashMap<>();
+ properties.put(DataSourceConfigKeys.TYPE, "mysql");
+ properties.put(DataSourceConfigKeys.JDBC_URL,
"jdbc:mysql://127.0.0.1:3306/test");
+ properties.put(DataSourceConfigKeys.TABLE, "source_table");
+
+ provider.ensureInitialized(1L, properties);
+
+
Assert.assertEquals("jdbc:mysql://127.0.0.1:3306/test?yearIsDateType=false"
+ +
"&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf-8",
+
provider.getSourceProperties().get(DataSourceConfigKeys.JDBC_URL));
+ Assert.assertEquals("jdbc:mysql://127.0.0.1:3306/test",
+ properties.get(DataSourceConfigKeys.JDBC_URL));
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunctionTest.java
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunctionTest.java
index 1be26fa69da..09eb838f759 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunctionTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunctionTest.java
@@ -22,9 +22,11 @@ import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.datasource.jdbc.client.JdbcClient;
import org.apache.doris.job.cdc.DataSourceConfigKeys;
+import org.apache.doris.job.cdc.request.FetchRecordRequest;
import org.apache.doris.job.common.DataSourceType;
import org.apache.doris.job.util.StreamingJobUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.MockedStatic;
@@ -37,6 +39,8 @@ import java.util.Map;
public class CdcStreamTableValuedFunctionTest {
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
@Test
public void testDeleteSignIsExcludedByDefault() throws Exception {
List<Column> columns = getTableColumns(baseProperties());
@@ -70,6 +74,17 @@ public class CdcStreamTableValuedFunctionTest {
Assert.assertTrue(exception.getMessage().contains("include_delete_sign"));
}
+ @Test
+ public void testMysqlJdbcUrlIsNormalizedInPayload() throws Exception {
+ CdcStreamTableValuedFunction function = new
CdcStreamTableValuedFunction(baseProperties());
+
+ FetchRecordRequest request = OBJECT_MAPPER.readValue(
+ function.getBackendConnectProperties().get("http.payload"),
FetchRecordRequest.class);
+
Assert.assertEquals("jdbc:mysql://localhost:3306/test_db?yearIsDateType=false"
+ +
"&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf-8",
+ request.getConfig().get(DataSourceConfigKeys.JDBC_URL));
+ }
+
private List<Column> getTableColumns(Map<String, String> properties)
throws Exception {
JdbcClient jdbcClient = Mockito.mock(JdbcClient.class);
List<Column> sourceColumns = new ArrayList<>();
diff --git
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
index 53c74af0cb6..b9277075513 100644
---
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
+++
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
@@ -102,6 +102,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.mysql.cj.conf.ConnectionUrl;
+import com.mysql.cj.conf.PropertyKey;
import io.debezium.connector.mysql.MySqlConnection;
import io.debezium.connector.mysql.MySqlConnectorConfig;
import io.debezium.connector.mysql.MySqlPartition;
@@ -1018,6 +1019,13 @@ public class MySqlSourceReader extends
AbstractCdcSourceReader {
// Keep genuinely ancient (<100) DATE/DATETIME years; MySQL already
completes 2-digit years.
dbzProps.setProperty("enable.time.adjuster", "false");
+ // The converter is valid only when snapshot JDBC exposes YEAR values
as numbers.
+ if ("false"
+ .equalsIgnoreCase(
+
jdbcProperteis.getProperty(PropertyKey.yearIsDateType.getKeyName()))) {
+ dbzProps.setProperty("converters", "dorisYear");
+ dbzProps.setProperty("dorisYear.type",
MySqlYearConverter.class.getName());
+ }
configFactory.debeziumProperties(dbzProps);
configFactory.heartbeatInterval(Duration.ofMillis(DEBEZIUM_HEARTBEAT_INTERVAL_MS));
diff --git
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlYearConverter.java
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlYearConverter.java
new file mode 100644
index 00000000000..f5aa1646639
--- /dev/null
+++
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlYearConverter.java
@@ -0,0 +1,70 @@
+// 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.cdcclient.source.reader.mysql;
+
+import org.apache.kafka.connect.data.SchemaBuilder;
+
+import java.sql.Date;
+import java.util.Properties;
+
+import io.debezium.spi.converter.CustomConverter;
+import io.debezium.spi.converter.RelationalColumn;
+
+/** Preserves MySQL's special zero YEAR value in both snapshot and binlog
records. */
+public class MySqlYearConverter implements CustomConverter<SchemaBuilder,
RelationalColumn> {
+ private static final int BINLOG_ZERO_YEAR = 1900;
+
+ @Override
+ public void configure(Properties props) {}
+
+ @Override
+ public void converterFor(
+ RelationalColumn field, ConverterRegistration<SchemaBuilder>
registration) {
+ if (!"YEAR".equalsIgnoreCase(field.typeName())) {
+ return;
+ }
+ String qualifiedColumnName = field.dataCollection() + "." +
field.name();
+ registration.register(
+ io.debezium.time.Year.builder(),
+ value -> MySqlYearConverter.convertYear(value,
qualifiedColumnName));
+ }
+
+ static Integer convertYear(Object value, String qualifiedColumnName) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof java.time.Year) {
+ int year = ((java.time.Year) value).getValue();
+ return year == BINLOG_ZERO_YEAR ? 0 : year;
+ }
+ if (value instanceof Date) {
+ return ((Date) value).toLocalDate().getYear();
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof String) {
+ return Integer.valueOf((String) value);
+ }
+ throw new IllegalArgumentException(
+ "Unsupported value type for MySQL YEAR column "
+ + qualifiedColumnName
+ + ": "
+ + value.getClass().getName());
+ }
+}
diff --git
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
index 2422d6c7c98..4136e4e617a 100644
---
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
+++
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
@@ -20,6 +20,7 @@ package org.apache.doris.cdcclient.source.reader.mysql;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -165,6 +166,34 @@ public class MySqlSourceReaderTest {
assertTrue(reader.getTableSchemas().containsKey(otherTableId));
}
+ @Test
+ void mysqlConfigRegistersYearConverterOnlyWhenYearIsDateTypeIsFalse()
throws Exception {
+ MySqlSourceConfig falseConfig =
+ sourceConfig(
+ "initial",
+ Map.of(
+ DataSourceConfigKeys.JDBC_URL,
+
"jdbc:mysql://localhost:3306/testdb?yearIsDateType=false"));
+
+ assertEquals("dorisYear",
falseConfig.getDbzProperties().getProperty("converters"));
+ assertEquals(
+
"org.apache.doris.cdcclient.source.reader.mysql.MySqlYearConverter",
+ falseConfig.getDbzProperties().getProperty("dorisYear.type"));
+
+ MySqlSourceConfig trueConfig =
+ sourceConfig(
+ "initial",
+ Map.of(
+ DataSourceConfigKeys.JDBC_URL,
+
"jdbc:mysql://localhost:3306/testdb?yearIsDateType=true"));
+ assertNull(trueConfig.getDbzProperties().getProperty("converters"));
+
assertNull(trueConfig.getDbzProperties().getProperty("dorisYear.type"));
+
+ MySqlSourceConfig missingConfig = sourceConfig("initial");
+ assertNull(missingConfig.getDbzProperties().getProperty("converters"));
+
assertNull(missingConfig.getDbzProperties().getProperty("dorisYear.type"));
+ }
+
// Drive the real generateMySqlConfig JSON-offset path and return the
rebuilt startup offset.
private BinlogOffset startupBinlogOffset(String offsetJson) throws
Exception {
return sourceConfig(offsetJson).getStartupOptions().binlogOffset;
diff --git
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlYearConverterTest.java
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlYearConverterTest.java
new file mode 100644
index 00000000000..4264567130b
--- /dev/null
+++
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlYearConverterTest.java
@@ -0,0 +1,75 @@
+// 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.cdcclient.source.reader.mysql;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import java.sql.Date;
+import java.time.LocalDate;
+import java.time.Year;
+
+class MySqlYearConverterTest {
+ private static final String YEAR_COLUMN = "inventory.orders.order_year";
+
+ @Test
+ void preservesSnapshotYearZero() {
+ assertEquals(0, MySqlYearConverter.convertYear((short) 0,
YEAR_COLUMN));
+ }
+
+ @Test
+ void restoresBinlogYearZero() {
+ assertEquals(0, MySqlYearConverter.convertYear(Year.of(1900),
YEAR_COLUMN));
+ }
+
+ @Test
+ void preservesRegularYears() {
+ assertEquals(2000, MySqlYearConverter.convertYear((short) 2000,
YEAR_COLUMN));
+ assertEquals(2024, MySqlYearConverter.convertYear(Year.of(2024),
YEAR_COLUMN));
+ }
+
+ @Test
+ void acceptsDateFromYearIsDateType() {
+ assertEquals(
+ 2024,
+ MySqlYearConverter.convertYear(Date.valueOf("2024-01-01"),
YEAR_COLUMN));
+ }
+
+ @Test
+ void preservesNull() {
+ assertNull(MySqlYearConverter.convertYear(null, YEAR_COLUMN));
+ }
+
+ @Test
+ void unsupportedTypeIncludesColumnAndType() {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ MySqlYearConverter.convertYear(
+ LocalDate.of(2024, 1, 1),
YEAR_COLUMN));
+
+ assertEquals(
+ "Unsupported value type for MySQL YEAR column "
+ + "inventory.orders.order_year: java.time.LocalDate",
+ exception.getMessage());
+ }
+}
diff --git
a/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.out
b/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.out
index e4878ae8359..0d7e893c805 100644
---
a/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.out
+++
b/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.out
@@ -12,7 +12,7 @@ decimal4_u text Yes false \N NONE
decimal5_u text Yes false \N NONE
double_u double Yes false \N NONE
float_u float Yes false \N NONE
-boolean boolean Yes false \N NONE
+boolean tinyint Yes false \N NONE
tinyint tinyint Yes false \N NONE
smallint smallint Yes false \N NONE
mediumint int Yes false \N NONE
@@ -46,9 +46,8 @@ varbinary text Yes false \N NONE
enum text Yes false \N NONE
-- !select_all_types_null --
-1 120 50000 1000000 9000000000 1000 12345.67
987654.12345 123456789.1234567890 99999999.123456789012345678901234567890
123.45 12.34 true -5 -300 -20000 -500000 -8000000000 -123.45
-12.34 -1000 -1234.56 -98765.43210 -123456789.1234567890
-99999999.123456789012345678901234567890 2023 08:30 08:30:00.123
08:30:00.123456 2023-06-15 2023-06-15T08:30 2023-06-15T08:30
2023-06-15T08:30:00.123 2023-06-15T08:30:00.123456 abc user_001
normal text content c2ltcGxlIGJsb2IgZGF0YQ== {"id": 1, "name": "u
[...]
+1 120 50000 1000000 9000000000 1000 12345.67
987654.12345 123456789.1234567890 99999999.123456789012345678901234567890
123.45 12.34 1 -5 -300 -20000 -500000 -8000000000 -123.45
-12.34 -1000 -1234.56 -98765.43210 -123456789.1234567890
-99999999.123456789012345678901234567890 0 08:30 08:30:00.123
08:30:00.123456 2023-06-15 2023-06-15T08:30 2023-06-15T08:30
2023-06-15T08:30:00.123 2023-06-15T08:30:00.123456 abc user_001
normal text content c2ltcGxlIGJsb2IgZGF0YQ== {"id": 1, "name":
"userA"} [...]
-- !select_all_types_null2 --
-1 120 50000 1000000 9000000000 1000 12345.67
987654.12345 123456789.1234567890 99999999.123456789012345678901234567890
123.45 12.34 true -5 -300 -20000 -500000 -8000000000 -123.45
-12.34 -1000 -1234.56 -98765.43210 -123456789.1234567890
-99999999.123456789012345678901234567890 2023 08:30 08:30:00.123
08:30:00.123456 2023-06-15 2023-06-15T08:30 2023-06-15T08:30
2023-06-15T08:30:00.123 2023-06-15T08:30:00.123456 abc user_001
normal text content c2ltcGxlIGJsb2IgZGF0YQ== {"id": 1, "name": "u
[...]
-2 100 100000 1000000000 100000000000 12345 12345.67
123456789.12345 12345678901234567890.1234567890
123456789012345678901234567890.123456789012345678901234567890 12345.6789
123.456 true -10 -1000 -100000 -100000000 -1000000000000
-12345.6789 -123.456 -12345 -12345.67 -123456789.12345
-12345678901234567890.1234567890
-123456789012345678901234567890.123456789012345678901234567890 2024
12:34:56 12:34:56.789 12:34:56.789123 2024-01-01
2024-01-01T12:34:56 2024-01-01T12:34:56 2024-01-01T12:34: [...]
-
+1 120 50000 1000000 9000000000 1000 12345.67
987654.12345 123456789.1234567890 99999999.123456789012345678901234567890
123.45 12.34 1 -5 -300 -20000 -500000 -8000000000 -123.45
-12.34 -1000 -1234.56 -98765.43210 -123456789.1234567890
-99999999.123456789012345678901234567890 0 08:30 08:30:00.123
08:30:00.123456 2023-06-15 2023-06-15T08:30 2023-06-15T08:30
2023-06-15T08:30:00.123 2023-06-15T08:30:00.123456 abc user_001
normal text content c2ltcGxlIGJsb2IgZGF0YQ== {"id": 1, "name":
"userA"} [...]
+2 100 100000 1000000000 100000000000 12345 12345.67
123456789.12345 12345678901234567890.1234567890
123456789012345678901234567890.123456789012345678901234567890 12345.6789
123.456 1 -10 -1000 -100000 -100000000 -1000000000000
-12345.6789 -123.456 -12345 -12345.67 -123456789.12345
-12345678901234567890.1234567890
-123456789012345678901234567890.123456789012345678901234567890 2024
12:34:56 12:34:56.789 12:34:56.789123 2024-01-01
2024-01-01T12:34:56 2024-01-01T12:34:56 2024-01-01T12:34:56. [...]
diff --git
a/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.out
b/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.out
index c372d368e72..9b0040492b4 100644
---
a/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.out
+++
b/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.out
@@ -7,25 +7,24 @@ smallint_u int Yes false \N NONE
mediumint_u int Yes false \N NONE
int_u bigint Yes false \N NONE
bigint_u largeint Yes false \N NONE
-bool_col boolean Yes false \N NONE
+tinyint_1 tinyint Yes false \N NONE
tinyint_s tinyint Yes false \N NONE
-- !select_snapshot --
-1 all_zero 0 0 0 0 0 false 0
-2 signed_max 127 32767 8388607 2147483647
9223372036854775807 true 127
-3 signed_max_plus1 128 32768 8388608 2147483648
9223372036854775808 false -128
-4 unsigned_max 255 65535 16777215 4294967295
18446744073709551615 true -1
-5 mid_value 100 30000 8000000 2000000000
9000000000000000000 false 50
+1 all_zero 0 0 0 0 0 0 0
+2 signed_max 127 32767 8388607 2147483647
9223372036854775807 1 127
+3 signed_max_plus1 128 32768 8388608 2147483648
9223372036854775808 -1 -128
+4 unsigned_max 255 65535 16777215 4294967295
18446744073709551615 127 -1
+5 mid_value 100 30000 8000000 2000000000
9000000000000000000 4 50
-- !select_after_incr --
-1 all_zero 0 0 0 0 18446744073709551615
false 0
-2 signed_max 127 32767 8388607 2147483647
9223372036854775807 false 127
-3 signed_max_plus1 128 32768 8388608 2147483648
9223372036854775808 false -128
-4 unsigned_max 255 65535 16777215 4294967295
18446744073709551615 true -1
-5 mid_value 100 30000 8000000 4294967295
9000000000000000000 false 50
-101 all_zero 0 0 0 0 0 false 0
-102 signed_max 127 32767 8388607 2147483647
9223372036854775807 true 127
-103 signed_max_plus1 128 32768 8388608 2147483648
9223372036854775808 false -128
-104 unsigned_max 255 65535 16777215 4294967295
18446744073709551615 true -1
-105 mid_value 100 30000 8000000 2000000000
9000000000000000000 false 50
-
+1 all_zero 0 0 0 0 18446744073709551615
0 0
+2 signed_max 127 32767 8388607 2147483647
9223372036854775807 4 127
+3 signed_max_plus1 128 32768 8388608 2147483648
9223372036854775808 -1 -128
+4 unsigned_max 255 65535 16777215 4294967295
18446744073709551615 127 -1
+5 mid_value 100 30000 8000000 4294967295
9000000000000000000 4 50
+101 all_zero 0 0 0 0 0 0 0
+102 signed_max 127 32767 8388607 2147483647
9223372036854775807 1 127
+103 signed_max_plus1 128 32768 8388608 2147483648
9223372036854775808 -1 -128
+104 unsigned_max 255 65535 16777215 4294967295
18446744073709551615 127 -1
+105 mid_value 100 30000 8000000 2000000000
9000000000000000000 4 50
diff --git
a/regression-test/data/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.out
b/regression-test/data/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.out
index 02b13ffe0b2..17277ad47e7 100644
---
a/regression-test/data/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.out
+++
b/regression-test/data/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.out
@@ -1,11 +1,10 @@
-- This file is automatically generated. You should know what you did if you
want to edit this
-- !select_tvf --
-C1 3
-C1 99
-D1 4
-D1 4
+C1 3 4 0
+C1 99 4 0
+D1 4 -1 2024
+D1 4 -1 2024
-- !select_tvf_dml --
-C1 99
-D1 4
-
+C1 99 4 0
+D1 4 -1 2024
diff --git
a/regression-test/data/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_jdbc_type_defaults.out
b/regression-test/data/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_jdbc_type_defaults.out
new file mode 100644
index 00000000000..c3c7d34ee6e
--- /dev/null
+++
b/regression-test/data/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_jdbc_type_defaults.out
@@ -0,0 +1,14 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !snapshot_data --
+A1 -1 0
+B1 0 2024
+C1 2 0
+D1 4 2025
+
+-- !final_data --
+A1 -1 0
+B1 0 2024
+C1 2 0
+D1 4 2025
+E1 127 0
+F1 -128 2026
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.groovy
index 1955476bee1..0b5753ac930 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_all_type.groovy
@@ -90,9 +90,9 @@ suite("test_streaming_mysql_job_all_type",
"p0,external,mysql,external_docker,ex
`enum` enum('Value1', 'Value2', 'Value3')
) engine=innodb charset=utf8;
"""
- // mock snapshot data
+ // Use numeric YEAR 0 to verify yearIsDateType=false preserves
0000 instead of 2000.
sql """
- insert into ${mysqlDb}.${table1} values
(1,120,50000,1000000,9000000000,1000,12345.67,987654.12345,123456789.1234567890,99999999.123456789012345678901234567890,123.45,12.34,true,-5,-300,-20000,-500000,-8000000000,-123.45,-12.34,-1000,-1234.56,-98765.43210,-123456789.1234567890,-99999999.123456789012345678901234567890,2023,'08:30:00','08:30:00.123','08:30:00.123456','2023-06-15','2023-06-15
08:30:00','2023-06-15 08:30:00','2023-06-15 08:30:00.123','2023-06-15
08:30:00.123456', [...]
+ insert into ${mysqlDb}.${table1} values
(1,120,50000,1000000,9000000000,1000,12345.67,987654.12345,123456789.1234567890,99999999.123456789012345678901234567890,123.45,12.34,true,-5,-300,-20000,-500000,-8000000000,-123.45,-12.34,-1000,-1234.56,-98765.43210,-123456789.1234567890,-99999999.123456789012345678901234567890,0,'08:30:00','08:30:00.123','08:30:00.123456','2023-06-15','2023-06-15
08:30:00','2023-06-15 08:30:00','2023-06-15 08:30:00.123','2023-06-15
08:30:00.123456','ab [...]
"""
}
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_charset_and_strings.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_charset_and_strings.groovy
index dcec811ed27..f89e937f1a6 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_charset_and_strings.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_charset_and_strings.groovy
@@ -31,8 +31,8 @@ import static java.util.concurrent.TimeUnit.SECONDS
// Same themes run twice: ids 1-6 via JDBC/snapshot, ids 101-106 via binlog.
// Plus UPDATEs that switch a row's payload through binlog.
//
-// JDBC URL uses characterEncoding=utf8 to keep the driver round-trip stable;
-// gbk and latin1 columns are converted by MySQL on insert.
+// The streaming job URL omits Unicode and character encoding options to verify
+// that the MySQL defaults are added before metadata discovery and snapshot
reads.
suite("test_streaming_mysql_job_charset_and_strings",
"p0,external,mysql,external_docker,external_docker_mysql,nondatalake") {
def jobName = "test_streaming_mysql_job_charset_and_strings_name"
def currentDb = (sql "select database()")[0][0]
@@ -98,7 +98,7 @@ suite("test_streaming_mysql_job_charset_and_strings",
"p0,external,mysql,externa
sql """CREATE JOB ${jobName}
ON STREAMING
FROM MYSQL (
- "jdbc_url" =
"jdbc:mysql://${externalEnvIp}:${mysql_port}?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8",
+ "jdbc_url" =
"jdbc:mysql://${externalEnvIp}:${mysql_port}?serverTimezone=UTC",
"driver_url" = "${driver_url}",
"driver_class" = "com.mysql.cj.jdbc.Driver",
"user" = "root",
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_create_alter.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_create_alter.groovy
index a0dbd5e36ce..2e6443db633 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_create_alter.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_create_alter.groovy
@@ -334,18 +334,9 @@ suite("test_streaming_mysql_job_create_alter",
"p0,external,mysql,external_docke
test {
sql """ALTER JOB ${jobName}
FROM MYSQL (
- "jdbc_url" = "jdbc:mysql://${externalEnvIp}:${mysql_port}",
- "driver_url" = "${driver_url}",
- "driver_class" = "com.mysql.cj.jdbc.Driver",
- "user" = "root",
- "password" = "123456",
- "database" = "${mysqlDb}",
- "include_tables" = "changeTable",
- "offset" = "initial"
- )
- TO DATABASE ${currentDb} (
- "table.create.properties.replication_num" = "1"
+ "include_tables" = "changeTable"
)
+ TO DATABASE ${currentDb}
"""
exception "The include_tables property cannot be modified in ALTER
JOB"
}
@@ -354,19 +345,9 @@ suite("test_streaming_mysql_job_create_alter",
"p0,external,mysql,external_docke
test {
sql """ALTER JOB ${jobName}
FROM MYSQL (
- "jdbc_url" = "jdbc:mysql://${externalEnvIp}:${mysql_port}",
- "driver_url" = "${driver_url}",
- "driver_class" = "com.mysql.cj.jdbc.Driver",
- "user" = "root",
- "password" = "123456",
- "database" = "${mysqlDb}",
- "include_tables" = "${table1}",
- "exclude_tables" = "xxxx",
- "offset" = "initial"
- )
- TO DATABASE ${currentDb} (
- "table.create.properties.replication_num" = "1"
+ "exclude_tables" = "xxxx"
)
+ TO DATABASE ${currentDb}
"""
exception "The exclude_tables property cannot be modified in ALTER
JOB"
}
@@ -433,15 +414,7 @@ suite("test_streaming_mysql_job_create_alter",
"p0,external,mysql,external_docke
test {
sql """ALTER JOB ${jobName}
FROM MYSQL (
- "jdbc_url" = "jdbc:mysql://${externalEnvIp}:${mysql_port}",
- "driver_url" = "${driver_url}",
- "driver_class" = "com.mysql.cj.jdbc.Driver",
- "user" = "root",
- "password" = "123456",
- "database" = "${mysqlDb}",
- "include_tables" = "${table1}",
- "offset" = "initial",
- "xxx"="xxx"
+ "xxx" = "xxx"
)
TO DATABASE ${currentDb}
"""
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.groovy
index be00ffa7389..80e29d18da0 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_integer_boundary.groovy
@@ -22,7 +22,7 @@ import static java.util.concurrent.TimeUnit.SECONDS
// Guard MySQL integer-type boundaries that all_type does NOT cover:
// 1) UNSIGNED boundaries (esp. BIGINT UNSIGNED >= 2^63, which overflows
Java long)
-// 2) TINYINT(1) <-> BOOLEAN mapping (MySQL ambiguity not present in PG)
+// 2) TINYINT(1) remains TINYINT and preserves non-boolean values
//
// Coverage is run twice: ids 1-5 cover the snapshot path, ids 101-105 repeat
// the same boundary themes through the binlog path. Plus UPDATEs that switch
@@ -60,20 +60,20 @@ suite("test_streaming_mysql_job_integer_boundary",
"p0,external,mysql,external_d
`mediumint_u` mediumint unsigned,
`int_u` int unsigned,
`bigint_u` bigint unsigned,
- `bool_col` tinyint(1),
+ `tinyint_1` tinyint(1),
`tinyint_s` tinyint
) engine=innodb default charset=utf8;
"""
// ----- Snapshot rows: 5 boundary themes via JDBC path -----
- // tinyint_u smallint_u
mediumint_u int_u bigint_u bool tinyint_s
- sql """insert into ${mysqlDb}.${table1} values (1, 'all_zero',
0, 0, 0, 0, 0,
0, 0)"""
- sql """insert into ${mysqlDb}.${table1} values (2, 'signed_max',
127, 32767, 8388607, 2147483647, 9223372036854775807,
1, 127)"""
+ // tinyint_u smallint_u
mediumint_u int_u bigint_u tinyint(1) tinyint_s
+ sql """insert into ${mysqlDb}.${table1} values (1, 'all_zero',
0, 0, 0, 0, 0,
0, 0)"""
+ sql """insert into ${mysqlDb}.${table1} values (2, 'signed_max',
127, 32767, 8388607, 2147483647, 9223372036854775807,
1, 127)"""
// signed_max+1: every column passes its signed boundary; bigint_u
steps into Java long overflow range.
- sql """insert into ${mysqlDb}.${table1} values (3,
'signed_max_plus1', 128, 32768, 8388608, 2147483648,
9223372036854775808, 0, -128)"""
+ sql """insert into ${mysqlDb}.${table1} values (3,
'signed_max_plus1', 128, 32768, 8388608, 2147483648,
9223372036854775808, -1, -128)"""
// unsigned_max: each column at its unsigned upper bound. bigint_u
is 2^64-1.
- sql """insert into ${mysqlDb}.${table1} values (4, 'unsigned_max',
255, 65535, 16777215, 4294967295, 18446744073709551615,
1, -1)"""
- sql """insert into ${mysqlDb}.${table1} values (5, 'mid_value',
100, 30000, 8000000, 2000000000, 9000000000000000000,
0, 50)"""
+ sql """insert into ${mysqlDb}.${table1} values (4, 'unsigned_max',
255, 65535, 16777215, 4294967295, 18446744073709551615,
127, -1)"""
+ sql """insert into ${mysqlDb}.${table1} values (5, 'mid_value',
100, 30000, 8000000, 2000000000, 9000000000000000000,
4, 50)"""
}
sql """CREATE JOB ${jobName}
@@ -113,18 +113,18 @@ suite("test_streaming_mysql_job_integer_boundary",
"p0,external,mysql,external_d
// Verify type mapping in Doris (tinyint_u->smallint,
bigint_u->largeint, etc.)
qt_desc_integer_boundary """desc ${currentDb}.${table1};"""
- qt_select_snapshot """select id, tag, tinyint_u, smallint_u,
mediumint_u, int_u, bigint_u, bool_col, tinyint_s from ${currentDb}.${table1}
order by id;"""
+ qt_select_snapshot """select id, tag, tinyint_u, smallint_u,
mediumint_u, int_u, bigint_u, tinyint_1, tinyint_s from ${currentDb}.${table1}
order by id;"""
// ===== Binlog phase: repeat the SAME 5 boundary themes through
binlog path =====
connect("root", "123456",
"jdbc:mysql://${externalEnvIp}:${mysql_port}") {
- sql """insert into ${mysqlDb}.${table1} values (101, 'all_zero',
0, 0, 0, 0, 0,
0, 0)"""
- sql """insert into ${mysqlDb}.${table1} values (102, 'signed_max',
127, 32767, 8388607, 2147483647, 9223372036854775807,
1, 127)"""
- sql """insert into ${mysqlDb}.${table1} values (103,
'signed_max_plus1', 128, 32768, 8388608, 2147483648,
9223372036854775808, 0, -128)"""
- sql """insert into ${mysqlDb}.${table1} values (104,
'unsigned_max', 255, 65535, 16777215, 4294967295,
18446744073709551615, 1, -1)"""
- sql """insert into ${mysqlDb}.${table1} values (105, 'mid_value',
100, 30000, 8000000, 2000000000, 9000000000000000000,
0, 50)"""
+ sql """insert into ${mysqlDb}.${table1} values (101, 'all_zero',
0, 0, 0, 0, 0,
0, 0)"""
+ sql """insert into ${mysqlDb}.${table1} values (102, 'signed_max',
127, 32767, 8388607, 2147483647, 9223372036854775807,
1, 127)"""
+ sql """insert into ${mysqlDb}.${table1} values (103,
'signed_max_plus1', 128, 32768, 8388608, 2147483648,
9223372036854775808, -1, -128)"""
+ sql """insert into ${mysqlDb}.${table1} values (104,
'unsigned_max', 255, 65535, 16777215, 4294967295,
18446744073709551615, 127, -1)"""
+ sql """insert into ${mysqlDb}.${table1} values (105, 'mid_value',
100, 30000, 8000000, 2000000000, 9000000000000000000,
4, 50)"""
sql """update ${mysqlDb}.${table1} set
bigint_u=18446744073709551615 where id=1"""
- sql """update ${mysqlDb}.${table1} set bool_col=0 where id=2"""
+ sql """update ${mysqlDb}.${table1} set tinyint_1=4 where id=2"""
sql """update ${mysqlDb}.${table1} set int_u=4294967295 where
id=5"""
}
@@ -135,15 +135,16 @@ suite("test_streaming_mysql_job_integer_boundary",
"p0,external,mysql,external_d
{
def cnt = sql """select count(1) from
${currentDb}.${table1}"""
def upd1 = sql """select cast(bigint_u as string) from
${currentDb}.${table1} where id=1"""
- def upd2 = sql """select bool_col from
${currentDb}.${table1} where id=2"""
+ def upd2 = sql """select tinyint_1 from
${currentDb}.${table1} where id=2"""
def upd5 = sql """select int_u from
${currentDb}.${table1} where id=5"""
def b1 = upd1.get(0).get(0) == null ? '' :
upd1.get(0).get(0).toString()
- def b2 = upd2.get(0).get(0)
+ def t2 = upd2.get(0).get(0)
def b5 = upd5.get(0).get(0)
- log.info("incr count=" + cnt + " id1.bigint_u=" + b1 +
" id2.bool_col=" + b2 + " id5.int_u=" + b5)
+ log.info("incr count=" + cnt + " id1.bigint_u=" + b1
+ + " id2.tinyint_1=" + t2 + " id5.int_u=" + b5)
cnt.get(0).get(0) == 10 &&
b1 == '18446744073709551615' &&
- b2 != null && b2.toString() == 'false' &&
+ t2 != null && t2.toString() == '4' &&
b5 != null && b5.toString() == '4294967295'
}
)
@@ -155,7 +156,7 @@ suite("test_streaming_mysql_job_integer_boundary",
"p0,external,mysql,external_d
throw ex
}
- qt_select_after_incr """select id, tag, tinyint_u, smallint_u,
mediumint_u, int_u, bigint_u, bool_col, tinyint_s from ${currentDb}.${table1}
order by id;"""
+ qt_select_after_incr """select id, tag, tinyint_u, smallint_u,
mediumint_u, int_u, bigint_u, tinyint_1, tinyint_s from ${currentDb}.${table1}
order by id;"""
sql """DROP JOB IF EXISTS where jobname = '${jobName}'"""
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_special_offset.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_special_offset.groovy
index 3c988239225..c4234913661 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_special_offset.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_special_offset.groovy
@@ -182,18 +182,9 @@ suite("test_streaming_mysql_job_special_offset",
"p0,external,mysql,external_doc
test {
sql """ALTER JOB ${jobName}
FROM MYSQL (
- "jdbc_url" =
"jdbc:mysql://${externalEnvIp}:${mysql_port}",
- "driver_url" = "${driver_url}",
- "driver_class" = "com.mysql.cj.jdbc.Driver",
- "user" = "root",
- "password" = "123456",
- "database" = "${mysqlDb}",
- "include_tables" = "${table1}",
"offset" = "latest"
)
- TO DATABASE ${currentDb} (
- "table.create.properties.replication_num" = "1"
- )
+ TO DATABASE ${currentDb}
"""
exception "The offset in source properties cannot be modified in
ALTER JOB"
}
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
index ba6f0eacac3..744ab7539ac 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
@@ -146,17 +146,20 @@ suite("test_cdc_stream_tvf_mysql",
"p0,external,mysql,external_docker,external_d
sql """CREATE TABLE ${mysqlDb}.${table1} (
`name` varchar(200) NOT NULL,
`age` int DEFAULT NULL,
+ `tinyint_1` tinyint(1) DEFAULT NULL,
+ `year_zero` year DEFAULT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB"""
- sql """INSERT INTO ${mysqlDb}.${table1} (name, age) VALUES ('A1',
1);"""
- sql """INSERT INTO ${mysqlDb}.${table1} (name, age) VALUES ('B1',
2);"""
+ sql """INSERT INTO ${mysqlDb}.${table1} VALUES ('A1', 1, 0, 0);"""
+ sql """INSERT INTO ${mysqlDb}.${table1} VALUES ('B1', 2, 1,
2023);"""
def result = sql_return_maparray "show master status"
def file = result[0]["File"]
def position = result[0]["Position"]
offset = """{"file":"${file}","pos":"${position}"}"""
- sql """INSERT INTO ${mysqlDb}.${table1} (name, age) VALUES ('C1',
3);"""
- sql """INSERT INTO ${mysqlDb}.${table1} (name, age) VALUES ('D1',
4);"""
+ // Values 4 and YEAR 0 distinguish numeric JDBC semantics from
boolean/date semantics.
+ sql """INSERT INTO ${mysqlDb}.${table1} VALUES ('C1', 3, 4, 0);"""
+ sql """INSERT INTO ${mysqlDb}.${table1} VALUES ('D1', 4, -1,
2024);"""
// capture offset before UPDATE/DELETE events
def result2 = sql_return_maparray "show master status"
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_jdbc_type_defaults.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_jdbc_type_defaults.groovy
new file mode 100644
index 00000000000..b246d653627
--- /dev/null
+++
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_jdbc_type_defaults.groovy
@@ -0,0 +1,126 @@
+// 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.
+
+
+import org.awaitility.Awaitility
+
+import static java.util.concurrent.TimeUnit.SECONDS
+
+/**
+ * Verify that a MySQL cdc_stream job uses numeric TINYINT(1) and YEAR
semantics
+ * consistently while fetching snapshot splits and scanning snapshot/binlog
rows.
+ */
+suite("test_streaming_job_cdc_stream_mysql_jdbc_type_defaults",
+ "p0,external,mysql,external_docker,external_docker_mysql,nondatalake")
{
+ def jobName = "test_cdc_stream_mysql_jdbc_defaults_job"
+ def currentDb = (sql "select database()")[0][0]
+
+ sql """DROP JOB IF EXISTS where jobname = '${jobName}'"""
+ sql """DROP TABLE IF EXISTS
${currentDb}.test_cdc_stream_mysql_jdbc_defaults_tbl FORCE"""
+
+ sql """
+ CREATE TABLE ${currentDb}.test_cdc_stream_mysql_jdbc_defaults_tbl (
+ `name` varchar(32) NULL,
+ `tinyint_value` tinyint NULL,
+ `year_value` smallint NULL
+ ) ENGINE=OLAP
+ DUPLICATE KEY(`name`)
+ DISTRIBUTED BY HASH(`name`) BUCKETS AUTO
+ PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+ """
+
+ String enabled = context.config.otherConfigs.get("enableJdbcTest")
+ if (enabled != null && enabled.equalsIgnoreCase("true")) {
+ String mysqlPort = context.config.otherConfigs.get("mysql_57_port")
+ String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+ String s3Endpoint = getS3Endpoint()
+ String bucket = getS3BucketName()
+ String driverUrl =
"https://${bucket}.${s3Endpoint}/regression/jdbc_driver/mysql-connector-j-8.4.0.jar"
+
+ connect("root", "123456",
"jdbc:mysql://${externalEnvIp}:${mysqlPort}") {
+ sql """CREATE DATABASE IF NOT EXISTS test_cdc_db"""
+ sql """DROP TABLE IF EXISTS
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src"""
+ sql """
+ CREATE TABLE
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src (
+ `name` varchar(32) NOT NULL,
+ `tinyint_value` tinyint(1) NOT NULL,
+ `year_value` year NOT NULL
+ ) ENGINE=InnoDB
+ """
+ sql """INSERT INTO
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src VALUES ('A1', -1, 0)"""
+ sql """INSERT INTO
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src VALUES ('B1', 0, 2024)"""
+ sql """INSERT INTO
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src VALUES ('C1', 2, 0)"""
+ sql """INSERT INTO
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src VALUES ('D1', 4, 2025)"""
+ }
+
+ sql """
+ CREATE JOB ${jobName}
+ ON STREAMING DO INSERT INTO
${currentDb}.test_cdc_stream_mysql_jdbc_defaults_tbl
+ (name, tinyint_value, year_value)
+ SELECT name, tinyint_value, year_value FROM cdc_stream(
+ "type" = "mysql",
+ "jdbc_url" =
"jdbc:mysql://${externalEnvIp}:${mysqlPort}",
+ "driver_url" = "${driverUrl}",
+ "driver_class" = "com.mysql.cj.jdbc.Driver",
+ "user" = "root",
+ "password" = "123456",
+ "database" = "test_cdc_db",
+ "table" =
"test_cdc_stream_mysql_jdbc_defaults_src",
+ "offset" = "initial",
+ "snapshot_split_key" = "tinyint_value",
+ "snapshot_split_size" = "1"
+ )
+ """
+
+ try {
+ Awaitility.await().atMost(300, SECONDS).pollInterval(2,
SECONDS).until({
+ def rows = sql """SELECT count(1) FROM
${currentDb}.test_cdc_stream_mysql_jdbc_defaults_tbl"""
+ log.info("snapshot rows: " + rows)
+ (rows.get(0).get(0) as int) == 4
+ })
+ } catch (Exception ex) {
+ log.info("job: " + (sql """select * from jobs("type"="insert")
where Name='${jobName}'"""))
+ log.info("tasks: " + (sql """select * from tasks("type"="insert")
where JobName='${jobName}'"""))
+ throw ex
+ }
+
+ order_qt_snapshot_data """SELECT * FROM
${currentDb}.test_cdc_stream_mysql_jdbc_defaults_tbl"""
+
+ connect("root", "123456",
"jdbc:mysql://${externalEnvIp}:${mysqlPort}") {
+ sql """INSERT INTO
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src VALUES ('E1', 127, 0)"""
+ sql """INSERT INTO
test_cdc_db.test_cdc_stream_mysql_jdbc_defaults_src VALUES ('F1', -128, 2026)"""
+ }
+
+ try {
+ Awaitility.await().atMost(120, SECONDS).pollInterval(2,
SECONDS).until({
+ def rows = sql """
+ SELECT count(1) FROM
${currentDb}.test_cdc_stream_mysql_jdbc_defaults_tbl
+ WHERE name IN ('E1', 'F1')
+ """
+ log.info("incremental rows: " + rows)
+ (rows.get(0).get(0) as int) == 2
+ })
+ } catch (Exception ex) {
+ log.info("job: " + (sql """select * from jobs("type"="insert")
where Name='${jobName}'"""))
+ log.info("tasks: " + (sql """select * from tasks("type"="insert")
where JobName='${jobName}'"""))
+ throw ex
+ }
+
+ order_qt_final_data """SELECT * FROM
${currentDb}.test_cdc_stream_mysql_jdbc_defaults_tbl"""
+ sql """DROP JOB IF EXISTS where jobname = '${jobName}'"""
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]