This is an automated email from the ASF dual-hosted git repository.
nzw921rx pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 91ba231cb4 [Fix][Connector-V2] Preserve MySQL DATETIME values across
time zones (#13025)
91ba231cb4 is described below
commit 91ba231cb4b524d42e22935d03f194d584cf2fd3
Author: Goutam Adwant <[email protected]>
AuthorDate: Tue Oct 27 22:19:18 2026 -0700
[Fix][Connector-V2] Preserve MySQL DATETIME values across time zones
(#13025)
---
docs/en/connectors/sink/Jdbc.md | 6 ++
.../introduction/concepts/incompatible-changes.md | 6 ++
docs/zh/connectors/sink/Jdbc.md | 6 ++
.../introduction/concepts/incompatible-changes.md | 6 ++
.../converter/AbstractJdbcRowConverter.java | 12 ++-
.../dialect/mysql/MysqlJdbcRowConverter.java | 7 ++
.../dialect/starrocks/StarRocksDialect.java | 20 ++++
.../dialect/mysql/MysqlJdbcRowConverterTest.java | 111 +++++++++++++++++++++
.../seatunnel/jdbc/JdbcMysqlTimestampIT.java | 92 +++++++++++++++++
.../resources/jdbc_mysql_datetime_round_trip.conf | 43 ++++++++
10 files changed, 307 insertions(+), 2 deletions(-)
diff --git a/docs/en/connectors/sink/Jdbc.md b/docs/en/connectors/sink/Jdbc.md
index f5a70fb80f..c7e48e81d5 100644
--- a/docs/en/connectors/sink/Jdbc.md
+++ b/docs/en/connectors/sink/Jdbc.md
@@ -8,6 +8,12 @@ The JDBC Sink connector writes SeaTunnel rows to databases
through a JDBC driver
If this is your first JDBC Sink job, start with [Choose a write
mode](#choose-a-write-mode) and the [Quick start](#quick-start-postgresql). The
complete option reference follows those sections.
+## MySQL temporal values
+
+MySQL `DATETIME` maps to SeaTunnel `TIMESTAMP` (`LocalDateTime`). The MySQL
JDBC sink binds its local date/time fields directly, preserving fractional
seconds supported by the target column even when the worker JVM and JDBC
connection time zones differ. MySQL `TIMESTAMP` maps to `TIMESTAMP_TZ` and
retains its instant-based handling.
+
+When upgrading from a version affected by
[#13024](https://github.com/apache/seatunnel/issues/13024), remove any manual
hour offset added to compensate for shifted `DATETIME` writes. Previously
stored values are not repaired automatically; compare them with the original
source before correcting or reloading data.
+
## Using Dependency
Install the `connector-jdbc` plugin first:
diff --git a/docs/en/introduction/concepts/incompatible-changes.md
b/docs/en/introduction/concepts/incompatible-changes.md
index 9b699028c5..a14cd53669 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -5,6 +5,12 @@ You need to check this document before you upgrade to related
version.
## dev
+### JDBC MySQL: DATETIME writes preserve local date/time fields
+
+The MySQL JDBC sink now binds SeaTunnel `TIMESTAMP` (`LocalDateTime`) directly
instead of converting it through `java.sql.Timestamp`. With different worker
JVM and JDBC connection time zones, affected versions could shift `DATETIME`
values or reject valid boundary values. Writes now preserve the local fields
and the fractional seconds supported by the target column. The `DATETIME`
source mapping and `TIMESTAMP_TZ` handling are unchanged.
+
+Remove any manual offset used to compensate for this defect. Existing stored
values are not changed automatically; verify them against the original source
before correcting or reloading them. No new configuration is required.
+
### Doris and StarRocks sources: a null MAP or ROW column no longer reads as
an empty map or fails the job
- **Behavior change: a null complex cell from an Arrow stream now reads as
`null`**
diff --git a/docs/zh/connectors/sink/Jdbc.md b/docs/zh/connectors/sink/Jdbc.md
index 8b85b2bf1a..a5d5e21c02 100644
--- a/docs/zh/connectors/sink/Jdbc.md
+++ b/docs/zh/connectors/sink/Jdbc.md
@@ -8,6 +8,12 @@ JDBC Sink 通过数据库厂商提供的 JDBC 驱动写入数据。它支持批
第一次配置 JDBC Sink 时,请先阅读[选择写入模式](#选择写入模式)和[快速入门](#快速入门postgresql),再按需查阅后面的完整参数说明。
+## MySQL 时间值
+
+MySQL `DATETIME` 映射为 SeaTunnel `TIMESTAMP`(`LocalDateTime`)。MySQL JDBC Sink
直接绑定本地日期和时间字段,即使 Worker JVM 与 JDBC 连接使用不同的时区,也会保留目标列支持的小数秒精度。MySQL `TIMESTAMP`
映射为 `TIMESTAMP_TZ`,仍按时间点处理。
+
+从受 [#13024](https://github.com/apache/seatunnel/issues/13024) 影响的版本升级时,请移除为补偿
`DATETIME` 写入偏移而手动添加的小时偏移。已经写入的数据不会自动修复;修正或重新加载之前,请先与原始数据进行比较。
+
## 使用依赖
先安装 `connector-jdbc` 插件:
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md
b/docs/zh/introduction/concepts/incompatible-changes.md
index 7a5ceb6b78..021601be5f 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -4,6 +4,12 @@
## dev
+### JDBC MySQL:DATETIME 写入保留本地日期和时间字段
+
+MySQL JDBC Sink 现在直接绑定 SeaTunnel `TIMESTAMP`(`LocalDateTime`),不再通过
`java.sql.Timestamp` 转换。当 Worker JVM 与 JDBC 连接的时区不同时,受影响的版本可能导致 `DATETIME`
值偏移,或拒绝合法的边界值。现在写入会保留本地字段及目标列支持的小数秒精度。`DATETIME` 的源端映射和 `TIMESTAMP_TZ` 的处理保持不变。
+
+请移除为补偿此缺陷而设置的手动偏移。已经存储的数据不会自动修改;修正或重新加载之前,请先与原始数据核对。无需新增配置。
+
### Doris 与 StarRocks Source:MAP 或 ROW 列为空值时不再读成空 Map 或导致作业失败
- **行为变更:Arrow 流中为空的复杂类型单元格现在读为 `null`**
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/converter/AbstractJdbcRowConverter.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/converter/AbstractJdbcRowConverter.java
index c2a3fb04ab..74c8a125c0 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/converter/AbstractJdbcRowConverter.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/converter/AbstractJdbcRowConverter.java
@@ -303,8 +303,7 @@ public abstract class AbstractJdbcRowConverter implements
JdbcRowConverter {
writeTime(statement, statementIndex, (LocalTime) value);
break;
case TIMESTAMP:
- LocalDateTime localDateTime = (LocalDateTime) value;
- statement.setTimestamp(statementIndex,
Timestamp.valueOf(localDateTime));
+ writeTimestamp(statement, statementIndex, (LocalDateTime)
value);
break;
case TIMESTAMP_TZ:
OffsetDateTime offsetDateTime = (OffsetDateTime) value;
@@ -360,4 +359,13 @@ public abstract class AbstractJdbcRowConverter implements
JdbcRowConverter {
throws SQLException {
statement.setTime(index, java.sql.Time.valueOf(time));
}
+
+ /**
+ * Binds a local timestamp using the legacy JDBC representation. Dialects
may override this
+ * method when their driver supports binding the local date/time fields
directly.
+ */
+ protected void writeTimestamp(PreparedStatement statement, int index,
LocalDateTime value)
+ throws SQLException {
+ statement.setTimestamp(index, Timestamp.valueOf(value));
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverter.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverter.java
index a4fe03ca0d..d22ee4ce47 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverter.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverter.java
@@ -46,4 +46,11 @@ public class MysqlJdbcRowConverter extends
AbstractJdbcRowConverter {
statement.setTimestamp(
index,
java.sql.Timestamp.valueOf(LocalDateTime.of(LocalDate.now(), time)));
}
+
+ /** Binds DATETIME fields without interpreting them in the JVM default
time zone. */
+ @Override
+ protected void writeTimestamp(PreparedStatement statement, int index,
LocalDateTime value)
+ throws SQLException {
+ statement.setObject(index, value);
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/starrocks/StarRocksDialect.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/starrocks/StarRocksDialect.java
index dd060fd918..c0e39de965 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/starrocks/StarRocksDialect.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/starrocks/StarRocksDialect.java
@@ -17,7 +17,14 @@
package
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.starrocks;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.converter.JdbcRowConverter;
import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.mysql.MysqlDialect;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.mysql.MysqlJdbcRowConverter;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
public class StarRocksDialect extends MysqlDialect {
@@ -27,6 +34,19 @@ public class StarRocksDialect extends MysqlDialect {
this.fieldIde = fieldIde;
}
+ /** Keeps the existing StarRocks binding while MySQL uses its
DATETIME-specific writer. */
+ @Override
+ public JdbcRowConverter getRowConverter() {
+ return new MysqlJdbcRowConverter() {
+ @Override
+ protected void writeTimestamp(
+ PreparedStatement statement, int index, LocalDateTime
value)
+ throws SQLException {
+ statement.setTimestamp(index, Timestamp.valueOf(value));
+ }
+ };
+ }
+
@Override
public String hashModForField(String fieldName, int mod) {
return "ABS(murmur_hash3_32(" + quoteIdentifier(fieldName) + ") % " +
mod + ")";
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverterTest.java
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverterTest.java
index 15c65b4e03..cb94e8278c 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverterTest.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/mysql/MysqlJdbcRowConverterTest.java
@@ -21,22 +21,133 @@ import
org.apache.seatunnel.api.table.catalog.PhysicalColumn;
import org.apache.seatunnel.api.table.catalog.TableSchema;
import org.apache.seatunnel.api.table.type.LocalTimeType;
import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorException;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.converter.JdbcRowConverter;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.GenericDialect;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.starrocks.StarRocksDialect;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
+import java.sql.SQLException;
import java.sql.Timestamp;
+import java.time.LocalDateTime;
import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.util.Calendar;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MysqlJdbcRowConverterTest {
+ @Test
+ public void testTimestampReadAndWritePreserveLocalFields() throws
Exception {
+ for (LocalDateTime expected :
+ new LocalDateTime[] {
+ LocalDateTime.parse("2026-01-01T12:34:56.123456"),
+ LocalDateTime.parse("2026-03-08T02:30:00.123456"),
+ LocalDateTime.parse("2026-11-01T01:30:00.654321")
+ }) {
+ TableSchema schema =
timestampSchema(LocalTimeType.LOCAL_DATE_TIME_TYPE);
+ MysqlJdbcRowConverter converter = new MysqlJdbcRowConverter();
+ ResultSet resultSet = mock(ResultSet.class);
+ when(resultSet.getObject(1,
LocalDateTime.class)).thenReturn(expected);
+ SeaTunnelRow row = converter.toInternal(resultSet, schema);
+ assertEquals(expected, row.getField(0));
+
+ PreparedStatement statement = mock(PreparedStatement.class);
+ converter.toExternal(schema, schema, row, statement);
+ verify(statement).setObject(1, expected);
+ verify(statement, never()).setTimestamp(eq(1),
any(Timestamp.class));
+ }
+ }
+
+ @Test
+ public void testTimestampNullBindingIsUnchanged() throws Exception {
+ TableSchema schema =
timestampSchema(LocalTimeType.LOCAL_DATE_TIME_TYPE);
+ MysqlJdbcRowConverter converter = new MysqlJdbcRowConverter();
+ ResultSet resultSet = mock(ResultSet.class);
+ SeaTunnelRow row = converter.toInternal(resultSet, schema);
+ assertEquals(null, row.getField(0));
+ PreparedStatement statement = mock(PreparedStatement.class);
+ converter.toExternal(schema, row, statement);
+ verify(statement).setObject(1, null);
+ verify(statement, never()).setTimestamp(eq(1), any(Timestamp.class));
+ }
+
+ @Test
+ public void testTimestampBindingFailureIsNotRetriedWithTimestamp() throws
Exception {
+ TableSchema schema =
timestampSchema(LocalTimeType.LOCAL_DATE_TIME_TYPE);
+ LocalDateTime value =
LocalDateTime.parse("2026-01-01T12:34:56.123456");
+ PreparedStatement statement = mock(PreparedStatement.class);
+ SQLException failure = new SQLException("binding failed", "22007");
+ doThrow(failure).when(statement).setObject(1, value);
+ JdbcConnectorException actual =
+ assertThrows(
+ JdbcConnectorException.class,
+ () ->
+ new MysqlJdbcRowConverter()
+ .toExternal(
+ schema,
+ new SeaTunnelRow(new Object[]
{value}),
+ statement));
+ assertSame(failure, actual.getCause());
+ verify(statement, never()).setTimestamp(eq(1), any(Timestamp.class));
+ }
+
+ @Test
+ public void testDefaultAndStarRocksTimestampBindingsAreUnchanged() throws
Exception {
+ LocalDateTime value =
LocalDateTime.parse("2026-01-01T12:34:56.123456");
+ TableSchema schema =
timestampSchema(LocalTimeType.LOCAL_DATE_TIME_TYPE);
+ for (JdbcRowConverter converter :
+ new JdbcRowConverter[] {
+ new GenericDialect().getRowConverter(), new
StarRocksDialect().getRowConverter()
+ }) {
+ PreparedStatement statement = mock(PreparedStatement.class);
+ converter.toExternal(schema, new SeaTunnelRow(new Object[]
{value}), statement);
+ ArgumentCaptor<Timestamp> timestamp =
ArgumentCaptor.forClass(Timestamp.class);
+ verify(statement).setTimestamp(eq(1), timestamp.capture());
+ assertEquals(value, timestamp.getValue().toLocalDateTime());
+ verify(statement, never()).setObject(1, value);
+ }
+ }
+
+ @Test
+ public void testTimestampTzBindingAndUtcFallbackAreUnchanged() throws
Exception {
+ TableSchema schema =
timestampSchema(LocalTimeType.OFFSET_DATE_TIME_TYPE);
+ OffsetDateTime value =
OffsetDateTime.parse("2026-01-01T12:34:56.123456+08:00");
+ MysqlJdbcRowConverter converter = new MysqlJdbcRowConverter();
+ PreparedStatement statement = mock(PreparedStatement.class);
+ converter.toExternal(schema, new SeaTunnelRow(new Object[] {value}),
statement);
+ verify(statement).setObject(1, value);
+
+ PreparedStatement legacy = mock(PreparedStatement.class);
+ doThrow(new SQLException("unsupported")).when(legacy).setObject(1,
value);
+ converter.toExternal(schema, new SeaTunnelRow(new Object[] {value}),
legacy);
+ ArgumentCaptor<Timestamp> timestamp =
ArgumentCaptor.forClass(Timestamp.class);
+ ArgumentCaptor<Calendar> calendar =
ArgumentCaptor.forClass(Calendar.class);
+ verify(legacy).setTimestamp(eq(1), timestamp.capture(),
calendar.capture());
+ assertEquals(value.toInstant(), timestamp.getValue().toInstant());
+ assertEquals("UTC", calendar.getValue().getTimeZone().getID());
+ }
+
+ private TableSchema timestampSchema(LocalTimeType<?> type) {
+ return TableSchema.builder()
+
.column(PhysicalColumn.builder().name("timestamp_col").dataType(type).build())
+ .build();
+ }
+
@Test
public void testTimeReadAndWritePreserveMicroseconds() throws Exception {
LocalTime expected = LocalTime.parse("12:34:56.123456");
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlTimestampIT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlTimestampIT.java
index dee614aca1..9bd1cfaff5 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlTimestampIT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcMysqlTimestampIT.java
@@ -42,7 +42,11 @@ import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
+import java.sql.ResultSet;
import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@@ -169,6 +173,70 @@ public class JdbcMysqlTimestampIT extends TestSuiteBase
implements TestResource
+ result.getStderr());
}
+ /** Checks stored values independently of JDBC temporal deserialization. */
+ @TestTemplate
+ public void testMysqlDatetimeRoundTrip(TestContainer container) throws
Exception {
+ for (String connectionZone : Arrays.asList("UTC", "Asia/Shanghai")) {
+ for (boolean serverPrepared : Arrays.asList(false, true)) {
+ try (Connection connection = mysqlConnection();
+ Statement statement = connection.createStatement()) {
+ statement.execute("TRUNCATE TABLE datetime_sink");
+ }
+ Container.ExecResult result =
+ container.executeJob(
+ "/jdbc_mysql_datetime_round_trip.conf",
+ Arrays.asList(
+ "connection_zone=" + connectionZone,
+ "server_prepared=" + serverPrepared));
+ Assertions.assertEquals(
+ 0,
+ result.getExitCode(),
+ "DATETIME round trip failed: "
+ + connectionZone
+ + ", serverPrepared="
+ + serverPrepared
+ + "\n"
+ + result.getStderr());
+ Assertions.assertEquals(
+ storedTemporalValues("datetime_source"),
+ storedTemporalValues("datetime_sink"),
+ "Stored temporal values changed: "
+ + connectionZone
+ + ", serverPrepared="
+ + serverPrepared);
+ }
+ }
+ }
+
+ private Connection mysqlConnection() throws Exception {
+ return DriverManager.getConnection(
+ String.format(
+
"jdbc:mysql://%s:%d/%s?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC",
+ mysqlContainer.getHost(),
+ mysqlContainer.getFirstMappedPort(),
+ MYSQL_DATABASE),
+ MYSQL_USER,
+ MYSQL_PASSWORD);
+ }
+
+ private List<String> storedTemporalValues(String table) throws Exception {
+ List<String> values = new ArrayList<>();
+ try (Connection connection = mysqlConnection();
+ Statement statement = connection.createStatement();
+ ResultSet result =
+ statement.executeQuery(
+ "SELECT id, DATE_FORMAT(dt_col, '%Y-%m-%d
%H:%i:%s.%f'), "
+ + "UNIX_TIMESTAMP(ts_col) FROM "
+ + table
+ + " ORDER BY id")) {
+ while (result.next()) {
+ values.add(
+ result.getInt(1) + ":" + result.getString(2) + ":" +
result.getString(3));
+ }
+ }
+ return values;
+ }
+
//
-------------------------------------------------------------------------
// Helpers
//
-------------------------------------------------------------------------
@@ -188,6 +256,30 @@ public class JdbcMysqlTimestampIT extends TestSuiteBase
implements TestResource
+ " dt_col DATETIME,"
+ " ts_col TIMESTAMP NULL"
+ ")");
+ stmt.execute(
+ "CREATE TABLE IF NOT EXISTS datetime_source ("
+ + "id INT PRIMARY KEY, dt_col DATETIME(6), ts_col
TIMESTAMP(6) NULL)");
+ stmt.execute("CREATE TABLE IF NOT EXISTS datetime_sink LIKE
datetime_source");
+ stmt.execute("TRUNCATE TABLE datetime_source");
+ List<String> datetimes =
+ Arrays.asList(
+ "2026-01-01 12:34:56.123456",
+ "2025-12-31 23:59:59.999999",
+ "2026-01-01 00:00:00.000001",
+ "1969-12-31 23:59:59.123456",
+ "2026-03-08 02:30:00.123456",
+ "2026-11-01 01:30:00.654321",
+ "1000-01-01 00:00:00.000000",
+ "9999-12-31 23:59:59.999999");
+ for (int i = 0; i < datetimes.size(); i++) {
+ stmt.execute(
+ "INSERT INTO datetime_source VALUES ("
+ + i
+ + ", '"
+ + datetimes.get(i)
+ + "', '2026-01-01 12:34:56.123456')");
+ }
+ stmt.execute("INSERT INTO datetime_source VALUES (8, NULL, NULL)");
// Insert a fixed wall-clock value: 2026-01-01 00:00:00
// DATETIME stores it as-is (NTZ); TIMESTAMP stores UTC and
displays in session TZ.
stmt.execute(
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_mysql_datetime_round_trip.conf
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_mysql_datetime_round_trip.conf
new file mode 100644
index 0000000000..c242b49f98
--- /dev/null
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/resources/jdbc_mysql_datetime_round_trip.conf
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# Run with UTC and Asia/Shanghai worker JVMs to exercise both mismatch
directions.
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
+source {
+ Jdbc {
+ url =
"jdbc:mysql://mysql_ts_e2e:3306/ts_test?useSSL=false&serverTimezone="${connection_zone}
+ driver = "com.mysql.cj.jdbc.Driver"
+ user = "root"
+ password = "Abc!@#135_seatunnel"
+ query = "SELECT id, dt_col, ts_col FROM datetime_source"
+ }
+}
+
+sink {
+ Jdbc {
+ url =
"jdbc:mysql://mysql_ts_e2e:3306/ts_test?useSSL=false&serverTimezone="${connection_zone}"&useServerPrepStmts="${server_prepared}"&rewriteBatchedStatements=true"
+ driver = "com.mysql.cj.jdbc.Driver"
+ user = "root"
+ password = "Abc!@#135_seatunnel"
+ query = "INSERT INTO datetime_sink (id, dt_col, ts_col) VALUES (?, ?, ?)"
+ batch_size = 3
+ }
+}