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-12731-c70a7a49f24418aac42ffb96ee7a4350d050e4e7 in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit c0e247e51d779a68c15ed46ea7c05d503f575a61 Author: JeremyXin <[email protected]> AuthorDate: Thu Oct 22 14:22:17 2026 +0000 [Fix][Connector-File] Fix child_ids output inconsistent with the expected result (#12861) Co-authored-by: JeremyXin <--golbal> --- docs/en/connectors/source/CosFile.md | 2 +- docs/en/connectors/source/FtpFile.md | 2 +- docs/en/connectors/source/HdfsFile.md | 2 +- docs/en/connectors/source/Hive.md | 2 +- docs/en/connectors/source/LocalFile.md | 2 +- docs/en/connectors/source/ObsFile.md | 2 +- docs/en/connectors/source/OssFile.md | 2 +- docs/en/connectors/source/OssJindoFile.md | 2 +- docs/en/connectors/source/S3File.md | 2 +- docs/en/connectors/source/SftpFile.md | 2 +- .../introduction/concepts/incompatible-changes.md | 4 + docs/zh/connectors/source/CosFile.md | 2 +- docs/zh/connectors/source/FtpFile.md | 2 +- docs/zh/connectors/source/HdfsFile.md | 2 +- docs/zh/connectors/source/Hive.md | 2 +- docs/zh/connectors/source/LocalFile.md | 2 +- docs/zh/connectors/source/ObsFile.md | 2 +- docs/zh/connectors/source/OssFile.md | 2 +- docs/zh/connectors/source/OssJindoFile.md | 2 +- docs/zh/connectors/source/S3File.md | 2 +- docs/zh/connectors/source/SftpFile.md | 2 +- .../introduction/concepts/incompatible-changes.md | 4 + .../file/source/reader/MarkdownReadStrategy.java | 33 +++++++- .../seatunnel/file/source/BaseFileSourceTest.java | 5 ++ .../source/reader/MarkdownReadStrategyTest.java | 97 ++++++++++++++++++++-- .../e2e/connector/file/local/LocalFileIT.java | 2 +- ...local_file_markdown_rag_metadata_to_assert.conf | 81 +++++++++++++++++- 27 files changed, 235 insertions(+), 31 deletions(-) diff --git a/docs/en/connectors/source/CosFile.md b/docs/en/connectors/source/CosFile.md index ae4a92d3c5..2d8671338a 100644 --- a/docs/en/connectors/source/CosFile.md +++ b/docs/en/connectors/source/CosFile.md @@ -199,7 +199,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/FtpFile.md b/docs/en/connectors/source/FtpFile.md index ab53f543de..49bbe38f39 100644 --- a/docs/en/connectors/source/FtpFile.md +++ b/docs/en/connectors/source/FtpFile.md @@ -301,7 +301,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/HdfsFile.md b/docs/en/connectors/source/HdfsFile.md index df272f0aa8..5e77640827 100644 --- a/docs/en/connectors/source/HdfsFile.md +++ b/docs/en/connectors/source/HdfsFile.md @@ -126,7 +126,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/Hive.md b/docs/en/connectors/source/Hive.md index 5f4257589a..3af44f03dd 100644 --- a/docs/en/connectors/source/Hive.md +++ b/docs/en/connectors/source/Hive.md @@ -22,7 +22,7 @@ When using markdown format, SeaTunnel can parse markdown files stored in Hive ta - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element Note: Markdown format only supports reading, not writing. diff --git a/docs/en/connectors/source/LocalFile.md b/docs/en/connectors/source/LocalFile.md index 31885568e9..995a20447d 100644 --- a/docs/en/connectors/source/LocalFile.md +++ b/docs/en/connectors/source/LocalFile.md @@ -213,7 +213,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids` (`ARRAY<STRING>`): Direct child element IDs; an empty array for a leaf element. When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/ObsFile.md b/docs/en/connectors/source/ObsFile.md index a27053cdd6..b0db487279 100644 --- a/docs/en/connectors/source/ObsFile.md +++ b/docs/en/connectors/source/ObsFile.md @@ -247,7 +247,7 @@ schema { > - `page_number`: Page number (default: 1) > - `position_index`: Position index within the document > - `parent_id`: ID of the parent element -> - `child_ids`: Comma-separated list of child element IDs +> - `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element > > When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is > set to `true`, SeaTunnel appends the following RAG metadata fields after > `child_ids` for the corresponding file type: > - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/OssFile.md b/docs/en/connectors/source/OssFile.md index 2e3b8357cc..ad60d4b6dd 100644 --- a/docs/en/connectors/source/OssFile.md +++ b/docs/en/connectors/source/OssFile.md @@ -255,7 +255,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/OssJindoFile.md b/docs/en/connectors/source/OssJindoFile.md index af07373807..d8adb90a5f 100644 --- a/docs/en/connectors/source/OssJindoFile.md +++ b/docs/en/connectors/source/OssJindoFile.md @@ -204,7 +204,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/S3File.md b/docs/en/connectors/source/S3File.md index 9a253ac423..cdff12e9c2 100644 --- a/docs/en/connectors/source/S3File.md +++ b/docs/en/connectors/source/S3File.md @@ -305,7 +305,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/connectors/source/SftpFile.md b/docs/en/connectors/source/SftpFile.md index fc6f426032..eb04505c68 100644 --- a/docs/en/connectors/source/SftpFile.md +++ b/docs/en/connectors/source/SftpFile.md @@ -273,7 +273,7 @@ Each extracted element is converted to a document-element row with the following - `page_number`: Page number (default: 1) - `position_index`: Position index within the document - `parent_id`: ID of the parent element -- `child_ids`: Comma-separated list of child element IDs +- `child_ids`: Direct child element IDs as an `ARRAY<STRING>`; an empty array for a leaf element When either `markdown_rag_metadata_enabled` or `pdf_rag_metadata_enabled` is set to `true`, SeaTunnel appends the following RAG metadata fields after `child_ids` for the corresponding file type: - `source_uri`: Source file path or URI diff --git a/docs/en/introduction/concepts/incompatible-changes.md b/docs/en/introduction/concepts/incompatible-changes.md index 15453b9ae6..856f75963a 100644 --- a/docs/en/introduction/concepts/incompatible-changes.md +++ b/docs/en/introduction/concepts/incompatible-changes.md @@ -19,6 +19,10 @@ You need to check this document before you upgrade to related version. - **Impact**: Such tables are now read completely, but with one snapshot split instead of several. With `exactly_once = true`, a table whose only key is a nullable unique key, or a nullable column configured in `table-names-config.primaryKeys`, now fails at startup with `Exactly once is enabled, but not found non-nullable primary key or unique key for table ...` instead of running and losing rows. This also applies to an existing job whose configured `primaryKeys` column is nullable in [...] - **Migration Guide**: Declare a NOT NULL unique column, or configure `table-names-config.primaryKeys` / `snapshotSplitColumn` with a non-nullable column, to keep parallel snapshot reading. For `exactly_once = true`, add a primary key or a non-nullable unique key to the table, or make the configured `primaryKeys` column NOT NULL in the database. +### Markdown file source `child_ids` array + +The Markdown file source now declares `child_ids` as `ARRAY<STRING>` and emits a `String[]` for every row. Previously, it declared `STRING`, emitted comma-separated IDs for parents, and used `null` for leaves. Update downstream schemas and consumers to read an array; leaves now contain an empty array. The element IDs, types, page numbers, and position indexes retain their existing formats. + ### Runtime Requirements - **Breaking Change: Minimum Java runtime raised from Java 8 to Java 11** diff --git a/docs/zh/connectors/source/CosFile.md b/docs/zh/connectors/source/CosFile.md index fb219452d8..847f7fda81 100644 --- a/docs/zh/connectors/source/CosFile.md +++ b/docs/zh/connectors/source/CosFile.md @@ -197,7 +197,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/FtpFile.md b/docs/zh/connectors/source/FtpFile.md index bd06bf39b3..afa3d0ff89 100644 --- a/docs/zh/connectors/source/FtpFile.md +++ b/docs/zh/connectors/source/FtpFile.md @@ -282,7 +282,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/HdfsFile.md b/docs/zh/connectors/source/HdfsFile.md index 18d0504890..49b5d97493 100644 --- a/docs/zh/connectors/source/HdfsFile.md +++ b/docs/zh/connectors/source/HdfsFile.md @@ -132,7 +132,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/Hive.md b/docs/zh/connectors/source/Hive.md index a5bd5880c6..3fb36054da 100644 --- a/docs/zh/connectors/source/Hive.md +++ b/docs/zh/connectors/source/Hive.md @@ -16,7 +16,7 @@ import ChangeLog from '../changelog/connector-hive.md'; - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 注意:Markdown 格式仅支持读取,不支持写入。 diff --git a/docs/zh/connectors/source/LocalFile.md b/docs/zh/connectors/source/LocalFile.md index 3519e80980..1855e3bf7e 100644 --- a/docs/zh/connectors/source/LocalFile.md +++ b/docs/zh/connectors/source/LocalFile.md @@ -213,7 +213,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`(`ARRAY<STRING>`):直接子元素的 ID;叶子元素为空数组。 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/ObsFile.md b/docs/zh/connectors/source/ObsFile.md index 1c22092ac7..e09a3c9847 100644 --- a/docs/zh/connectors/source/ObsFile.md +++ b/docs/zh/connectors/source/ObsFile.md @@ -120,7 +120,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/OssFile.md b/docs/zh/connectors/source/OssFile.md index a3b5915e25..54ba631ea7 100644 --- a/docs/zh/connectors/source/OssFile.md +++ b/docs/zh/connectors/source/OssFile.md @@ -288,7 +288,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/OssJindoFile.md b/docs/zh/connectors/source/OssJindoFile.md index a872c98592..d6773acbf2 100644 --- a/docs/zh/connectors/source/OssJindoFile.md +++ b/docs/zh/connectors/source/OssJindoFile.md @@ -126,7 +126,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/S3File.md b/docs/zh/connectors/source/S3File.md index 8dc9fd2752..aae121c25e 100644 --- a/docs/zh/connectors/source/S3File.md +++ b/docs/zh/connectors/source/S3File.md @@ -454,7 +454,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/connectors/source/SftpFile.md b/docs/zh/connectors/source/SftpFile.md index 4f9cf54d9f..c94e04060f 100644 --- a/docs/zh/connectors/source/SftpFile.md +++ b/docs/zh/connectors/source/SftpFile.md @@ -274,7 +274,7 @@ markdown 解析器提取各种元素,包括标题、段落、列表、代码 - `page_number`:页码(默认:1) - `position_index`:文档中的位置索引 - `parent_id`:父元素的 ID -- `child_ids`:子元素 ID 的逗号分隔列表 +- `child_ids`:直接子元素的 ID,类型为 `ARRAY<STRING>`;叶子元素为空数组 当 `markdown_rag_metadata_enabled` 或 `pdf_rag_metadata_enabled` 设置为 `true` 时,SeaTunnel 会针对对应文件类型在 `child_ids` 之后追加以下 RAG 元数据字段: - `source_uri`:源文件路径或 URI diff --git a/docs/zh/introduction/concepts/incompatible-changes.md b/docs/zh/introduction/concepts/incompatible-changes.md index ff9e054595..bf78a745e4 100644 --- a/docs/zh/introduction/concepts/incompatible-changes.md +++ b/docs/zh/introduction/concepts/incompatible-changes.md @@ -18,6 +18,10 @@ - **影响**:这类表现在会被完整读取,但快照只有一个分片而不是多个。开启 `exactly_once = true` 时,若表唯一的键是可空唯一键,或 `table-names-config.primaryKeys` 中配置的是可空列,作业会在启动时报错 `Exactly once is enabled, but not found non-nullable primary key or unique key for table ...`,而不是继续运行并丢失数据。对于已有作业,即使配置的 `primaryKeys` 列中实际没有 NULL,只要该列在数据库中可空,也会如此。 - **迁移指南**:如需保持快照并行读取,请声明一个 NOT NULL 的唯一列,或通过 `table-names-config.primaryKeys` / `snapshotSplitColumn` 配置非空列。对于 `exactly_once = true`,请为表添加主键或非空唯一键,或将配置的 `primaryKeys` 列在数据库中改为 NOT NULL。 +### Markdown File Source 的 `child_ids` 数组 + +Markdown File Source 现在将 `child_ids` 声明为 `ARRAY<STRING>`,每行输出 `String[]`。此前该字段声明为 `STRING`,父元素输出逗号分隔的 ID,叶子元素输出 `null`。请将下游 schema 和消费逻辑改为读取数组;叶子元素现在输出空数组。元素 ID、类型、页码和位置索引保持原有格式。 + ### 运行环境要求 - **破坏性变更:最低 Java 运行时从 Java 8 提升到 Java 11** diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategy.java index 5d0aeaf055..aa065b2466 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategy.java @@ -20,6 +20,7 @@ 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.type.ArrayType; import org.apache.seatunnel.api.table.type.BasicType; import org.apache.seatunnel.api.table.type.KnowledgeSyncMetadataField; import org.apache.seatunnel.api.table.type.SeaTunnelDataType; @@ -56,7 +57,9 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.DigestInputStream; import java.security.MessageDigest; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; @@ -85,7 +88,7 @@ public class MarkdownReadStrategy extends AbstractReadStrategy { BasicType.INT_TYPE, BasicType.INT_TYPE, BasicType.STRING_TYPE, - BasicType.STRING_TYPE + ArrayType.STRING_ARRAY_TYPE }; /** Stable metadata fields appended for downstream RAG/document indexing pipelines. */ private static final String[] RAG_METADATA_FIELD_NAMES = { @@ -218,6 +221,7 @@ public class MarkdownReadStrategy extends AbstractReadStrategy { List<SeaTunnelRow> rows = new ArrayList<>(); assignIdsAndCollectTree(document, null, nodeInfoMap, DEFAULT_POSITION, typeCounters); + assignHeadingParents(document, nodeInfoMap); generateRows( document, rows, @@ -261,6 +265,31 @@ public class MarkdownReadStrategy extends AbstractReadStrategy { } } + /** Assigns section parents to top-level blocks after preserving nested AST relationships. */ + private void assignHeadingParents(Node document, Map<Node, NodeInfo> nodeInfoMap) { + Deque<Heading> headings = new ArrayDeque<>(); + for (Node node = document.getFirstChild(); node != null; node = node.getNext()) { + if (!isEligibleForRow(node)) { + continue; + } + if (node instanceof Heading) { + Heading heading = (Heading) node; + while (!headings.isEmpty() && headings.peek().getLevel() >= heading.getLevel()) { + headings.pop(); + } + } + if (!headings.isEmpty()) { + NodeInfo parentInfo = nodeInfoMap.get(headings.peek()); + NodeInfo nodeInfo = nodeInfoMap.get(node); + nodeInfo.parentId = parentInfo.elementId; + parentInfo.childIds.add(nodeInfo.elementId); + } + if (node instanceof Heading) { + headings.push((Heading) node); + } + } + } + private void generateRows( Node node, List<SeaTunnelRow> rows, @@ -288,7 +317,7 @@ public class MarkdownReadStrategy extends AbstractReadStrategy { pageNumber, nodeInfo.positionIndex, nodeInfo.parentId, - nodeInfo.childIds.isEmpty() ? null : String.join(",", nodeInfo.childIds) + nodeInfo.childIds.toArray(new String[0]) }; String contentHash = null; if (markdownRagMetadataEnabled) { diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/BaseFileSourceTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/BaseFileSourceTest.java index 93e6c76dd0..eaa73d1a55 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/BaseFileSourceTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/BaseFileSourceTest.java @@ -19,6 +19,7 @@ package org.apache.seatunnel.connectors.seatunnel.file.source; import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.type.ArrayType; import org.apache.seatunnel.api.table.type.KnowledgeSyncMetadataField; import org.apache.seatunnel.connectors.seatunnel.file.config.FileBaseSourceOptions; import org.apache.seatunnel.connectors.seatunnel.file.config.HadoopConf; @@ -89,6 +90,8 @@ public class BaseFileSourceTest { Assertions.assertArrayEquals( MARKDOWN_FIELD_NAMES, catalogTable.getSeaTunnelRowType().getFieldNames()); + Assertions.assertEquals( + ArrayType.STRING_ARRAY_TYPE, catalogTable.getSeaTunnelRowType().getFieldType(7)); Assertions.assertTrue(catalogTable.getMetadataSchema().getColumns().isEmpty()); } @@ -101,6 +104,8 @@ public class BaseFileSourceTest { Assertions.assertArrayEquals( concat(MARKDOWN_FIELD_NAMES, MARKDOWN_RAG_METADATA_FIELD_NAMES), catalogTable.getSeaTunnelRowType().getFieldNames()); + Assertions.assertEquals( + ArrayType.STRING_ARRAY_TYPE, catalogTable.getSeaTunnelRowType().getFieldType(7)); assertMarkdownKnowledgeSyncMetadata(catalogTable); } diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategyTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategyTest.java index 7b71cade1d..910bc1d5f4 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategyTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/MarkdownReadStrategyTest.java @@ -19,6 +19,7 @@ package org.apache.seatunnel.connectors.seatunnel.file.source.reader; import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory; +import org.apache.seatunnel.api.table.type.ArrayType; import org.apache.seatunnel.api.table.type.KnowledgeSyncMetadataField; import org.apache.seatunnel.api.table.type.SeaTunnelRow; import org.apache.seatunnel.api.table.type.SeaTunnelRowType; @@ -38,6 +39,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.MessageDigest; import java.util.Arrays; +import java.util.List; import java.util.Map; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_DEFAULT_NAME_DEFAULT; @@ -75,6 +77,7 @@ class MarkdownReadStrategyTest { markdownReadStrategy.read(path, "", tempCollector); Assertions.assertArrayEquals(DEFAULT_FIELD_NAMES, rowType.getFieldNames()); + Assertions.assertEquals(ArrayType.STRING_ARRAY_TYPE, rowType.getFieldType(7)); Assertions.assertEquals(75, tempCollector.getRows().size()); Assertions.assertEquals( DEFAULT_FIELD_NAMES.length, tempCollector.getRows().get(0).getArity()); @@ -89,7 +92,13 @@ class MarkdownReadStrategyTest { Assertions.assertEquals(1, tempCollector.getRows().get(0).getField(4)); Assertions.assertEquals(1, tempCollector.getRows().get(0).getField(5)); Assertions.assertNull(tempCollector.getRows().get(0).getField(6)); - Assertions.assertNull(tempCollector.getRows().get(0).getField(7)); + Assertions.assertArrayEquals( + new String[] { + "Paragraph_1", "Heading_2", "Heading_3", "Heading_4", "Heading_5", + "Heading_6", "Heading_7", "Heading_8", "Heading_9", "Heading_10", + "Heading_11" + }, + (String[]) tempCollector.getRows().get(0).getField(7)); Assertions.assertEquals("OrderedList_1", tempCollector.getRows().get(3).getField(0)); Assertions.assertEquals("OrderedList", tempCollector.getRows().get(3).getField(1)); @@ -107,10 +116,14 @@ class MarkdownReadStrategyTest { tempCollector.getRows().get(3).getField(3)); Assertions.assertEquals(1, tempCollector.getRows().get(3).getField(4)); Assertions.assertEquals(5, tempCollector.getRows().get(3).getField(5)); - Assertions.assertNull(tempCollector.getRows().get(3).getField(6)); - Assertions.assertEquals( - "OrderedListItem_1,OrderedListItem_2,OrderedListItem_3,OrderedListItem_4,OrderedListItem_5,OrderedListItem_6,OrderedListItem_7,OrderedListItem_8,OrderedListItem_9", - tempCollector.getRows().get(3).getField(7)); + Assertions.assertEquals("Heading_2", tempCollector.getRows().get(3).getField(6)); + Assertions.assertArrayEquals( + new String[] { + "OrderedListItem_1", "OrderedListItem_2", "OrderedListItem_3", + "OrderedListItem_4", "OrderedListItem_5", "OrderedListItem_6", + "OrderedListItem_7", "OrderedListItem_8", "OrderedListItem_9" + }, + (String[]) tempCollector.getRows().get(3).getField(7)); Assertions.assertEquals("OrderedListItem_1", tempCollector.getRows().get(4).getField(0)); Assertions.assertEquals("OrderedListItem", tempCollector.getRows().get(4).getField(1)); @@ -120,7 +133,77 @@ class MarkdownReadStrategyTest { Assertions.assertEquals(1, tempCollector.getRows().get(4).getField(4)); Assertions.assertEquals(1, tempCollector.getRows().get(4).getField(5)); Assertions.assertEquals("OrderedList_1", tempCollector.getRows().get(4).getField(6)); - Assertions.assertNull(tempCollector.getRows().get(4).getField(7)); + Assertions.assertArrayEquals( + new String[0], (String[]) tempCollector.getRows().get(4).getField(7)); + } + + @Test + public void testHeadingHierarchyKeepsOriginalRowFields() throws Exception { + Path markdownFile = tempDir.resolve("heading-hierarchy.md"); + Files.write( + markdownFile, + ("# Data Source Configuration\n\n" + + "## Kafka Configuration\n" + + "Users can configure Kafka.\n\n" + + "## MySQL Configuration\n" + + "MySQL requires a JDBC URL.\n\n" + + "### Notes\n" + + "Test the connection before deploying.\n") + .getBytes(StandardCharsets.UTF_8)); + + TempCollector collector = new TempCollector(); + createMarkdownReadStrategy().read(markdownFile.toString(), "", collector); + List<SeaTunnelRow> rows = collector.getRows(); + + Assertions.assertEquals(7, rows.size()); + Assertions.assertEquals("Heading_1", rows.get(0).getField(0)); + Assertions.assertEquals("Heading", rows.get(0).getField(1)); + Assertions.assertEquals(1, rows.get(0).getField(4)); + Assertions.assertEquals(1, rows.get(0).getField(5)); + Assertions.assertNull(rows.get(0).getField(6)); + Assertions.assertArrayEquals( + new String[] {"Heading_2", "Heading_3"}, (String[]) rows.get(0).getField(7)); + + Assertions.assertEquals("Heading_1", rows.get(1).getField(6)); + Assertions.assertArrayEquals( + new String[] {"Paragraph_1"}, (String[]) rows.get(1).getField(7)); + Assertions.assertEquals("Heading_2", rows.get(2).getField(6)); + Assertions.assertArrayEquals(new String[0], (String[]) rows.get(2).getField(7)); + Assertions.assertEquals("Heading_1", rows.get(3).getField(6)); + Assertions.assertArrayEquals( + new String[] {"Paragraph_2", "Heading_4"}, (String[]) rows.get(3).getField(7)); + Assertions.assertEquals("Heading_3", rows.get(4).getField(6)); + Assertions.assertEquals("Heading_3", rows.get(5).getField(6)); + Assertions.assertArrayEquals( + new String[] {"Paragraph_3"}, (String[]) rows.get(5).getField(7)); + Assertions.assertEquals("Heading_4", rows.get(6).getField(6)); + } + + @Test + public void testHeadingHierarchyPreservesNestedAstRelations() throws Exception { + Path markdownFile = tempDir.resolve("nested-heading.md"); + Files.write( + markdownFile, + ("# Main\n\n- First\n- Second\n\n> ## Quoted\n\nAfter quote\n") + .getBytes(StandardCharsets.UTF_8)); + + TempCollector collector = new TempCollector(); + createMarkdownReadStrategy().read(markdownFile.toString(), "", collector); + List<SeaTunnelRow> rows = collector.getRows(); + + Assertions.assertArrayEquals( + new String[] {"BulletList_1", "BlockQuote_1", "Paragraph_1"}, + (String[]) rows.get(0).getField(7)); + Assertions.assertEquals("Heading_1", rows.get(1).getField(6)); + Assertions.assertArrayEquals( + new String[] {"BulletListItem_1", "BulletListItem_2"}, + (String[]) rows.get(1).getField(7)); + Assertions.assertEquals("BulletList_1", rows.get(2).getField(6)); + Assertions.assertEquals("Heading_1", rows.get(4).getField(6)); + Assertions.assertArrayEquals( + new String[] {"Heading_2"}, (String[]) rows.get(4).getField(7)); + Assertions.assertEquals("BlockQuote_1", rows.get(5).getField(6)); + Assertions.assertEquals("Heading_1", rows.get(6).getField(6)); } @Test @@ -147,7 +230,9 @@ class MarkdownReadStrategyTest { Assertions.assertArrayEquals( concat(DEFAULT_FIELD_NAMES, RAG_FIELD_NAMES), rowType.getFieldNames()); + Assertions.assertEquals(ArrayType.STRING_ARRAY_TYPE, rowType.getFieldType(7)); Assertions.assertEquals(75, firstCollector.getRows().size()); + Assertions.assertInstanceOf(String[].class, firstCollector.getRows().get(0).getField(7)); Assertions.assertEquals(13, firstCollector.getRows().get(0).getArity()); Assertions.assertEquals(path, firstCollector.getRows().get(0).getField(8)); Assertions.assertTrue( 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 3eb325e074..50d24fe30f 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 @@ -337,7 +337,7 @@ public class LocalFileIT extends TestSuiteBase { container.execInContainer( "sh", "-c", - "mkdir -p /seatunnel/read/markdown && printf '# E2E Markdown RAG\\n' > /seatunnel/read/markdown/e2e.md"); + "mkdir -p /seatunnel/read/markdown && printf '# E2E Markdown RAG\\n## Child\\nBody\\n' > /seatunnel/read/markdown/e2e.md"); ContainerUtil.copyFileIntoContainers( "/text/e2e.txt", "/seatunnel/read/recursive/e2e.txt", container); diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/markdown/local_file_markdown_rag_metadata_to_assert.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/markdown/local_file_markdown_rag_metadata_to_assert.conf index 33f5aedc1b..b23ce9507c 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/markdown/local_file_markdown_rag_metadata_to_assert.conf +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/resources/markdown/local_file_markdown_rag_metadata_to_assert.conf @@ -45,11 +45,41 @@ transform { ChunkHash = "chunk_hash" } } + Sql { + plugin_input = "markdown_rows_with_logical_metadata" + plugin_output = "heading_rows" + query = "select * from markdown_rows_with_logical_metadata where element_id = 'Heading_1'" + } + Sql { + plugin_input = "markdown_rows_with_logical_metadata" + plugin_output = "child_heading_rows" + query = "select * from markdown_rows_with_logical_metadata where element_id = 'Heading_2'" + } + Sql { + plugin_input = "markdown_rows_with_logical_metadata" + plugin_output = "paragraph_rows" + query = "select * from markdown_rows_with_logical_metadata where element_id = 'Paragraph_1'" + } + TableRename { + plugin_input = "heading_rows" + plugin_output = "heading_assert_rows" + suffix = "_heading" + } + TableRename { + plugin_input = "child_heading_rows" + plugin_output = "child_heading_assert_rows" + suffix = "_child_heading" + } + TableRename { + plugin_input = "paragraph_rows" + plugin_output = "paragraph_assert_rows" + suffix = "_paragraph" + } } sink { Assert { - plugin_input = "markdown_rows_with_logical_metadata" + plugin_input = "heading_assert_rows" rules { row_rules = [ { @@ -107,6 +137,11 @@ sink { } ] }, + { + field_name = child_ids + field_type = array<string> + field_value = [{ equals_to = ["Heading_2"] }] + }, { field_name = source_uri field_type = string @@ -175,7 +210,7 @@ sink { field_type = string field_value = [ { - equals_to = "37c48a8c1958a60f1cbd66f00afaef821c1e9dc1e549e8df96a58dc004be3609" + equals_to = "35f4024ea492a3a66048f502cce55f396f8eb1b03bffc22dc9a36e83d29decc9" } ] }, @@ -191,4 +226,46 @@ sink { ] } } + Assert { + plugin_input = "child_heading_assert_rows" + rules { + row_rules = [ + { rule_type = MAX_ROW, rule_value = 1 } + { rule_type = MIN_ROW, rule_value = 1 } + ] + field_rules = [ + { + field_name = parent_id + field_type = string + field_value = [{ equals_to = "Heading_1" }] + } + { + field_name = child_ids + field_type = array<string> + field_value = [{ equals_to = ["Paragraph_1"] }] + } + ] + } + } + Assert { + plugin_input = "paragraph_assert_rows" + rules { + row_rules = [ + { rule_type = MAX_ROW, rule_value = 1 } + { rule_type = MIN_ROW, rule_value = 1 } + ] + field_rules = [ + { + field_name = parent_id + field_type = string + field_value = [{ equals_to = "Heading_2" }] + } + { + field_name = child_ids + field_type = array<string> + field_value = [{ equals_to = [] }] + } + ] + } + } }
