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-12768-f72a75f1aa120cae0cb7b08d135297950e08132f in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit 14bcba582d256c50609a7e4c3fb30ab078d1567c Author: JeremyXin <[email protected]> AuthorDate: Sun Oct 18 14:02:38 2026 +0000 [Fix][Connector-File] Fix Excel column mapping and read_columns projection (#12768) Co-authored-by: JeremyXin <--golbal> --- docs/en/connectors/source/LocalFile.md | 9 +- docs/zh/connectors/source/LocalFile.md | 9 +- .../file/config/FileBaseSourceOptions.java | 8 ++ .../seatunnel/file/excel/ExcelReaderListener.java | 65 ++++++++- .../file/source/reader/ExcelReadStrategy.java | 70 +++++++-- .../file/reader/ExcelReadStrategyTest.java | 157 +++++++++++++++++++++ .../file/bos/source/BosFileSourceFactory.java | 1 + .../file/cos/source/CosFileSourceFactory.java | 1 + .../file/ftp/source/FtpFileSourceFactory.java | 1 + .../file/gcs/source/GcsFileSourceFactory.java | 1 + .../file/hdfs/source/HdfsFileSourceFactory.java | 1 + .../oss/jindo/source/OssFileSourceFactory.java | 1 + .../file/local/source/LocalFileSourceFactory.java | 1 + .../seatunnel/file/local/LocalFileFactoryTest.java | 4 + .../file/obs/source/ObsFileSourceFactory.java | 1 + .../file/oss/source/OssFileSourceFactory.java | 1 + .../file/s3/source/S3FileSourceFactory.java | 1 + .../file/sftp/source/SftpFileSourceFactory.java | 1 + .../file/smb/source/SmbFileSourceFactory.java | 1 + .../e2e/connector/file/local/LocalFileIT.java | 31 ++++ ...xcel_header_projection_easyexcel_to_assert.conf | 61 ++++++++ ...ocal_excel_header_projection_poi_to_assert.conf | 61 ++++++++ 22 files changed, 468 insertions(+), 19 deletions(-) diff --git a/docs/en/connectors/source/LocalFile.md b/docs/en/connectors/source/LocalFile.md index 413101e451..31885568e9 100644 --- a/docs/en/connectors/source/LocalFile.md +++ b/docs/en/connectors/source/LocalFile.md @@ -67,6 +67,7 @@ If you use SeaTunnel Engine, It automatically integrated the hadoop jar when you | schema | config | no | - | | sheet_name | string | no | - | | excel_engine | string | no | POI | +| excel_use_header_line | boolean | no | false | | poi_excel_max_file_size | long | no | 52428800 | | xml_row_tag | string | no | - | | xml_use_attr_format | boolean | no | - | @@ -314,7 +315,7 @@ default `HH:mm:ss` ### skip_header_row_number [long] -Skip the first few lines, but only for the txt and csv. +Skip the first few lines for text, CSV, and Excel. For example, set like following: @@ -357,6 +358,12 @@ The default Excel reading engine is POI. POI keeps the historical read behavior, You can set `excel_engine = EasyExcel` to use streaming reads for large Excel files. +### excel_use_header_line [boolean] + +Only used when `file_format_type` is `excel`. The default is `false`, so Excel columns are read by position even when `skip_header_row_number` is set. + +Set `excel_use_header_line = true` to use the first row as the header and map schema fields by name. This skips the first row; when both options are set, `excel_use_header_line` takes precedence over `skip_header_row_number`. Fields missing from the header produce null values and a warning. + ### poi_excel_max_file_size [long] Only used when `file_format` is excel and `excel_engine` is POI. diff --git a/docs/zh/connectors/source/LocalFile.md b/docs/zh/connectors/source/LocalFile.md index 396274b888..3519e80980 100644 --- a/docs/zh/connectors/source/LocalFile.md +++ b/docs/zh/connectors/source/LocalFile.md @@ -67,6 +67,7 @@ import ChangeLog from '../changelog/connector-file-local.md'; | schema | config | 否 | - | | sheet_name | string | 否 | - | | excel_engine | string | 否 | POI | +| excel_use_header_line | boolean | 否 | false | | poi_excel_max_file_size | long | 否 | 52428800 | | xml_row_tag | string | 否 | - | | xml_use_attr_format | boolean | 否 | - | @@ -314,7 +315,7 @@ PDF 特有的解析行为如下: ### skip_header_row_number [long] -跳过前几行,但仅适用于 txt 和 csv。 +跳过 text、CSV 或 Excel 文件的前几行。 例如,设置如下: @@ -357,6 +358,12 @@ PDF 特有的解析行为如下: 如果需要读取大 Excel 文件,可以设置 `excel_engine = EasyExcel` 使用流式读取。 +### excel_use_header_line [boolean] + +仅用于 `file_format_type` 为 `excel` 的源。默认值为 `false`,因此即使设置了 `skip_header_row_number`,Excel 仍按列位置读取。 + +设置 `excel_use_header_line = true` 后,第一行作为表头,按列名映射到 schema 字段,并跳过第一行。如果同时设置了 `skip_header_row_number`,则以 `excel_use_header_line` 为准。表头中缺少的字段将产生 null 值并记录警告。 + ### poi_excel_max_file_size [long] 仅在 `file_format` 为 excel 且 `excel_engine` 为 POI 时使用。 diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileBaseSourceOptions.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileBaseSourceOptions.java index edffd618d8..85e134bf9b 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileBaseSourceOptions.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileBaseSourceOptions.java @@ -97,6 +97,14 @@ public class FileBaseSourceOptions extends FileBaseOptions { .defaultValue(0L) .withDescription("The number of rows to skip"); + public static final Option<Boolean> EXCEL_USE_HEADER_LINE = + Options.key("excel_use_header_line") + .booleanType() + .defaultValue(false) + .withDescription( + "Use the first Excel row as column headers for name-based mapping. " + + "Only used when file_format_type is excel."); + public static final Option<List<String>> READ_PARTITIONS = Options.key("read_partitions") .listType() diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/excel/ExcelReaderListener.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/excel/ExcelReaderListener.java index 297e667b65..5cb2ab51b6 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/excel/ExcelReaderListener.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/excel/ExcelReaderListener.java @@ -19,6 +19,7 @@ package org.apache.seatunnel.connectors.seatunnel.file.excel; import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.ObjectMapper; import org.apache.seatunnel.shade.com.typesafe.config.Config; +import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils; import org.apache.seatunnel.api.source.Collector; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; @@ -52,6 +53,10 @@ public class ExcelReaderListener extends AnalysisEventListener<Map<Integer, Obje protected SeaTunnelRowType seaTunnelRowType; private SeaTunnelDataType<?>[] fieldTypes; + private final int[] indexes; + private final boolean useHeaderLine; + // Maps output field indexes to Excel column indexes. + private final Map<Integer, Integer> headerColumns = new HashMap<>(); private ExcelCellUtils excelCellUtils; @@ -62,20 +67,59 @@ public class ExcelReaderListener extends AnalysisEventListener<Map<Integer, Obje Collector<SeaTunnelRow> output, ExcelCellUtils excelCellUtils, SeaTunnelRowType seaTunnelRowType) { + this(tableId, output, excelCellUtils, seaTunnelRowType, null, true); + } + + public ExcelReaderListener( + String tableId, + Collector<SeaTunnelRow> output, + ExcelCellUtils excelCellUtils, + SeaTunnelRowType seaTunnelRowType, + int[] indexes) { + this(tableId, output, excelCellUtils, seaTunnelRowType, indexes, true); + } + + public ExcelReaderListener( + String tableId, + Collector<SeaTunnelRow> output, + ExcelCellUtils excelCellUtils, + SeaTunnelRowType seaTunnelRowType, + int[] indexes, + boolean useHeaderLine) { this.tableId = tableId; this.output = output; this.excelCellUtils = excelCellUtils; this.seaTunnelRowType = seaTunnelRowType; + this.indexes = indexes; + this.useHeaderLine = useHeaderLine; fieldTypes = seaTunnelRowType.getFieldTypes(); } @Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { - for (int i = 0; i < headMap.size(); i++) { - String header = headMap.get(i).getStringValue(); - if (!"null".equals(header)) { - customHeaders.put(i, header); + headerColumns.clear(); + customHeaders.clear(); + for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) { + String header = entry.getValue().getStringValue(); + if (StringUtils.isNotEmpty(header) && !"null".equals(header)) { + customHeaders.put(entry.getKey(), header); + // Schema fields may appear in a different order than the Excel columns. + if (useHeaderLine) { + int fieldIndex = seaTunnelRowType.indexOf(header, false); + if (fieldIndex >= 0) { + headerColumns.put(fieldIndex, entry.getKey()); + } + } + } + } + if (useHeaderLine) { + for (int i = 0; i < fieldTypes.length; i++) { + if (!headerColumns.containsKey(i)) { + log.warn( + "Excel header does not contain schema field '{}'; values will be null", + seaTunnelRowType.getFieldName(i)); + } } } } @@ -87,11 +131,18 @@ public class ExcelReaderListener extends AnalysisEventListener<Map<Integer, Obje Map<Integer, Cell> cellMap = context.readRowHolder().getCellMap(); int i = 0; for (; i < fieldTypes.length; i++) { - if (cellMap.get(i) == null) { + Integer columnIndex; + if (!useHeaderLine) { + columnIndex = indexes == null ? i : indexes[i]; + } else { + columnIndex = headerColumns.get(i); + } + Cell cell = columnIndex == null ? null : cellMap.get(columnIndex); + if (cell == null) { seaTunnelRow.setField(i, null); } else { - Object cell = excelCellUtils.convert(data.get(i), fieldTypes[i], cellMap.get(i)); - seaTunnelRow.setField(i, cell); + Object value = excelCellUtils.convert(data.get(columnIndex), fieldTypes[i], cell); + seaTunnelRow.setField(i, value); } } seaTunnelRow.setTableId(tableId); diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java index d63143f278..697f9be30a 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java @@ -17,6 +17,8 @@ package org.apache.seatunnel.connectors.seatunnel.file.source.reader; +import org.apache.seatunnel.shade.com.typesafe.config.Config; + import org.apache.seatunnel.api.source.Collector; import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; @@ -42,6 +44,7 @@ import org.apache.poi.ss.usermodel.CellValue; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.NumberToTextConverter; @@ -56,6 +59,7 @@ import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.InputStream; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.stream.IntStream; @@ -75,6 +79,8 @@ public class ExcelReadStrategy extends AbstractReadStrategy { private int cellCount; + private boolean useHeaderLine; + @SneakyThrows @Override public void read(String path, String tableId, Collector<SeaTunnelRow> output) { @@ -183,7 +189,13 @@ public class ExcelReadStrategy extends AbstractReadStrategy { ExcelReaderBuilder read = EasyExcel.read( inputStream, - new ExcelReaderListener(tableId, output, excelCellUtils, seaTunnelRowType)); + new ExcelReaderListener( + tableId, + output, + excelCellUtils, + seaTunnelRowType, + indexes, + useHeaderLine)); if (pluginConfig.hasPath(FileBaseSourceOptions.SHEET_NAME.key())) { read.sheet(pluginConfig.getString(FileBaseSourceOptions.SHEET_NAME.key())) .headRowNumber((int) skipHeaderNumber) @@ -261,21 +273,46 @@ public class ExcelReadStrategy extends AbstractReadStrategy { CommonErrorCodeDeprecated.UNSUPPORTED_OPERATION, "Skip the number of rows exceeds the maximum or minimum limit of Sheet"); } + int[] cellIndexes = indexes == null ? IntStream.range(0, cellCount).toArray() : indexes; + // Maps output field indexes to Excel column indexes. + // Header mapping is opt-in; skipping rows alone keeps positional reads. + Map<Integer, Integer> headerColumns = new HashMap<>(); + if (useHeaderLine) { + Row headerRow = sheet.getRow(startRow - 1); + if (headerRow != null) { + for (Cell headerCell : headerRow) { + String header = formatter.formatCellValue(headerCell, formulaEvaluator); + int fieldIndex = seaTunnelRowType.indexOf(header, false); + if (fieldIndex >= 0) { + headerColumns.put(fieldIndex, headerCell.getColumnIndex()); + } + } + } + for (int i = 0; i < fieldTypes.length; i++) { + if (!headerColumns.containsKey(i)) { + log.warn( + "Excel file {} header does not contain schema field '{}'; values will be null", + currentFileName, + seaTunnelRowType.getFieldName(i)); + } + } + } IntStream.range(startRow, lastRowNum + 1) .mapToObj(sheet::getRow) .filter(Objects::nonNull) .forEach( rowData -> { - int[] cellIndexes = - indexes == null - ? IntStream.range(0, cellCount).toArray() - : indexes; - int z = 0; SeaTunnelRow seaTunnelRow = new SeaTunnelRow(cellCount); - for (int j : cellIndexes) { - Cell cell = rowData.getCell(j); + for (int i = 0; i < fieldTypes.length; i++) { + Integer columnIndex; + columnIndex = + useHeaderLine + ? headerColumns.get(i) + : Integer.valueOf(cellIndexes[i]); + Cell cell = + columnIndex == null ? null : rowData.getCell(columnIndex); seaTunnelRow.setField( - z++, + i, cell == null ? null : excelCellUtils.convert( @@ -284,7 +321,7 @@ public class ExcelReadStrategy extends AbstractReadStrategy { cell, formulaEvaluator, formatter), - fieldTypes[z - 1], + fieldTypes[i], null)); } if (isMergePartition) { @@ -383,6 +420,19 @@ public class ExcelReadStrategy extends AbstractReadStrategy { "User must defined schema for json file type"); } + /** + * Loads the Excel header option. When enabled, the first row is used for column-name mapping + * and takes precedence over {@code skip_header_row_number}. + */ + @Override + public void setPluginConfig(Config pluginConfig) { + super.setPluginConfig(pluginConfig); + useHeaderLine = readonlyConfig.get(FileBaseSourceOptions.EXCEL_USE_HEADER_LINE); + if (useHeaderLine) { + skipHeaderNumber = 1L; + } + } + private Object getCellValue( CellType cellType, Cell cell, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/reader/ExcelReadStrategyTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/reader/ExcelReadStrategyTest.java index 3186f3421d..166bda2ab2 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/reader/ExcelReadStrategyTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/reader/ExcelReadStrategyTest.java @@ -243,6 +243,163 @@ public class ExcelReadStrategyTest { testLargeExcelRead("/excel/e2e.xlsx", "/excel/e2exls.conf", 5); } + @Test + public void testEasyExcelReadColumnsUsesSourceColumnIndexes() + throws IOException, URISyntaxException { + URL excelFile = + ExcelReadStrategyTest.class.getResource("/excel/test_read_excel_date_string.xlsx"); + URL conf = + ExcelReadStrategyTest.class.getResource("/excel/test_read_excel_data_string.conf"); + Assertions.assertNotNull(excelFile); + Assertions.assertNotNull(conf); + Config pluginConfig = + ConfigFactory.parseString("read_columns = [\"c_string\", \"c_int\"]") + .withFallback(ConfigFactory.parseFile(new File(conf.toURI()))); + ExcelReadStrategy readStrategy = new ExcelReadStrategy(); + readStrategy.setPluginConfig(pluginConfig); + readStrategy.init(new LocalConf(FS_DEFAULT_NAME_DEFAULT)); + readStrategy.setCatalogTable(CatalogTableUtil.buildWithConfig(pluginConfig)); + + TestCollector collector = new TestCollector(); + readStrategy.read(Paths.get(excelFile.toURI()).toString(), "", collector); + + Assertions.assertEquals(1, collector.getRows().size()); + SeaTunnelRow row = collector.getRows().get(0); + Assertions.assertEquals(2, row.getArity()); + Assertions.assertEquals("Cosmos", row.getField(0)); + Assertions.assertEquals(333, row.getField(1)); + } + + @Test + public void testReadColumnsMatchesExcelHeadersWithProjectedSchema() + throws IOException, URISyntaxException { + URL excelFile = + ExcelReadStrategyTest.class.getResource("/excel/test_read_excel_date_string.xlsx"); + Assertions.assertNotNull(excelFile); + for (String engine : new String[] {"POI", "EasyExcel"}) { + for (String skipRows : new String[] {"", "skip_header_row_number = 2\n"}) { + Config pluginConfig = + ConfigFactory.parseString( + "file_format_type = excel\n" + + "excel_engine = " + + engine + + "\n" + + "excel_use_header_line = true\n" + + skipRows + + "schema { columns = [{name = c_bytes, type = string}, " + + "{name = c_int, type = int}, " + + "{name = c_string, type = string}] }\n" + + "read_columns = [c_string, c_int]"); + ExcelReadStrategy readStrategy = new ExcelReadStrategy(); + readStrategy.setPluginConfig(pluginConfig); + readStrategy.init(new LocalConf(FS_DEFAULT_NAME_DEFAULT)); + readStrategy.setCatalogTable(CatalogTableUtil.buildWithConfig(pluginConfig)); + + TestCollector collector = new TestCollector(); + readStrategy.read(Paths.get(excelFile.toURI()).toString(), "", collector); + + Assertions.assertEquals(1, collector.getRows().size(), engine); + SeaTunnelRow row = collector.getRows().get(0); + Assertions.assertEquals("Cosmos", row.getField(0), engine); + Assertions.assertEquals(333, row.getField(1), engine); + } + } + } + + @Test + public void testExcelHeaderWithoutMatchingSchemaKeepsPositionalReading() + throws IOException, URISyntaxException { + URL excelFile = + ExcelReadStrategyTest.class.getResource("/excel/test_read_excel_date_string.xlsx"); + Assertions.assertNotNull(excelFile); + for (String engine : new String[] {"POI", "EasyExcel"}) { + Config pluginConfig = + ConfigFactory.parseString( + "file_format_type = excel\n" + + "excel_engine = " + + engine + + "\n" + + "skip_header_row_number = 1\n" + + "schema { columns = [{name = first, type = string}, " + + "{name = second, type = string}] }"); + ExcelReadStrategy readStrategy = new ExcelReadStrategy(); + readStrategy.setPluginConfig(pluginConfig); + readStrategy.init(new LocalConf(FS_DEFAULT_NAME_DEFAULT)); + readStrategy.setCatalogTable(CatalogTableUtil.buildWithConfig(pluginConfig)); + + TestCollector collector = new TestCollector(); + readStrategy.read(Paths.get(excelFile.toURI()).toString(), "", collector); + + Assertions.assertEquals(1, collector.getRows().size(), engine); + SeaTunnelRow row = collector.getRows().get(0); + Assertions.assertEquals("1", row.getField(0), engine); + Assertions.assertEquals("22", row.getField(1), engine); + } + } + + @Test + public void testExcelPartialHeaderMatchWithoutOptInKeepsPositionalReading() + throws IOException, URISyntaxException { + URL excelFile = + ExcelReadStrategyTest.class.getResource("/excel/test_read_excel_date_string.xlsx"); + Assertions.assertNotNull(excelFile); + for (String engine : new String[] {"POI", "EasyExcel"}) { + Config pluginConfig = + ConfigFactory.parseString( + "file_format_type = excel\n" + + "excel_engine = " + + engine + + "\n" + + "skip_header_row_number = 1\n" + + "schema { columns = [{name = c_bytes, type = string}, " + + "{name = second, type = string}] }"); + ExcelReadStrategy readStrategy = new ExcelReadStrategy(); + readStrategy.setPluginConfig(pluginConfig); + readStrategy.init(new LocalConf(FS_DEFAULT_NAME_DEFAULT)); + readStrategy.setCatalogTable(CatalogTableUtil.buildWithConfig(pluginConfig)); + + TestCollector collector = new TestCollector(); + readStrategy.read(Paths.get(excelFile.toURI()).toString(), "", collector); + + Assertions.assertEquals(1, collector.getRows().size(), engine); + SeaTunnelRow row = collector.getRows().get(0); + Assertions.assertEquals("1", row.getField(0), engine); + Assertions.assertEquals("22", row.getField(1), engine); + } + } + + @Test + public void testExcelHeaderMissingProjectedFieldReturnsNull() + throws IOException, URISyntaxException { + URL excelFile = + ExcelReadStrategyTest.class.getResource("/excel/test_read_excel_date_string.xlsx"); + Assertions.assertNotNull(excelFile); + for (String engine : new String[] {"POI", "EasyExcel"}) { + Config pluginConfig = + ConfigFactory.parseString( + "file_format_type = excel\n" + + "excel_engine = " + + engine + + "\n" + + "excel_use_header_line = true\n" + + "schema { columns = [{name = c_string, type = string}, " + + "{name = missing_field, type = string}] }\n" + + "read_columns = [c_string, missing_field]"); + ExcelReadStrategy readStrategy = new ExcelReadStrategy(); + readStrategy.setPluginConfig(pluginConfig); + readStrategy.init(new LocalConf(FS_DEFAULT_NAME_DEFAULT)); + readStrategy.setCatalogTable(CatalogTableUtil.buildWithConfig(pluginConfig)); + + TestCollector collector = new TestCollector(); + readStrategy.read(Paths.get(excelFile.toURI()).toString(), "", collector); + + Assertions.assertEquals(1, collector.getRows().size(), engine); + SeaTunnelRow row = collector.getRows().get(0); + Assertions.assertEquals("Cosmos", row.getField(0), engine); + Assertions.assertNull(row.getField(1), engine); + } + } + @Test public void testEasyExcelIgnoresPoiFileSizeLimit() throws IOException, URISyntaxException { URL excelFile = ExcelReadStrategyTest.class.getResource("/excel/e2e.xlsx"); diff --git a/seatunnel-connectors-v2/connector-file/connector-file-bos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/bos/source/BosFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-bos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/bos/source/BosFileSourceFactory.java index d3e90a1719..06c4bd2f62 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-bos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/bos/source/BosFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-bos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/bos/source/BosFileSourceFactory.java @@ -94,6 +94,7 @@ public class BosFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSourceFactory.java index fe6422e75c..e918631e03 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-cos/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/cos/source/CosFileSourceFactory.java @@ -94,6 +94,7 @@ public class CosFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/source/FtpFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/source/FtpFileSourceFactory.java index e965ffca14..2b2c0c26b1 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/source/FtpFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/source/FtpFileSourceFactory.java @@ -104,6 +104,7 @@ public class FtpFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSourceFactory.java index 945cb2c03f..ad9ff53959 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSourceFactory.java @@ -117,6 +117,7 @@ public class GcsFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/HdfsFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/HdfsFileSourceFactory.java index b90a7e8aeb..6ab6ace818 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/HdfsFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-hadoop/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/hdfs/source/HdfsFileSourceFactory.java @@ -113,6 +113,7 @@ public class HdfsFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( HdfsFileSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSourceFactory.java index f94430a48f..a66194550d 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-jindo-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/jindo/source/OssFileSourceFactory.java @@ -94,6 +94,7 @@ public class OssFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-local/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/local/source/LocalFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-local/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/local/source/LocalFileSourceFactory.java index b6ef0900ba..ce2f0f65a8 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-local/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/local/source/LocalFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-local/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/local/source/LocalFileSourceFactory.java @@ -113,6 +113,7 @@ public class LocalFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-local/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/local/LocalFileFactoryTest.java b/seatunnel-connectors-v2/connector-file/connector-file-local/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/local/LocalFileFactoryTest.java index 20f32463c8..8ea1c03b49 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-local/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/local/LocalFileFactoryTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-local/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/local/LocalFileFactoryTest.java @@ -66,6 +66,10 @@ class LocalFileFactoryTest { optionRule.getOptionalOptions().contains(FileBaseSourceOptions.START_MODE)); Assertions.assertTrue( optionRule.getOptionalOptions().contains(FileBaseSourceOptions.POST_SYNC_ACTION)); + Assertions.assertTrue( + optionRule + .getOptionalOptions() + .contains(FileBaseSourceOptions.EXCEL_USE_HEADER_LINE)); Assertions.assertTrue( optionRule.getOptionalOptions().contains(FileBaseSourceOptions.BACKUP_PATH)); Assertions.assertTrue( diff --git a/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSourceFactory.java index 23876525b3..fb19d0a949 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-obs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/obs/source/ObsFileSourceFactory.java @@ -84,6 +84,7 @@ public class ObsFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/source/OssFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/source/OssFileSourceFactory.java index 634e0ce627..871093fed2 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/source/OssFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-oss/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/oss/source/OssFileSourceFactory.java @@ -104,6 +104,7 @@ public class OssFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-s3/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/s3/source/S3FileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-s3/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/s3/source/S3FileSourceFactory.java index 03cb29e75c..6e60f416ea 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-s3/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/s3/source/S3FileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-s3/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/s3/source/S3FileSourceFactory.java @@ -137,6 +137,7 @@ public class S3FileSourceFactory implements TableSourceFactory, SupportSourceDry .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-sftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sftp/source/SftpFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-sftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sftp/source/SftpFileSourceFactory.java index 768c8f75ed..951d273504 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-sftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sftp/source/SftpFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-sftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/sftp/source/SftpFileSourceFactory.java @@ -96,6 +96,7 @@ public class SftpFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-connectors-v2/connector-file/connector-file-smb/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/smb/source/SmbFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-smb/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/smb/source/SmbFileSourceFactory.java index c109039f83..c97d1599f6 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-smb/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/smb/source/SmbFileSourceFactory.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-smb/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/smb/source/SmbFileSourceFactory.java @@ -97,6 +97,7 @@ public class SmbFileSourceFactory implements TableSourceFactory { .optional( FileBaseSourceOptions.SHEET_NAME, FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.EXCEL_USE_HEADER_LINE, FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) .conditional( FileBaseSourceOptions.FILE_FORMAT_TYPE, diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/local/LocalFileIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/local/LocalFileIT.java index 80864b1ada..3eb325e074 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/local/LocalFileIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/local/LocalFileIT.java @@ -38,6 +38,8 @@ import org.apache.seatunnel.e2e.common.util.JobIdGenerator; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.awaitility.Awaitility; import org.junit.jupiter.api.Assertions; @@ -45,6 +47,7 @@ import org.junit.jupiter.api.TestTemplate; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.shaded.com.github.dockerjava.core.command.ExecStartResultCallback; +import org.testcontainers.utility.MountableFile; import com.github.dockerjava.api.command.ExecCreateCmdResponse; import io.airlift.compress.lzo.LzopCodec; @@ -251,6 +254,18 @@ public class LocalFileIT extends TestSuiteBase { "/seatunnel/read/excel/name=tyrantlucifer/hobby=coding/e2e.xls", container); + Path headerProjectionFile = + Files.createTempFile("excel-header-projection-", ".xlsx"); + try { + createExcelHeaderProjectionFile(headerProjectionFile); + // Temporary files can be owner-only; allow the container process to read it. + container.copyFileToContainer( + MountableFile.forHostPath(headerProjectionFile, 0644), + "/seatunnel/read/excel_header_projection/e2e.xlsx"); + } finally { + Files.deleteIfExists(headerProjectionFile); + } + Path xlsxZip = convertToZipFile( Lists.newArrayList( @@ -360,6 +375,8 @@ public class LocalFileIT extends TestSuiteBase { helper.execute("/excel/fake_to_local_excel.conf"); helper.execute("/excel/local_excel_to_assert.conf"); helper.execute("/excel/local_excel_projection_to_assert.conf"); + helper.execute("/excel/local_excel_header_projection_poi_to_assert.conf"); + helper.execute("/excel/local_excel_header_projection_easyexcel_to_assert.conf"); helper.execute("/excel/special_excel_to_assert.conf"); helper.execute("/excel/local_filter_excel_to_assert.conf"); helper.execute("/excel/local_filter_regex_excel_to_assert.conf"); @@ -971,6 +988,20 @@ public class LocalFileIT extends TestSuiteBase { return path; } + private void createExcelHeaderProjectionFile(Path file) throws IOException { + try (XSSFWorkbook workbook = new XSSFWorkbook(); + FileOutputStream output = new FileOutputStream(file.toFile())) { + Sheet sheet = workbook.createSheet(); + sheet.createRow(0).createCell(0).setCellValue("header-b"); + sheet.getRow(0).createCell(1).setCellValue("header-a"); + sheet.getRow(0).createCell(2).setCellValue("header-c"); + sheet.createRow(1).createCell(0).setCellValue("value-b"); + sheet.getRow(1).createCell(1).setCellValue("value-a"); + sheet.getRow(1).createCell(2).setCellValue("value-c"); + workbook.write(output); + } + } + public Path convertToZipFile(List<File> files, String name) throws IOException { if (files == null || files.isEmpty()) { throw new IllegalArgumentException("File list is empty or invalid"); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/excel/local_excel_header_projection_easyexcel_to_assert.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/excel/local_excel_header_projection_easyexcel_to_assert.conf new file mode 100644 index 0000000000..77c57a073e --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/excel/local_excel_header_projection_easyexcel_to_assert.conf @@ -0,0 +1,61 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" +} + +source { + LocalFile { + path = "/seatunnel/read/excel_header_projection/e2e.xlsx" + file_format_type = excel + excel_engine = EasyExcel + excel_use_header_line = true + read_columns = ["header-a", "header-c"] + schema { + fields { + "header-a" = string + "header-b" = string + "header-c" = string + } + } + } +} + +sink { + Assert { + rules { + row_rules = [ + { rule_type = MIN_ROW, rule_value = 1 }, + { rule_type = MAX_ROW, rule_value = 1 } + ] + field_rules = [ + { + field_name = "header-a" + field_type = string + field_value = [{ rule_type = NOT_NULL, equals_to = "value-a" }] + }, + { + field_name = "header-c" + field_type = string + field_value = [{ rule_type = NOT_NULL, equals_to = "value-c" }] + } + ] + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/excel/local_excel_header_projection_poi_to_assert.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/excel/local_excel_header_projection_poi_to_assert.conf new file mode 100644 index 0000000000..d01cabfeea --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/excel/local_excel_header_projection_poi_to_assert.conf @@ -0,0 +1,61 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" +} + +source { + LocalFile { + path = "/seatunnel/read/excel_header_projection/e2e.xlsx" + file_format_type = excel + excel_engine = POI + excel_use_header_line = true + read_columns = ["header-a", "header-c"] + schema { + fields { + "header-a" = string + "header-b" = string + "header-c" = string + } + } + } +} + +sink { + Assert { + rules { + row_rules = [ + { rule_type = MIN_ROW, rule_value = 1 }, + { rule_type = MAX_ROW, rule_value = 1 } + ] + field_rules = [ + { + field_name = "header-a" + field_type = string + field_value = [{ rule_type = NOT_NULL, equals_to = "value-a" }] + }, + { + field_name = "header-c" + field_type = string + field_value = [{ rule_type = NOT_NULL, equals_to = "value-c" }] + } + ] + } + } +}
