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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12541-07f32fffbda5452bbf4a3f4dc6de5e36ca69e5ab
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit d1009b873caed13cffc6a296873e2534d83656eb
Author: hutiefang76 <[email protected]>
AuthorDate: Thu Oct 8 13:35:49 2026 +0000

    [Fix][Connector-V2] Preserve DuckDB TIME microseconds (#12541)
    
    Co-authored-by: David Zollo <[email protected]>
    Co-authored-by: Claude Sonnet 5.5 <[email protected]>
---
 docs/en/connectors/sink/DuckDB.md                  |  2 +
 docs/en/connectors/source/DuckDB.md                |  2 +
 docs/zh/connectors/sink/DuckDB.md                  |  2 +
 docs/zh/connectors/source/DuckDB.md                |  2 +
 .../dialect/duckdb/DuckDBJdbcRowConverter.java     | 18 ++++
 .../dialect/duckdb/DuckDBSourceAndSinkTest.java    | 95 ++++++++++++++++++++++
 6 files changed, 121 insertions(+)

diff --git a/docs/en/connectors/sink/DuckDB.md 
b/docs/en/connectors/sink/DuckDB.md
index e44679be70..60a14d55c8 100644
--- a/docs/en/connectors/sink/DuckDB.md
+++ b/docs/en/connectors/sink/DuckDB.md
@@ -63,6 +63,8 @@ works against a local database file path 
(`jdbc:duckdb:/path/to/database.db`) or
 | TIMESTAMP                                                           | 
TIMESTAMP        |
 | BYTES<br/>ARRAY<br/>ROW<br/>MAP                                     | BLOB   
          |
 
+DuckDB `TIME` values preserve microsecond precision when read or written 
through the JDBC connector. They represent a local time of day without a time 
zone.
+
 ## Sink Options
 
 |                   Name                    |  Type   | Required |           
Default            |                                                            
                                                      Description               
                                                                                
                    |
diff --git a/docs/en/connectors/source/DuckDB.md 
b/docs/en/connectors/source/DuckDB.md
index 64675855bf..1bca13ce7c 100644
--- a/docs/en/connectors/source/DuckDB.md
+++ b/docs/en/connectors/source/DuckDB.md
@@ -74,6 +74,8 @@ MySQL automatic DDL cannot create a full-column primary key 
on `LONGTEXT`. If a
 | TIMESTAMP<br/>TIMESTAMP WITH TIME ZONE                              | 
TIMESTAMP           |
 | BLOB<br/>ARRAY<br/>STRUCT<br/>MAP                                   | BYTES  
             |
 
+DuckDB `TIME` values preserve microsecond precision when read or written 
through the JDBC connector. They represent a local time of day without a time 
zone.
+
 > Type names are matched without regard to case or the JVM default locale. For 
 > example, `integer` and `INTEGER` both map to `INT`, including under `tr-TR`.
 
 ## Source Options
diff --git a/docs/zh/connectors/sink/DuckDB.md 
b/docs/zh/connectors/sink/DuckDB.md
index 8f5df43252..b0894d59e4 100644
--- a/docs/zh/connectors/sink/DuckDB.md
+++ b/docs/zh/connectors/sink/DuckDB.md
@@ -59,6 +59,8 @@ import ChangeLog from '../changelog/connector-jdbc.md';
 | TIMESTAMP                       | TIMESTAMP      |
 | BYTES<br/>ARRAY<br/>ROW<br/>MAP | BLOB           |
 
+JDBC 连接器读取和写入 DuckDB `TIME` 时保留微秒精度。该类型表示不带时区的本地时刻。
+
 ## Sink 选项
 
 | 名称                           | 类型      | 是否必需 | 默认值                          
| 描述                                                                            
              |
diff --git a/docs/zh/connectors/source/DuckDB.md 
b/docs/zh/connectors/source/DuckDB.md
index 78c03ed450..755731a123 100644
--- a/docs/zh/connectors/source/DuckDB.md
+++ b/docs/zh/connectors/source/DuckDB.md
@@ -71,6 +71,8 @@ MySQL 自动建表无法在 `LONGTEXT` 上创建使用完整列值的主键。
 | TIMESTAMP<br/>TIMESTAMP WITH TIME ZONE                   | TIMESTAMP      |
 | BLOB<br/>ARRAY<br/>STRUCT<br/>MAP                        | BYTES          |
 
+JDBC 连接器读取和写入 DuckDB `TIME` 时保留微秒精度。该类型表示不带时区的本地时刻。
+
 > 类型名识别不区分大小写,也不受 JVM 默认区域设置影响。例如,在 `tr-TR` 下,`integer` 和 `INTEGER` 均映射为 `INT`。
 
 ## 源选项
diff --git 
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBJdbcRowConverter.java
 
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBJdbcRowConverter.java
index 59250279af..6c853e6ebc 100644
--- 
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBJdbcRowConverter.java
+++ 
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBJdbcRowConverter.java
@@ -19,12 +19,30 @@ package 
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.duckdb;
 
 import 
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.converter.AbstractJdbcRowConverter;
 import 
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseIdentifier;
+import org.apache.seatunnel.connectors.seatunnel.jdbc.utils.JdbcFieldTypeUtils;
 
 import lombok.extern.slf4j.Slf4j;
 
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.LocalTime;
+
 @Slf4j
 public class DuckDBJdbcRowConverter extends AbstractJdbcRowConverter {
 
+    @Override
+    protected LocalTime readTime(ResultSet resultSet, int index) throws 
SQLException {
+        // java.sql.Time discards DuckDB TIME's fractional seconds.
+        return JdbcFieldTypeUtils.getLocalTime(resultSet, index);
+    }
+
+    @Override
+    protected void writeTime(PreparedStatement statement, int index, LocalTime 
time)
+            throws SQLException {
+        statement.setObject(index, time);
+    }
+
     @Override
     public String converterName() {
         return DatabaseIdentifier.DUCKDB;
diff --git 
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBSourceAndSinkTest.java
 
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBSourceAndSinkTest.java
index 33dd8ed038..cab6f35522 100644
--- 
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBSourceAndSinkTest.java
+++ 
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/dialect/duckdb/DuckDBSourceAndSinkTest.java
@@ -22,6 +22,7 @@ import org.apache.seatunnel.api.sink.DataSaveMode;
 import org.apache.seatunnel.api.sink.SchemaSaveMode;
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.catalog.TablePath;
+import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.common.utils.JdbcUrlUtil;
 import 
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.duckdb.DuckDBCatalog;
@@ -44,10 +45,12 @@ import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.ResultSet;
 import java.sql.Statement;
+import java.time.LocalTime;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.TimeZone;
 
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public class DuckDBSourceAndSinkTest {
@@ -81,6 +84,98 @@ public class DuckDBSourceAndSinkTest {
         }
     }
 
+    @Test
+    public void testSourceTimePrecision() throws Exception {
+        verifyTimePrecision(true);
+    }
+
+    @Test
+    public void testSinkTimePrecision() throws Exception {
+        verifyTimePrecision(false);
+    }
+
+    private void verifyTimePrecision(boolean verifySource) throws Exception {
+        TimeZone original = TimeZone.getDefault();
+        LocalTime[] times = {
+            LocalTime.parse("00:00:00"),
+            LocalTime.parse("00:00:00.000001"),
+            LocalTime.parse("12:34:56.123456"),
+            LocalTime.parse("23:59:59.999999"),
+            null
+        };
+        try {
+            for (String zone : new String[] {"UTC", "Asia/Shanghai", 
"America/New_York"}) {
+                TimeZone.setDefault(TimeZone.getTimeZone(zone));
+                try (Connection connection = 
DriverManager.getConnection(jdbcUrl);
+                        Statement statement = connection.createStatement()) {
+                    statement.execute(
+                            "CREATE OR REPLACE TABLE 
main.time_precision_source (id INTEGER, value TIME)");
+                    statement.execute(
+                            "CREATE OR REPLACE TABLE main.time_precision_sink 
(id INTEGER, value TIME)");
+                    statement.execute(
+                            "INSERT INTO main.time_precision_source VALUES (0, 
TIME '00:00:00'), (1, TIME '00:00:00.000001'), (2, TIME '12:34:56.123456'), (3, 
TIME '23:59:59.999999'), (4, NULL)");
+                }
+                Map<String, Object> sourceOptions = new HashMap<>();
+                sourceOptions.put("url", jdbcUrl);
+                sourceOptions.put("driver", "org.duckdb.DuckDBDriver");
+                sourceOptions.put("table_path", "main.time_precision_source");
+                ReadonlyConfig config = ReadonlyConfig.fromMap(sourceOptions);
+                CatalogTable table =
+                        new JdbcSourceFactory()
+                                .inferSchemaForDryRun(
+                                        new TableSourceFactoryContext(
+                                                config, 
getClass().getClassLoader()))
+                                .get(0);
+                List<SeaTunnelRow> rows =
+                        SourceFlowTestUtils.runBatchWithCheckpointDisabled(
+                                config, new JdbcSourceFactory());
+                Assertions.assertEquals(times.length, rows.size());
+                if (verifySource) {
+                    for (SeaTunnelRow row : rows) {
+                        Assertions.assertEquals(
+                                times[(Integer) row.getField(0)],
+                                row.getField(1),
+                                "Source precision in " + zone);
+                    }
+                }
+                // Use the original values independently of Source so a 
matching truncation on
+                // both sides cannot make the Sink regression pass.
+                List<SeaTunnelRow> sinkRows = new ArrayList<>();
+                for (int id = 0; id < times.length; id++) {
+                    sinkRows.add(new SeaTunnelRow(new Object[] {id, 
times[id]}));
+                }
+                Map<String, Object> sinkOptions = new HashMap<>();
+                sinkOptions.put("url", jdbcUrl);
+                sinkOptions.put("driver", "org.duckdb.DuckDBDriver");
+                sinkOptions.put("query", "INSERT INTO main.time_precision_sink 
VALUES (?, ?)");
+                sinkOptions.put("schema_save_mode", SchemaSaveMode.IGNORE);
+                sinkOptions.put("data_save_mode", DataSaveMode.APPEND_DATA);
+                SinkFlowTestUtils.runBatchWithCheckpointDisabled(
+                        table,
+                        ReadonlyConfig.fromMap(sinkOptions),
+                        new JdbcSinkFactory(),
+                        sinkRows);
+                try (Connection connection = 
DriverManager.getConnection(jdbcUrl);
+                        Statement statement = connection.createStatement();
+                        ResultSet result =
+                                statement.executeQuery(
+                                        "SELECT value::VARCHAR FROM 
main.time_precision_sink ORDER BY id")) {
+                    for (LocalTime time : times) {
+                        Assertions.assertTrue(result.next());
+                        String value = result.getString(1);
+                        Assertions.assertEquals(
+                                time,
+                                value == null ? null : LocalTime.parse(value),
+                                "Sink precision in " + zone);
+                    }
+                    Assertions.assertFalse(result.next());
+                }
+            }
+        } finally {
+            TimeZone.setDefault(original);
+        }
+    }
+
     @SneakyThrows
     @Test
     public void testFlow() {

Reply via email to