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);
+ }
+}