This is an automated email from the ASF dual-hosted git repository.
leonardBang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new ad41a62bf9b [FLINK-20746][csv] Add csv.ignore-first-line for
filesystem CSV connector
ad41a62bf9b is described below
commit ad41a62bf9b4223dfd23eb335fd4241c5df58a4e
Author: Vishal Kamlapure <[email protected]>
AuthorDate: Tue Sep 15 08:20:36 2026 +0530
[FLINK-20746][csv] Add csv.ignore-first-line for filesystem CSV connector
This closes #28594.
---
.../docs/connectors/table/formats/csv.md | 8 +++
docs/content/docs/connectors/table/formats/csv.md | 8 +++
.../flink/formats/csv/CsvFileFormatFactory.java | 8 ++-
.../apache/flink/formats/csv/CsvFormatOptions.java | 9 +++
.../flink/formats/csv/CsvFormatFactoryTest.java | 81 ++++++++++++++++++++++
5 files changed, 113 insertions(+), 1 deletion(-)
diff --git a/docs/content.zh/docs/connectors/table/formats/csv.md
b/docs/content.zh/docs/connectors/table/formats/csv.md
index 348c7bd043f..846005a6d10 100644
--- a/docs/content.zh/docs/connectors/table/formats/csv.md
+++ b/docs/content.zh/docs/connectors/table/formats/csv.md
@@ -199,6 +199,14 @@ Format 参数
<td>Boolean</td>
<td>可选标志,用于将空字符串值视为 null(默认禁用)。仅影响反序列化。</td>
</tr>
+ <tr>
+ <td><h5>csv.ignore-first-line</h5></td>
+ <td>可选</td>
+ <td>否</td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>可选标志,用于跳过每个输入 CSV 文件的第一条记录(默认值为 false)。仅支持 filesystem
连接器。仅影响反序列化。</td>
+ </tr>
</tbody>
</table>
diff --git a/docs/content/docs/connectors/table/formats/csv.md
b/docs/content/docs/connectors/table/formats/csv.md
index 7262757459a..f14bf82269c 100644
--- a/docs/content/docs/connectors/table/formats/csv.md
+++ b/docs/content/docs/connectors/table/formats/csv.md
@@ -200,6 +200,14 @@ Format Options
<td>Boolean</td>
<td>Optional flag to treat empty string values as null (disabled by
default). Only affects deserialization.</td>
</tr>
+ <tr>
+ <td><h5>csv.ignore-first-line</h5></td>
+ <td>optional</td>
+ <td>no</td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>Optional flag to skip the first CSV record of each input file (false
by default). Only supported by the filesystem connector. Only affects
deserialization.</td>
+ </tr>
</tbody>
</table>
diff --git
a/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFileFormatFactory.java
b/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFileFormatFactory.java
index e58796bdb9c..d69dd42264e 100644
---
a/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFileFormatFactory.java
+++
b/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFileFormatFactory.java
@@ -58,6 +58,7 @@ import
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.dataformat.csv.Csv
import org.apache.commons.text.StringEscapeUtils;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -69,6 +70,7 @@ import static
org.apache.flink.formats.csv.CsvFormatOptions.EMPTY_STRING_AS_NULL
import static org.apache.flink.formats.csv.CsvFormatOptions.ESCAPE_CHARACTER;
import static
org.apache.flink.formats.csv.CsvFormatOptions.FAIL_ON_MISSING_COLUMNS;
import static org.apache.flink.formats.csv.CsvFormatOptions.FIELD_DELIMITER;
+import static org.apache.flink.formats.csv.CsvFormatOptions.IGNORE_FIRST_LINE;
import static
org.apache.flink.formats.csv.CsvFormatOptions.IGNORE_PARSE_ERRORS;
import static
org.apache.flink.formats.csv.CsvFormatOptions.IGNORE_TRAILING_UNMAPPABLE;
import static org.apache.flink.formats.csv.CsvFormatOptions.NULL_LITERAL;
@@ -92,7 +94,9 @@ public class CsvFileFormatFactory implements
BulkReaderFormatFactory, BulkWriter
@Override
public Set<ConfigOption<?>> optionalOptions() {
- return CsvCommons.optionalOptions();
+ Set<ConfigOption<?>> options = new
HashSet<>(CsvCommons.optionalOptions());
+ options.add(IGNORE_FIRST_LINE);
+ return options;
}
@Override
@@ -212,6 +216,8 @@ public class CsvFileFormatFactory implements
BulkReaderFormatFactory, BulkWriter
options.getOptional(ALLOW_COMMENTS).ifPresent(csvBuilder::setAllowComments);
+
options.getOptional(IGNORE_FIRST_LINE).ifPresent(csvBuilder::setSkipFirstDataRow);
+
options.getOptional(ARRAY_ELEMENT_DELIMITER)
.ifPresent(csvBuilder::setArrayElementSeparator);
diff --git
a/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFormatOptions.java
b/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFormatOptions.java
index ee624a48e0b..c5f05122ff8 100644
---
a/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFormatOptions.java
+++
b/flink-formats/flink-csv/src/main/java/org/apache/flink/formats/csv/CsvFormatOptions.java
@@ -139,5 +139,14 @@ public class CsvFormatOptions {
+ "(disabled by default). "
+ "Only affects deserialization.");
+ public static final ConfigOption<Boolean> IGNORE_FIRST_LINE =
+ ConfigOptions.key("ignore-first-line")
+ .booleanType()
+ .defaultValue(false)
+ .withDescription(
+ "Optional flag to skip the first CSV record of
each input file "
+ + "(false by default). "
+ + "Only affects deserialization.");
+
private CsvFormatOptions() {}
}
diff --git
a/flink-formats/flink-csv/src/test/java/org/apache/flink/formats/csv/CsvFormatFactoryTest.java
b/flink-formats/flink-csv/src/test/java/org/apache/flink/formats/csv/CsvFormatFactoryTest.java
index fbe1c993c74..ba56f8e9cad 100644
---
a/flink-formats/flink-csv/src/test/java/org/apache/flink/formats/csv/CsvFormatFactoryTest.java
+++
b/flink-formats/flink-csv/src/test/java/org/apache/flink/formats/csv/CsvFormatFactoryTest.java
@@ -23,6 +23,8 @@ import
org.apache.flink.api.common.serialization.SerializationSchema;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.file.src.FileSourceSplit;
import org.apache.flink.connector.file.src.reader.BulkFormat;
+import org.apache.flink.connector.file.src.util.Utils;
+import org.apache.flink.core.fs.Path;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.ValidationException;
import org.apache.flink.table.catalog.Column;
@@ -36,11 +38,17 @@ import org.apache.flink.table.data.RowData;
import org.apache.flink.table.factories.TestDynamicTableFactory;
import
org.apache.flink.table.runtime.connector.source.ScanRuntimeProviderContext;
import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.DataType;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -62,6 +70,15 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
/** Tests for {@link CsvFormatFactory}. */
class CsvFormatFactoryTest {
+ private static final DataType HEADER_ROW_DATA_TYPE =
+ ResolvedSchema.of(
+ Column.physical("id", DataTypes.STRING()),
+ Column.physical("name", DataTypes.STRING()),
+ Column.physical("amount", DataTypes.STRING()))
+ .toPhysicalRowDataType();
+
+ private static final String CSV_WITH_HEADER =
"id,name,amount\n1,Alice,100\n2,Bob,200";
+
@Test
void testSeDeSchema() {
final CsvRowDataDeserializationSchema expectedDeser =
@@ -456,6 +473,43 @@ class CsvFormatFactoryTest {
assertThat(bulkFormat).isNotNull();
}
+ @Test
+ void testIgnoreFirstLineSkipsFirstRecord(@TempDir java.nio.file.Path
tempDir) throws Exception {
+ final File file = tempDir.resolve("with-header.csv").toFile();
+ Files.writeString(file.toPath(), CSV_WITH_HEADER,
StandardCharsets.UTF_8);
+
+ final Configuration formatOptions = new Configuration();
+ formatOptions.set(CsvFormatOptions.IGNORE_FIRST_LINE, true);
+
+ final List<RowData> rows = readBulkFormatRows(formatOptions, file,
HEADER_ROW_DATA_TYPE);
+
+ assertThat(rows).hasSize(2);
+ assertThat(rows.get(0).getString(0).toString()).isEqualTo("1");
+ assertThat(rows.get(0).getString(1).toString()).isEqualTo("Alice");
+ assertThat(rows.get(0).getString(2).toString()).isEqualTo("100");
+ assertThat(rows.get(1).getString(0).toString()).isEqualTo("2");
+ assertThat(rows.get(1).getString(1).toString()).isEqualTo("Bob");
+ assertThat(rows.get(1).getString(2).toString()).isEqualTo("200");
+ }
+
+ @Test
+ void testIgnoreFirstLineDisabledByDefault(@TempDir java.nio.file.Path
tempDir)
+ throws Exception {
+ final File file = tempDir.resolve("with-header.csv").toFile();
+ Files.writeString(file.toPath(), CSV_WITH_HEADER,
StandardCharsets.UTF_8);
+
+ final List<RowData> rows =
+ readBulkFormatRows(new Configuration(), file,
HEADER_ROW_DATA_TYPE);
+
+ assertThat(rows).hasSize(3);
+ assertThat(rows.get(0).getString(0).toString()).isEqualTo("id");
+ assertThat(rows.get(0).getString(1).toString()).isEqualTo("name");
+ assertThat(rows.get(0).getString(2).toString()).isEqualTo("amount");
+ assertThat(rows.get(1).getString(0).toString()).isEqualTo("1");
+ assertThat(rows.get(1).getString(2).toString()).isEqualTo("100");
+ assertThat(rows.get(2).getString(1).toString()).isEqualTo("Bob");
+ }
+
// ------------------------------------------------------------------------
// Utilities
// ------------------------------------------------------------------------
@@ -490,6 +544,33 @@ class CsvFormatFactoryTest {
return options;
}
+ private static List<RowData> readBulkFormatRows(
+ Configuration formatOptions, File file, DataType dataType) throws
IOException {
+ final CsvFileFormatFactory.CsvBulkDecodingFormat bulkDecodingFormat =
+ new CsvFileFormatFactory.CsvBulkDecodingFormat(formatOptions);
+ final BulkFormat<RowData, FileSourceSplit> bulkFormat =
+ bulkDecodingFormat.createRuntimeDecoder(
+ ScanRuntimeProviderContext.INSTANCE,
+ dataType,
+ Projection.all(dataType).toNestedIndexes());
+
+ final FileSourceSplit split =
+ new FileSourceSplit(
+ "split-0",
+ new Path(file.toURI()),
+ 0,
+ file.length(),
+ file.lastModified(),
+ file.length());
+
+ final List<RowData> rows = new ArrayList<>();
+ try (BulkFormat.Reader<RowData> reader =
+ bulkFormat.createReader(new Configuration(), split)) {
+ Utils.forEachRemaining(reader, rows::add);
+ }
+ return rows;
+ }
+
private static DeserializationSchema<RowData> createDeserializationSchema(
Map<String, String> options) {
TestDynamicTableFactory.DynamicTableSourceMock sourceMock =