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

github-merge-queue[bot] 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 390c462c83 [Fix][Connector-V2] Apply date, time and datetime formats 
in the XML file sink (#12801)
390c462c83 is described below

commit 390c462c83236c8a5ae8d0a8a401f1536c7c1003
Author: FatMan <[email protected]>
AuthorDate: Fri Oct 16 17:30:55 2026 +0000

    [Fix][Connector-V2] Apply date, time and datetime formats in the XML file 
sink (#12801)
    
    Co-authored-by: chen <[email protected]>
---
 .../introduction/concepts/incompatible-changes.md  |   7 +
 .../introduction/concepts/incompatible-changes.md  |   7 +
 .../seatunnel/file/sink/util/XmlWriter.java        |  21 ++-
 .../seatunnel/file/writer/XmlWriterTest.java       | 141 +++++++++++++++++++++
 4 files changed, 173 insertions(+), 3 deletions(-)

diff --git a/docs/en/introduction/concepts/incompatible-changes.md 
b/docs/en/introduction/concepts/incompatible-changes.md
index 09d4fa1e45..2fe3b2041b 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -5,6 +5,13 @@ You need to check this document before you upgrade to related 
version.
 
 ## dev
 
+### File sink: XML applies `date_format`, `datetime_format` and `time_format`
+
+- **Behavior change: the XML writer of the file sinks formats DATE, TIME and 
TIMESTAMP values with the configured formats**
+  - **Description**: With `file_format_type = "xml"`, DATE, TIME and TIMESTAMP 
values were written with Java `toString()`, ignoring `date_format`, 
`datetime_format` and `time_format`. Timestamps came out as 
`2026-01-02T10:13:45.123`, and as `2026-01-02T10:13` when the seconds were 
zero, so a file could mix several layouts and could not be read back with a 
single `datetime_format`. They are now formatted with the configured options, 
like the CSV, Text and Excel writers.
+  - **Impact**: With the defaults, timestamps are written as `yyyy-MM-dd 
HH:mm:ss` (a space instead of `T`, fractional seconds dropped) and times as 
`HH:mm:ss`. Readers that expect the previous ISO layout must be adjusted.
+  - **Migration Guide**: To keep an ISO layout, set `datetime_format` on the 
sink, for example `YYYY_MM_DD_HH_MM_SS_ISO8601` (`yyyy-MM-dd'T'HH:mm:ss`) or 
`YYYY_MM_DD_HH_MM_SS_SSS_ISO8601` (`yyyy-MM-dd'T'HH:mm:ss.SSS`) to keep 
milliseconds, and read the files with the matching source `datetime_format`.
+
 ### CDC: nullable columns are no longer used as the snapshot split column
 
 - **Behavior change: JDBC-based CDC sources (MySQL, PostgreSQL, Oracle, SQL 
Server, DB2) no longer split the snapshot on a nullable column**
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md 
b/docs/zh/introduction/concepts/incompatible-changes.md
index b0c4680c9e..1586229b9e 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -4,6 +4,13 @@
 
 ## dev
 
+### File Sink:XML 应用 `date_format`、`datetime_format` 和 `time_format`
+
+- **行为变更:文件类 Sink 的 XML 写入器按配置的格式输出 DATE、TIME 和 TIMESTAMP**
+  - **描述**:使用 `file_format_type = "xml"` 时,此前 DATE、TIME 和 TIMESTAMP 直接用 Java 
`toString()` 写出,忽略 `date_format`、`datetime_format` 和 `time_format`。时间戳会写成 
`2026-01-02T10:13:45.123`,秒为 0 时写成 `2026-01-02T10:13`,同一个文件里可能混有多种格式,无法用同一个 
`datetime_format` 读回。现在会按配置的格式输出,与 CSV、Text、Excel 写入器一致。
+  - **影响**:使用默认配置时,时间戳写成 `yyyy-MM-dd HH:mm:ss`(`T` 改为空格,丢弃小数秒),时间写成 
`HH:mm:ss`。依赖原 ISO 格式的下游需要调整。
+  - **迁移指南**:如需保留 ISO 格式,请在 Sink 上配置 `datetime_format`,例如 
`YYYY_MM_DD_HH_MM_SS_ISO8601`(`yyyy-MM-dd'T'HH:mm:ss`),或使用 
`YYYY_MM_DD_HH_MM_SS_SSS_ISO8601`(`yyyy-MM-dd'T'HH:mm:ss.SSS`)保留毫秒,并在读取时为 
Source 配置对应的 `datetime_format`。
+
 ### CDC:可空列不再用作快照分片列
 
 - **行为变更:基于 JDBC 的 CDC 源(MySQL、PostgreSQL、Oracle、SQL Server、DB2)不再使用可空列切分快照**
diff --git 
a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/util/XmlWriter.java
 
b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/util/XmlWriter.java
index 19a35b2142..9964f3c3f9 100644
--- 
a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/util/XmlWriter.java
+++ 
b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sink/util/XmlWriter.java
@@ -21,8 +21,11 @@ import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
 import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated;
+import org.apache.seatunnel.common.utils.DateTimeUtils;
+import org.apache.seatunnel.common.utils.DateUtils;
 import org.apache.seatunnel.common.utils.EncodingUtils;
 import org.apache.seatunnel.common.utils.JsonUtils;
+import org.apache.seatunnel.common.utils.TimeUtils;
 import 
org.apache.seatunnel.connectors.seatunnel.file.exception.FileConnectorException;
 import 
org.apache.seatunnel.connectors.seatunnel.file.sink.config.FileSinkConfig;
 
@@ -36,6 +39,9 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.util.AbstractMap;
 import java.util.List;
 
@@ -48,6 +54,9 @@ public class XmlWriter {
     private final Document document;
     private final Element rootElement;
     private final String fieldDelimiter;
+    private final DateUtils.Formatter dateFormat;
+    private final DateTimeUtils.Formatter dateTimeFormat;
+    private final TimeUtils.Formatter timeFormat;
     private OutputFormat format;
 
     public XmlWriter(
@@ -59,6 +68,9 @@ public class XmlWriter {
         this.seaTunnelRowType = seaTunnelRowType;
 
         this.fieldDelimiter = fileSinkConfig.getFieldDelimiter();
+        this.dateFormat = fileSinkConfig.getDateFormat();
+        this.dateTimeFormat = fileSinkConfig.getDatetimeFormat();
+        this.timeFormat = fileSinkConfig.getTimeFormat();
 
         setXmlOutputFormat();
         document = DocumentHelper.createDocument();
@@ -96,9 +108,6 @@ public class XmlWriter {
 
         switch (fieldType.getSqlType()) {
             case STRING:
-            case DATE:
-            case TIME:
-            case TIMESTAMP:
             case TINYINT:
             case SMALLINT:
             case INT:
@@ -108,6 +117,12 @@ public class XmlWriter {
             case DECIMAL:
             case BOOLEAN:
                 return fieldValue.toString();
+            case DATE:
+                return DateUtils.toString((LocalDate) fieldValue, dateFormat);
+            case TIME:
+                return TimeUtils.toString((LocalTime) fieldValue, timeFormat);
+            case TIMESTAMP:
+                return DateTimeUtils.toString((LocalDateTime) fieldValue, 
dateTimeFormat);
             case NULL:
                 return "";
             case ROW:
diff --git 
a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/XmlWriterTest.java
 
b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/XmlWriterTest.java
new file mode 100644
index 0000000000..24f82e83d0
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/XmlWriterTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.file.writer;
+
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.LocalTimeType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.common.utils.DateTimeUtils;
+import org.apache.seatunnel.common.utils.DateUtils;
+import org.apache.seatunnel.common.utils.TimeUtils;
+import 
org.apache.seatunnel.connectors.seatunnel.file.sink.config.FileSinkConfig;
+import org.apache.seatunnel.connectors.seatunnel.file.sink.util.XmlWriter;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Arrays;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class XmlWriterTest {
+
+    private static final SeaTunnelRowType ROW_TYPE =
+            new SeaTunnelRowType(
+                    new String[] {"id", "c_date", "c_time", "c_timestamp"},
+                    new SeaTunnelDataType[] {
+                        BasicType.INT_TYPE,
+                        LocalTimeType.LOCAL_DATE_TYPE,
+                        LocalTimeType.LOCAL_TIME_TYPE,
+                        LocalTimeType.LOCAL_DATE_TIME_TYPE
+                    });
+
+    // Zero seconds: LocalDateTime/LocalTime#toString() would drop them
+    private static final SeaTunnelRow ROW =
+            new SeaTunnelRow(
+                    new Object[] {
+                        1,
+                        LocalDate.of(2026, 1, 2),
+                        LocalTime.of(10, 13, 0),
+                        LocalDateTime.of(2026, 1, 2, 10, 13, 0)
+                    });
+
+    private FileSinkConfig fileSinkConfig;
+
+    @BeforeEach
+    public void setUp() {
+        fileSinkConfig = mock(FileSinkConfig.class);
+        when(fileSinkConfig.getXmlRootTag()).thenReturn("RECORDS");
+        when(fileSinkConfig.getXmlRowTag()).thenReturn("RECORD");
+        when(fileSinkConfig.getXmlUseAttrFormat()).thenReturn(false);
+        when(fileSinkConfig.getEncoding()).thenReturn("UTF-8");
+        when(fileSinkConfig.getFieldDelimiter()).thenReturn(",");
+        
when(fileSinkConfig.getDateFormat()).thenReturn(DateUtils.Formatter.YYYY_MM_DD);
+        when(fileSinkConfig.getDatetimeFormat())
+                .thenReturn(DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS);
+        
when(fileSinkConfig.getTimeFormat()).thenReturn(TimeUtils.Formatter.HH_MM_SS);
+    }
+
+    @Test
+    public void testDefaultFormatsKeepZeroSeconds() throws IOException {
+        String xml = write();
+
+        Assertions.assertTrue(xml.contains("<c_date>2026-01-02</c_date>"), 
xml);
+        Assertions.assertTrue(xml.contains("<c_time>10:13:00</c_time>"), xml);
+        Assertions.assertTrue(xml.contains("<c_timestamp>2026-01-02 
10:13:00</c_timestamp>"), xml);
+    }
+
+    @Test
+    public void testConfiguredFormatsAreApplied() throws IOException {
+        
when(fileSinkConfig.getDateFormat()).thenReturn(DateUtils.Formatter.YYYY_MM_DD_SPOT);
+        when(fileSinkConfig.getDatetimeFormat())
+                .thenReturn(DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS_SLASH);
+        
when(fileSinkConfig.getTimeFormat()).thenReturn(TimeUtils.Formatter.HH_MM_SS_SSS);
+
+        String xml = write();
+
+        Assertions.assertTrue(xml.contains("<c_date>2026.01.02</c_date>"), 
xml);
+        Assertions.assertTrue(xml.contains("<c_time>10:13:00.000</c_time>"), 
xml);
+        Assertions.assertTrue(xml.contains("<c_timestamp>2026/01/02 
10:13:00</c_timestamp>"), xml);
+    }
+
+    @Test
+    public void testAttributeFormatUsesConfiguredFormats() throws IOException {
+        when(fileSinkConfig.getXmlUseAttrFormat()).thenReturn(true);
+
+        String xml = write();
+
+        Assertions.assertTrue(xml.contains("c_time=\"10:13:00\""), xml);
+        Assertions.assertTrue(xml.contains("c_timestamp=\"2026-01-02 
10:13:00\""), xml);
+    }
+
+    @Test
+    public void testNestedRowUsesConfiguredFormats() throws IOException {
+        SeaTunnelRowType nestedRowType =
+                new SeaTunnelRowType(
+                        new String[] {"id", "c_row"},
+                        new SeaTunnelDataType[] {BasicType.INT_TYPE, 
ROW_TYPE});
+        XmlWriter writer = new XmlWriter(fileSinkConfig, Arrays.asList(0, 1), 
nestedRowType);
+        writer.writeData(new SeaTunnelRow(new Object[] {2, ROW}));
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        writer.flushAndCloseXmlWriter(output);
+        String xml = new String(output.toByteArray(), StandardCharsets.UTF_8);
+
+        // Fields of a nested row are joined with the field delimiter
+        Assertions.assertTrue(
+                xml.contains("<c_row>1,2026-01-02,10:13:00,2026-01-02 
10:13:00</c_row>"), xml);
+    }
+
+    private String write() throws IOException {
+        XmlWriter writer = new XmlWriter(fileSinkConfig, Arrays.asList(0, 1, 
2, 3), ROW_TYPE);
+        writer.writeData(ROW);
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        writer.flushAndCloseXmlWriter(output);
+        return new String(output.toByteArray(), StandardCharsets.UTF_8);
+    }
+}

Reply via email to