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

davidzollo 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 befd31c453 [Feature][API] Define Knowledge Sync metadata field 
contract (#10990)
befd31c453 is described below

commit befd31c453e407f5194af76be55d2bef18903a45
Author: yzeng1618 <[email protected]>
AuthorDate: Wed Aug 5 14:34:30 2026 +0800

    [Feature][API] Define Knowledge Sync metadata field contract (#10990)
    
    Co-authored-by: zengyi <[email protected]>
---
 docs/en/transforms/metadata.md                     |  45 ++++++++
 docs/zh/transforms/metadata.md                     |  45 ++++++++
 .../api/table/type/KnowledgeSyncMetadataField.java | 100 ++++++++++++++++++
 .../seatunnel/api/table/type/MetadataUtil.java     |  12 +++
 .../table/type/KnowledgeSyncMetadataFieldTest.java | 117 +++++++++++++++++++++
 .../transform/metadata/MetadataTransform.java      |  81 +++++++-------
 .../transform/metadata/MetadataTransformTest.java  |  86 +++++++++++++++
 7 files changed, 445 insertions(+), 41 deletions(-)

diff --git a/docs/en/transforms/metadata.md b/docs/en/transforms/metadata.md
index 5a311465b0..b0e3fa45f1 100644
--- a/docs/en/transforms/metadata.md
+++ b/docs/en/transforms/metadata.md
@@ -33,12 +33,35 @@ The Metadata transform plugin is used to extract metadata 
information from data
 | Gtid       | string | Global Transaction ID (`server_uuid:transaction_id`). 
`null` when GTID is disabled or for snapshot rows. | MySQL-CDC only |
 | Partition |  string  |  Partition information of the data, multiple 
partition fields separated by commas  | Connectors supporting partitions |
 
+## Knowledge Sync Metadata Fields
+
+Knowledge Sync pipelines can use the following logical metadata keys to carry 
document and chunk identity. These keys become physical fields only after they 
are explicitly projected by the `Metadata` transform.
+
+The `Metadata` transform does not generate Knowledge Sync metadata by itself. 
The upstream source or transform must declare these fields in 
`CatalogTable.metadataSchema` and write the corresponding values into 
`SeaTunnelRow.options`.
+
+| Metadata Key | Canonical Physical Field | Output Type | Description |
+|:---:|:---:|:---:|:---|
+| DocumentId | `document_id` | string | Non-null stable document identity for 
every document lifecycle event. |
+| DocumentHash | `document_hash` | string | Stable document version or content 
hash. |
+| SourceUri | `source_uri` | string | Credential-free stable source URI or 
path. |
+| SourceVersion | `source_version` | string | Source-side version, etag, 
revision, or similar marker. |
+| SourceModifiedAt | `source_modified_at` | long | Source modified time in 
epoch milliseconds. |
+| MimeType | `mime_type` | string | Source MIME type. |
+| Deleted | `deleted` | boolean | Non-null lifecycle marker: `false` for 
normal rows and `true` for document tombstones. |
+| ChunkId | `chunk_id` | string | Stable chunk identity. Required for normal 
chunk rows and nullable for document tombstones. |
+| ChunkHash | `chunk_hash` | string | Stable chunk content hash. Required for 
normal chunk rows and nullable for document tombstones. |
+| ChunkIndex | `chunk_index` | int | Zero-based chunk index. Required for 
normal chunk rows and nullable for document tombstones. |
+
 ### Important Notes
 
 1. **Metadata field names are case-sensitive**: Configuration must strictly 
follow the Key names in the table above (e.g., `Database`, `Table`, `RowKind`, 
etc.)
 2. **Time fields**: `Delay` and `SourceTimestamp` are only available for CDC 
connectors. `EventTime` is also provided by the Kafka source via 
`ConsumerRecord.timestamp` when available.
 3. **Kafka event time**: The Kafka source writes `ConsumerRecord.timestamp` 
(milliseconds) into `EventTime` when it is non-negative, so you can surface it 
with the `Metadata` transform.
 4. **Binlog/GTID fields**: `BinlogFile`, `BinlogPos`, `BinlogRow`, and `Gtid` 
are MySQL-CDC specific. For `startup.mode = initial`, snapshot rows return 
`null` for all four fields.
+5. **Knowledge Sync projection is explicit**: Knowledge Sync metadata fields 
are projected only when they are configured in `metadata_fields`, declared in 
the input table metadata schema, and present in row options. This transform 
reads logical row metadata; it does not read existing physical columns with the 
same names.
+6. **Markdown RAG compatibility**: Existing Markdown RAG output currently 
exposes physical fields such as `source_uri`, `document_id`, `chunk_id`, 
`chunk_index`, and `content_hash`. This transform does not migrate those 
physical fields into logical Knowledge Sync metadata, and this change does not 
rename them.
+7. **Source URI security**: Producers must remove URI user info, access 
tokens, signatures, and other transient authentication material before writing 
`SourceUri` into row options. Non-sensitive query parameters that are part of 
the stable resource identity may be retained.
+8. **Knowledge Sync nullability**: `DocumentId` identifies every document 
lifecycle event. When `Deleted` is declared, producers must write `false` for 
normal rows and `true` for document tombstones rather than `null`. Normal chunk 
rows require `ChunkId`, `ChunkHash`, and `ChunkIndex`; compact document 
tombstones may leave those chunk fields `null`.
 
 ## Options
 
@@ -76,6 +99,28 @@ metadata_fields {
 - The right side is a custom output field name, which cannot duplicate 
existing field names
 - You can select only the metadata fields you need, not all of them must be 
configured
 
+### Knowledge Sync Projection Example
+
+Project Knowledge Sync logical metadata keys into canonical physical columns. 
The upstream producer must already provide the metadata values through row 
options and declare them in the table metadata schema.
+
+```hocon
+transform {
+  Metadata {
+    plugin_input = "knowledge_chunks"
+    plugin_output = "knowledge_chunks_with_meta"
+    metadata_fields = {
+      DocumentId = "document_id"
+      DocumentHash = "document_hash"
+      ChunkId = "chunk_id"
+      ChunkHash = "chunk_hash"
+      ChunkIndex = "chunk_index"
+    }
+  }
+}
+```
+
+After this transform, downstream components can read `document_id`, 
`chunk_id`, and `chunk_hash` as regular physical fields in the input schema.
+
 ## Complete Examples
 
 ### Example 1: MySQL CDC Data Synchronization, Extracting All Metadata
diff --git a/docs/zh/transforms/metadata.md b/docs/zh/transforms/metadata.md
index d51c3264a7..b7a25dcece 100644
--- a/docs/zh/transforms/metadata.md
+++ b/docs/zh/transforms/metadata.md
@@ -33,12 +33,35 @@ Metadata 转换插件用于将数据行中的元数据信息提取为普通字
 | Gtid       | string | 全局事务 ID(格式:`server_uuid:transaction_id`)。GTID 
未启用或快照行时返回 `null`。 | 仅 MySQL-CDC |
 | Partition |  string  |  数据所属的分区信息,多个分区字段使用逗号分隔  | 支持分区的连接器 |
 
+## Knowledge Sync 元数据字段
+
+Knowledge Sync 流程可以使用下面的逻辑元数据 Key 携带文档和 chunk 身份信息。这些 Key 只有通过 `Metadata` 
转换显式投影后,才会成为真实的物理列。
+
+`Metadata` 转换本身不会生成 Knowledge Sync 元数据。上游 Source 或 Transform 必须先在 
`CatalogTable.metadataSchema` 中声明这些元数据字段,并将对应值写入 `SeaTunnelRow.options`。
+
+| 元数据 Key | 标准物理字段名 | 输出类型 | 说明 |
+|:---:|:---:|:---:|:---|
+| DocumentId | `document_id` | string | 每个文档生命周期事件都必须包含的非空稳定文档标识。 |
+| DocumentHash | `document_hash` | string | 稳定的文档版本或内容哈希。 |
+| SourceUri | `source_uri` | string | 不包含凭证的稳定来源 URI 或路径。 |
+| SourceVersion | `source_version` | string | 来源侧版本、etag、revision 等版本标记。 |
+| SourceModifiedAt | `source_modified_at` | long | 来源修改时间,epoch 毫秒。 |
+| MimeType | `mime_type` | string | 来源 MIME 类型。 |
+| Deleted | `deleted` | boolean | 非空生命周期标记:普通行为 `false`,文档 tombstone 为 `true`。 
|
+| ChunkId | `chunk_id` | string | 稳定的 chunk 标识。普通 chunk 行必填,文档 tombstone 可为 
`null`。 |
+| ChunkHash | `chunk_hash` | string | 稳定的 chunk 内容哈希。普通 chunk 行必填,文档 tombstone 
可为 `null`。 |
+| ChunkIndex | `chunk_index` | int | 文档内从 0 开始的 chunk 序号。普通 chunk 行必填,文档 
tombstone 可为 `null`。 |
+
 ### 重要说明
 
 1. **元数据字段区分大小写**:配置时必须严格按照上表中的 Key 名称(如 `Database`、`Table`、`RowKind` 等)。
 2. **时间相关字段**:`Delay` 和 `SourceTimestamp` 仅在 CDC 连接器有效。`EventTime` 也会在 Kafka 
源中使用 `ConsumerRecord.timestamp`(毫秒,非负时)写入。
 3. **Kafka 事件时间**:Kafka 源会在 `ConsumerRecord.timestamp` 非负时写入 `EventTime`,可通过 
Metadata 转换将其暴露为普通字段。
 4. **Binlog/GTID 字段**:`BinlogFile`、`BinlogPos`、`BinlogRow`、`Gtid` 仅适用于 
MySQL-CDC。使用 `startup.mode = initial` 时,快照行的这四个字段均为 `null`。
+5. **Knowledge Sync 投影需要显式配置**:Knowledge Sync 元数据字段只有在 `metadata_fields` 
中配置、输入表 metadata schema 中声明了对应 Key,并且行 options 
中存在对应值时才会被投影。该转换读取的是逻辑行元数据,不会读取同名的已有物理列。
+6. **兼容 Markdown RAG 物理字段**:现有 Markdown RAG 输出当前以物理字段暴露 
`source_uri`、`document_id`、`chunk_id`、`chunk_index` 和 
`content_hash`。该转换不会把这些物理字段迁移为逻辑 Knowledge Sync 元数据,本次变更也不会重命名这些字段。
+7. **来源 URI 安全**:producer 在将 `SourceUri` 写入行 options 前,必须移除 URI 
userinfo、访问令牌、签名以及其他临时鉴权信息。属于稳定资源标识且不敏感的查询参数可以保留。
+8. **Knowledge Sync 可空语义**:`DocumentId` 用于标识每个文档生命周期事件。声明 `Deleted` 后,producer 
必须为普通行写入 `false`,为文档 tombstone 写入 `true`,不能使用 `null`。普通 chunk 行必须包含 
`ChunkId`、`ChunkHash` 和 `ChunkIndex`;紧凑的文档 tombstone 可以将这些 chunk 字段留为 `null`。
 
 ## 配置选项
 
@@ -76,6 +99,28 @@ metadata_fields {
 - 右侧是自定义的输出字段名,不能与原有字段重名
 - 可以只选择需要的元数据字段,不必全部配置
 
+### Knowledge Sync 投影示例
+
+将 Knowledge Sync 逻辑元数据 Key 投影为标准物理列。上游 producer 必须已经通过行 options 提供这些元数据值,并在表 
metadata schema 中声明这些字段。
+
+```hocon
+transform {
+  Metadata {
+    plugin_input = "knowledge_chunks"
+    plugin_output = "knowledge_chunks_with_meta"
+    metadata_fields = {
+      DocumentId = "document_id"
+      DocumentHash = "document_hash"
+      ChunkId = "chunk_id"
+      ChunkHash = "chunk_hash"
+      ChunkIndex = "chunk_index"
+    }
+  }
+}
+```
+
+完成该转换后,下游组件可以把 `document_id`、`chunk_id`、`chunk_hash` 当作输入 schema 中的普通物理字段读取。
+
 ## 完整示例
 
 ### 示例 1:MySQL CDC 数据同步,提取所有元数据
diff --git 
a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/KnowledgeSyncMetadataField.java
 
b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/KnowledgeSyncMetadataField.java
new file mode 100644
index 0000000000..5ba464198b
--- /dev/null
+++ 
b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/KnowledgeSyncMetadataField.java
@@ -0,0 +1,100 @@
+/*
+ * 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.api.table.type;
+
+import org.apache.seatunnel.api.table.catalog.MetadataColumn;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Standard metadata contract for Knowledge Sync document and chunk identity 
fields.
+ *
+ * <p>{@link #DOCUMENT_ID} identifies every document lifecycle event. {@link 
#DELETED} must be
+ * explicitly {@code false} for normal rows or {@code true} for document 
tombstones. Chunk fields
+ * remain nullable because a document tombstone does not represent an 
individual chunk.
+ */
+public enum KnowledgeSyncMetadataField {
+    DOCUMENT_ID("DocumentId", "document_id", BasicType.STRING_TYPE, false),
+    DOCUMENT_HASH("DocumentHash", "document_hash", BasicType.STRING_TYPE, 
true),
+    /**
+     * Credential-free stable source URI or path.
+     *
+     * <p>Producers must remove URI user info, access tokens, signatures, and 
other transient
+     * authentication material before storing this value in row options.
+     */
+    SOURCE_URI("SourceUri", "source_uri", BasicType.STRING_TYPE, true),
+    SOURCE_VERSION("SourceVersion", "source_version", BasicType.STRING_TYPE, 
true),
+    SOURCE_MODIFIED_AT("SourceModifiedAt", "source_modified_at", 
BasicType.LONG_TYPE, true),
+    MIME_TYPE("MimeType", "mime_type", BasicType.STRING_TYPE, true),
+    DELETED("Deleted", "deleted", BasicType.BOOLEAN_TYPE, false),
+    CHUNK_ID("ChunkId", "chunk_id", BasicType.STRING_TYPE, true),
+    CHUNK_HASH("ChunkHash", "chunk_hash", BasicType.STRING_TYPE, true),
+    CHUNK_INDEX("ChunkIndex", "chunk_index", BasicType.INT_TYPE, true);
+
+    private static final Set<String> FIELD_NAMES =
+            Collections.unmodifiableSet(
+                    Stream.of(values())
+                            .map(KnowledgeSyncMetadataField::getName)
+                            .collect(Collectors.toSet()));
+
+    private final String name;
+    private final String physicalName;
+    private final SeaTunnelDataType<?> dataType;
+    private final boolean nullable;
+
+    KnowledgeSyncMetadataField(
+            String name, String physicalName, SeaTunnelDataType<?> dataType, 
boolean nullable) {
+        this.name = name;
+        this.physicalName = physicalName;
+        this.dataType = dataType;
+        this.nullable = nullable;
+    }
+
+    /** Returns the logical metadata key stored in row options and metadata 
schemas. */
+    public String getName() {
+        return name;
+    }
+
+    /** Returns the canonical physical field name used when projecting this 
metadata. */
+    public String getPhysicalName() {
+        return physicalName;
+    }
+
+    /** Returns the field data type defined by the Knowledge Sync contract. */
+    public SeaTunnelDataType<?> getDataType() {
+        return dataType;
+    }
+
+    /** Returns whether the metadata column may contain {@code null}. */
+    public boolean isNullable() {
+        return nullable;
+    }
+
+    /** Creates the metadata column declaration for a Knowledge Sync producer 
schema. */
+    public MetadataColumn toMetadataColumn() {
+        return MetadataColumn.of(name, dataType, (Long) null, nullable, null, 
null);
+    }
+
+    /** Returns whether the name is a logical Knowledge Sync metadata key. */
+    public static boolean isKnowledgeSyncMetadataField(String name) {
+        return FIELD_NAMES.contains(name);
+    }
+}
diff --git 
a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/MetadataUtil.java
 
b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/MetadataUtil.java
index 9e80335a49..9a06e58586 100644
--- 
a/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/MetadataUtil.java
+++ 
b/seatunnel-api/src/main/java/org/apache/seatunnel/api/table/type/MetadataUtil.java
@@ -44,6 +44,9 @@ public class MetadataUtil {
                 .filter(CommonOptions::isSupportMetadataTrans)
                 .map(CommonOptions::getName)
                 .forEach(METADATA_FIELDS::add);
+        Stream.of(KnowledgeSyncMetadataField.values())
+                .map(KnowledgeSyncMetadataField::getName)
+                .forEach(METADATA_FIELDS::add);
     }
 
     public static void setDelay(SeaTunnelRow row, Long delay) {
@@ -116,6 +119,15 @@ public class MetadataUtil {
         return (String[]) row.getOptions().get(PARTITION.getName());
     }
 
+    /**
+     * Returns whether the logical metadata key is supported for explicit 
metadata projection.
+     *
+     * <p>This method does not inspect or classify physical table columns that 
happen to use the
+     * same name.
+     *
+     * @param fieldName logical metadata key
+     * @return whether the key is supported for metadata projection
+     */
     public static boolean isMetadataField(String fieldName) {
         return METADATA_FIELDS.contains(fieldName);
     }
diff --git 
a/seatunnel-api/src/test/java/org/apache/seatunnel/api/table/type/KnowledgeSyncMetadataFieldTest.java
 
b/seatunnel-api/src/test/java/org/apache/seatunnel/api/table/type/KnowledgeSyncMetadataFieldTest.java
new file mode 100644
index 0000000000..dee28bde17
--- /dev/null
+++ 
b/seatunnel-api/src/test/java/org/apache/seatunnel/api/table/type/KnowledgeSyncMetadataFieldTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.api.table.type;
+
+import org.apache.seatunnel.api.table.catalog.MetadataColumn;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class KnowledgeSyncMetadataFieldTest {
+
+    @Test
+    void shouldRegisterKnowledgeSyncMetadataFields() {
+        Assertions.assertTrue(MetadataUtil.isMetadataField("DocumentId"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("DocumentHash"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("SourceUri"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("SourceVersion"));
+        
Assertions.assertTrue(MetadataUtil.isMetadataField("SourceModifiedAt"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("MimeType"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("Deleted"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("ChunkId"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("ChunkHash"));
+        Assertions.assertTrue(MetadataUtil.isMetadataField("ChunkIndex"));
+    }
+
+    @Test
+    void shouldRecognizeKnowledgeSyncMetadataFieldNames() {
+        for (KnowledgeSyncMetadataField field : 
KnowledgeSyncMetadataField.values()) {
+            Assertions.assertTrue(
+                    
KnowledgeSyncMetadataField.isKnowledgeSyncMetadataField(field.getName()));
+        }
+        Assertions.assertFalse(
+                
KnowledgeSyncMetadataField.isKnowledgeSyncMetadataField("UnknownMetadata"));
+        
Assertions.assertFalse(KnowledgeSyncMetadataField.isKnowledgeSyncMetadataField(null));
+    }
+
+    @Test
+    void shouldExposeCanonicalPhysicalNamesAndTypes() {
+        Assertions.assertEquals(
+                "document_id", 
KnowledgeSyncMetadataField.DOCUMENT_ID.getPhysicalName());
+        Assertions.assertEquals(
+                "document_hash", 
KnowledgeSyncMetadataField.DOCUMENT_HASH.getPhysicalName());
+        Assertions.assertEquals(
+                "source_uri", 
KnowledgeSyncMetadataField.SOURCE_URI.getPhysicalName());
+        Assertions.assertEquals(
+                "source_version", 
KnowledgeSyncMetadataField.SOURCE_VERSION.getPhysicalName());
+        Assertions.assertEquals(
+                "source_modified_at",
+                
KnowledgeSyncMetadataField.SOURCE_MODIFIED_AT.getPhysicalName());
+        Assertions.assertEquals(
+                "mime_type", 
KnowledgeSyncMetadataField.MIME_TYPE.getPhysicalName());
+        Assertions.assertEquals("deleted", 
KnowledgeSyncMetadataField.DELETED.getPhysicalName());
+        Assertions.assertEquals("chunk_id", 
KnowledgeSyncMetadataField.CHUNK_ID.getPhysicalName());
+        Assertions.assertEquals(
+                "chunk_hash", 
KnowledgeSyncMetadataField.CHUNK_HASH.getPhysicalName());
+        Assertions.assertEquals(
+                "chunk_index", 
KnowledgeSyncMetadataField.CHUNK_INDEX.getPhysicalName());
+
+        Assertions.assertEquals(
+                BasicType.STRING_TYPE, 
KnowledgeSyncMetadataField.DOCUMENT_ID.getDataType());
+        Assertions.assertEquals(
+                BasicType.STRING_TYPE, 
KnowledgeSyncMetadataField.DOCUMENT_HASH.getDataType());
+        Assertions.assertEquals(
+                BasicType.STRING_TYPE, 
KnowledgeSyncMetadataField.SOURCE_URI.getDataType());
+        Assertions.assertEquals(
+                BasicType.STRING_TYPE, 
KnowledgeSyncMetadataField.SOURCE_VERSION.getDataType());
+        Assertions.assertEquals(
+                BasicType.LONG_TYPE, 
KnowledgeSyncMetadataField.SOURCE_MODIFIED_AT.getDataType());
+        Assertions.assertEquals(
+                BasicType.STRING_TYPE, 
KnowledgeSyncMetadataField.MIME_TYPE.getDataType());
+        Assertions.assertEquals(
+                BasicType.BOOLEAN_TYPE, 
KnowledgeSyncMetadataField.DELETED.getDataType());
+        Assertions.assertEquals(
+                BasicType.STRING_TYPE, 
KnowledgeSyncMetadataField.CHUNK_ID.getDataType());
+        Assertions.assertEquals(
+                BasicType.STRING_TYPE, 
KnowledgeSyncMetadataField.CHUNK_HASH.getDataType());
+        Assertions.assertEquals(
+                BasicType.INT_TYPE, 
KnowledgeSyncMetadataField.CHUNK_INDEX.getDataType());
+    }
+
+    @Test
+    void shouldCreateMetadataColumnsForProducerSchemas() {
+        MetadataColumn column = 
KnowledgeSyncMetadataField.DOCUMENT_ID.toMetadataColumn();
+
+        Assertions.assertEquals("DocumentId", column.getName());
+        Assertions.assertEquals(BasicType.STRING_TYPE, column.getDataType());
+        Assertions.assertFalse(column.isNullable());
+        Assertions.assertFalse(column.isPhysical());
+    }
+
+    @Test
+    void shouldDefineLifecycleNullability() {
+        Assertions.assertFalse(
+                
KnowledgeSyncMetadataField.DOCUMENT_ID.toMetadataColumn().isNullable());
+        
Assertions.assertFalse(KnowledgeSyncMetadataField.DELETED.toMetadataColumn().isNullable());
+        
Assertions.assertTrue(KnowledgeSyncMetadataField.CHUNK_ID.toMetadataColumn().isNullable());
+        Assertions.assertTrue(
+                
KnowledgeSyncMetadataField.CHUNK_HASH.toMetadataColumn().isNullable());
+        Assertions.assertTrue(
+                
KnowledgeSyncMetadataField.CHUNK_INDEX.toMetadataColumn().isNullable());
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransform.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransform.java
index 6db61c900d..bb7b8cb411 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransform.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransform.java
@@ -83,21 +83,7 @@ public class MetadataTransform extends 
MultipleFieldOutputTransform {
         for (Map.Entry<String, String> mapping : 
metadataFieldMapping.entrySet()) {
             String metadataFieldName = mapping.getKey();
             int i = fieldNames.indexOf(metadataFieldName);
-            Object fieldValue;
-            switch (CommonOptions.fromName(metadataFieldName)) {
-                case DATABASE:
-                    fieldValue = MetadataUtil.getDatabase(inputRow);
-                    break;
-                case TABLE:
-                    fieldValue = MetadataUtil.getTable(inputRow);
-                    break;
-                case ROW_KIND:
-                    fieldValue = MetadataUtil.getRowKind(inputRow);
-                    break;
-                default:
-                    fieldValue = inputRow.getOptions().get(metadataFieldName);
-            }
-            value[i] = fieldValue;
+            value[i] = getMetadataFieldValue(metadataFieldName, inputRow);
         }
         return value;
     }
@@ -111,38 +97,51 @@ public class MetadataTransform extends 
MultipleFieldOutputTransform {
             int i = fieldNames.indexOf(metadataFieldName);
             Column column;
 
-            switch (CommonOptions.fromName(metadataFieldName)) {
-                case DATABASE:
-                case TABLE:
-                case ROW_KIND:
-                    column =
-                            PhysicalColumn.of(
-                                    mappingFieldName,
-                                    BasicType.STRING_TYPE,
-                                    (Long) null,
-                                    null,
-                                    true,
-                                    null,
-                                    null);
-                    break;
-                default:
-                    if (metadataSchema.contains(metadataFieldName)) {
-                        column =
-                                ((MetadataColumn)
-                                                metadataSchema
-                                                        
.getColumn(metadataFieldName)
-                                                        
.rename(mappingFieldName))
-                                        .toPhysicalColumn();
-                    } else {
-                        throw 
TransformCommonError.cannotFindMetadataFieldError(
-                                getPluginName(), mappingFieldName);
-                    }
+            if (isComputedCommonMetadataField(metadataFieldName)) {
+                column =
+                        PhysicalColumn.of(
+                                mappingFieldName,
+                                BasicType.STRING_TYPE,
+                                (Long) null,
+                                null,
+                                true,
+                                null,
+                                null);
+            } else if (metadataSchema != null && 
metadataSchema.contains(metadataFieldName)) {
+                column =
+                        ((MetadataColumn)
+                                        metadataSchema
+                                                .getColumn(metadataFieldName)
+                                                .rename(mappingFieldName))
+                                .toPhysicalColumn();
+            } else {
+                throw TransformCommonError.cannotFindMetadataFieldError(
+                        getPluginName(), metadataFieldName);
             }
             columns[i] = column;
         }
         return columns;
     }
 
+    private Object getMetadataFieldValue(String metadataFieldName, 
SeaTunnelRowAccessor inputRow) {
+        if (CommonOptions.DATABASE.getName().equals(metadataFieldName)) {
+            return MetadataUtil.getDatabase(inputRow);
+        }
+        if (CommonOptions.TABLE.getName().equals(metadataFieldName)) {
+            return MetadataUtil.getTable(inputRow);
+        }
+        if (CommonOptions.ROW_KIND.getName().equals(metadataFieldName)) {
+            return MetadataUtil.getRowKind(inputRow);
+        }
+        return inputRow.getOptions().get(metadataFieldName);
+    }
+
+    private boolean isComputedCommonMetadataField(String metadataFieldName) {
+        return CommonOptions.DATABASE.getName().equals(metadataFieldName)
+                || CommonOptions.TABLE.getName().equals(metadataFieldName)
+                || CommonOptions.ROW_KIND.getName().equals(metadataFieldName);
+    }
+
     @VisibleForTesting
     public void initRowContainerGenerator() {
         transformTableSchema();
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/metadata/MetadataTransformTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/metadata/MetadataTransformTest.java
index 63277273f1..9641b14c91 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/metadata/MetadataTransformTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/metadata/MetadataTransformTest.java
@@ -29,9 +29,11 @@ import org.apache.seatunnel.api.table.catalog.TableSchema;
 import org.apache.seatunnel.api.table.type.ArrayType;
 import org.apache.seatunnel.api.table.type.BasicType;
 import org.apache.seatunnel.api.table.type.CommonOptions;
+import org.apache.seatunnel.api.table.type.KnowledgeSyncMetadataField;
 import org.apache.seatunnel.api.table.type.MetadataUtil;
 import org.apache.seatunnel.api.table.type.RowKind;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.transform.exception.TransformException;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -192,4 +194,88 @@ public class MetadataTransformTest {
         Assertions.assertEquals(eventTime, outputRow.getField(9));
         Assertions.assertEquals(150L, outputRow.getField(10));
     }
+
+    @Test
+    void shouldProjectKnowledgeSyncMetadataFromRowOptions() {
+        Map<String, String> metadataMapping = new LinkedHashMap<>();
+        metadataMapping.put(
+                KnowledgeSyncMetadataField.DOCUMENT_ID.getName(),
+                KnowledgeSyncMetadataField.DOCUMENT_ID.getPhysicalName());
+        metadataMapping.put(
+                KnowledgeSyncMetadataField.CHUNK_HASH.getName(),
+                KnowledgeSyncMetadataField.CHUNK_HASH.getPhysicalName());
+        Map<String, Object> config = new HashMap<>();
+        config.put("metadata_fields", metadataMapping);
+        MetadataTransform transform =
+                new MetadataTransform(
+                        ReadonlyConfig.fromMap(config), 
knowledgeSyncCatalogTable(true));
+        transform.initRowContainerGenerator();
+
+        Column[] columns = transform.getOutputColumns();
+        Assertions.assertEquals("document_id", columns[0].getName());
+        Assertions.assertEquals("chunk_hash", columns[1].getName());
+        Assertions.assertEquals(BasicType.STRING_TYPE, 
columns[0].getDataType());
+        Assertions.assertEquals(BasicType.STRING_TYPE, 
columns[1].getDataType());
+        Assertions.assertInstanceOf(PhysicalColumn.class, columns[0]);
+        Assertions.assertInstanceOf(PhysicalColumn.class, columns[1]);
+        Assertions.assertFalse(columns[0].isNullable());
+        Assertions.assertTrue(columns[1].isNullable());
+
+        SeaTunnelRow input = new SeaTunnelRow(new Object[] {"chunk text"});
+        
input.getOptions().put(KnowledgeSyncMetadataField.DOCUMENT_ID.getName(), 
"doc_faq");
+        
input.getOptions().put(KnowledgeSyncMetadataField.CHUNK_HASH.getName(), 
"hash_chunk_0");
+
+        SeaTunnelRow output = transform.map(input);
+        Assertions.assertEquals(3, output.getArity());
+        Assertions.assertEquals("chunk text", output.getField(0));
+        Assertions.assertEquals("doc_faq", output.getField(1));
+        Assertions.assertEquals("hash_chunk_0", output.getField(2));
+        Assertions.assertArrayEquals(
+                new String[] {"text", "document_id", "chunk_hash"},
+                
transform.getProducedCatalogTable().getTableSchema().getFieldNames());
+    }
+
+    @Test
+    void shouldRejectKnowledgeSyncMetadataWhenSchemaDoesNotDeclareIt() {
+        Map<String, String> metadataMapping = new LinkedHashMap<>();
+        metadataMapping.put(
+                KnowledgeSyncMetadataField.DOCUMENT_ID.getName(),
+                KnowledgeSyncMetadataField.DOCUMENT_ID.getPhysicalName());
+        Map<String, Object> config = new HashMap<>();
+        config.put("metadata_fields", metadataMapping);
+        MetadataTransform transform =
+                new MetadataTransform(
+                        ReadonlyConfig.fromMap(config), 
knowledgeSyncCatalogTable(false));
+
+        TransformException exception =
+                Assertions.assertThrows(
+                        TransformException.class, 
transform::initRowContainerGenerator);
+        Assertions.assertTrue(
+                
exception.getMessage().contains(KnowledgeSyncMetadataField.DOCUMENT_ID.getName()));
+    }
+
+    private static CatalogTable knowledgeSyncCatalogTable(boolean 
includeKnowledgeSyncMetadata) {
+        List<Column> metadata = new ArrayList<>();
+        if (includeKnowledgeSyncMetadata) {
+            
metadata.add(KnowledgeSyncMetadataField.DOCUMENT_ID.toMetadataColumn());
+            
metadata.add(KnowledgeSyncMetadataField.CHUNK_HASH.toMetadataColumn());
+        }
+        return CatalogTable.of(
+                TableIdentifier.of("catalog", TablePath.DEFAULT),
+                TableSchema.builder()
+                        .column(
+                                PhysicalColumn.of(
+                                        "text",
+                                        BasicType.STRING_TYPE,
+                                        (Long) null,
+                                        true,
+                                        null,
+                                        null))
+                        .build(),
+                new HashMap<>(),
+                new ArrayList<>(),
+                "comment",
+                "test",
+                MetadataSchema.builder().columns(metadata).build());
+    }
 }

Reply via email to