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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 63da558652 [flink] Implement FLIP-314 LineageVertexProvider for 
non-table source & sink APIs (#8001)
63da558652 is described below

commit 63da558652192e06624e1740c5b7f19bcb30bfc4
Author: jsingh-yelp <[email protected]>
AuthorDate: Wed Jul 1 02:09:53 2026 -0400

    [flink] Implement FLIP-314 LineageVertexProvider for non-table source & 
sink APIs (#8001)
---
 .../apache/paimon/flink/lineage/LineageUtils.java  |  74 ++++++-
 .../paimon/flink/lineage/PaimonLineageDataset.java |  51 +++++
 .../flink/sink/FlinkFormatTableDataStreamSink.java |  10 +-
 .../org/apache/paimon/flink/sink/FlinkSink.java    |   3 +-
 .../paimon/flink/sink/PaimonDiscardingSink.java    |  46 ++++
 .../paimon/flink/source/FlinkSourceBuilder.java    |   5 +-
 .../flink/source/PaimonDataStreamSource.java       |  89 ++++++++
 .../flink/source/operator/MonitorSource.java       |  44 +++-
 .../paimon/flink/lineage/LineageUtilsTest.java     | 238 +++++++++++++++------
 .../sink/FlinkFormatTableDataStreamSinkTest.java   |  73 +++++++
 .../flink/sink/FlinkSinkBuilderLineageTest.java    |  84 ++++++++
 .../flink/source/FlinkSourceBuilderTest.java       |  68 ++++++
 12 files changed, 705 insertions(+), 80 deletions(-)

diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/LineageUtils.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/LineageUtils.java
index e9c51d489a..df69031fcf 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/LineageUtils.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/LineageUtils.java
@@ -19,12 +19,19 @@
 package org.apache.paimon.flink.lineage;
 
 import org.apache.paimon.CoreOptions;
+import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.flink.FlinkCatalogFactory;
 import org.apache.paimon.fs.Path;
+import org.apache.paimon.iceberg.IcebergOptions;
+import org.apache.paimon.jdbc.JdbcCatalogFactory;
+import org.apache.paimon.jdbc.JdbcCatalogOptions;
 import org.apache.paimon.options.CatalogOptions;
+import org.apache.paimon.options.Options;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.FormatTable;
 import org.apache.paimon.table.Table;
+import org.apache.paimon.utils.StringUtils;
 
 import org.apache.flink.api.connector.source.Boundedness;
 import org.apache.flink.streaming.api.lineage.LineageDataset;
@@ -37,9 +44,11 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Lineage utilities for building {@link SourceLineageVertex} and {@link 
LineageVertex} from a
@@ -57,8 +66,13 @@ public class LineageUtils {
             new HashSet<>(
                     Arrays.asList(CatalogOptions.WAREHOUSE.key(), 
CatalogOptions.METASTORE.key()));
 
+    private static final String DEFAULT_CATALOG_IDENTIFIER = 
FlinkCatalogFactory.IDENTIFIER;
+
     private static final Set<String> PAIMON_OPTION_KEYS =
-            CoreOptions.getOptions().stream().map(opt -> 
opt.key()).collect(Collectors.toSet());
+            Stream.of(CoreOptions.getOptions(), IcebergOptions.getOptions())
+                    .flatMap(List::stream)
+                    .map(opt -> opt.key())
+                    .collect(Collectors.toSet());
 
     /** Extracts the {@link CatalogContext} from a table, or null if not 
available. */
     @Nullable
@@ -73,9 +87,9 @@ public class LineageUtils {
     }
 
     /**
-     * Builds the config map for a dataset facet. Includes filtered Paimon 
{@link CoreOptions},
-     * partition keys, primary keys, and a safe subset of catalog-level 
options (warehouse,
-     * metastore) prefixed with {@code "catalog."}.
+     * Builds the config map for a dataset facet from a {@link Table}. 
Includes {@link CoreOptions}
+     * and {@link IcebergOptions}, partition keys, primary keys, and a safe 
subset of catalog-level
+     * options prefixed with {@code "catalog."}.
      */
     private static Map<String, String> buildConfigMap(
             Table table, @Nullable CatalogContext catalogContext) {
@@ -122,6 +136,27 @@ public class LineageUtils {
         return DEFAULT_NAMESPACE;
     }
 
+    @VisibleForTesting
+    static String resolveNameByMetastore(Table table, @Nullable String 
defaultName) {
+        if (defaultName != null) {
+            return defaultName;
+        }
+
+        CatalogContext ctx = catalogContext(table);
+        if (ctx != null) {
+            Options catalogOptions = ctx.options();
+            // If jdbc metastore is used, use catalog-key as the catalog 
identifier.
+            if (JdbcCatalogFactory.IDENTIFIER.equals(
+                    catalogOptions.get(CatalogOptions.METASTORE))) {
+                String catalogKeyValue = 
catalogOptions.get(JdbcCatalogOptions.CATALOG_KEY);
+                if (!StringUtils.isNullOrWhitespaceOnly(catalogKeyValue)) {
+                    return catalogKeyValue + "." + table.fullName();
+                }
+            }
+        }
+        return DEFAULT_CATALOG_IDENTIFIER + "." + table.fullName();
+    }
+
     /**
      * Creates a {@link SourceLineageVertex} for a Paimon source table.
      *
@@ -134,12 +169,26 @@ public class LineageUtils {
         CatalogContext ctx = catalogContext(table);
         LineageDataset dataset =
                 new PaimonLineageDataset(
-                        name, getNamespace(table, ctx), buildConfigMap(table, 
ctx));
+                        resolveNameByMetastore(table, name),
+                        getNamespace(table, ctx),
+                        buildConfigMap(table, ctx),
+                        table.rowType());
         Boundedness boundedness =
                 isBounded ? Boundedness.BOUNDED : 
Boundedness.CONTINUOUS_UNBOUNDED;
         return new PaimonSourceLineageVertex(boundedness, 
Collections.singletonList(dataset));
     }
 
+    /**
+     * Creates a {@link SourceLineageVertex} for a Paimon DataStream source 
table. The table name is
+     * derived from the table's full name, prefixed with the {@code 
catalog-key} if available.
+     *
+     * @param isBounded whether the source is bounded (batch) or unbounded 
(streaming)
+     * @param table the Paimon table
+     */
+    public static SourceLineageVertex sourceLineageVertex(boolean isBounded, 
Table table) {
+        return sourceLineageVertex(null, isBounded, table);
+    }
+
     /**
      * Creates a {@link LineageVertex} for a Paimon sink table.
      *
@@ -150,7 +199,20 @@ public class LineageUtils {
         CatalogContext ctx = catalogContext(table);
         LineageDataset dataset =
                 new PaimonLineageDataset(
-                        name, getNamespace(table, ctx), buildConfigMap(table, 
ctx));
+                        resolveNameByMetastore(table, name),
+                        getNamespace(table, ctx),
+                        buildConfigMap(table, ctx),
+                        table.rowType());
         return new PaimonSinkLineageVertex(Collections.singletonList(dataset));
     }
+
+    /**
+     * Creates a {@link LineageVertex} for a Paimon DataStream sink table. The 
table name is derived
+     * from the table's full name, prefixed with the {@code catalog-key} if 
available.
+     *
+     * @param table the Paimon table
+     */
+    public static LineageVertex sinkLineageVertex(Table table) {
+        return sinkLineageVertex(null, table);
+    }
 }
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/PaimonLineageDataset.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/PaimonLineageDataset.java
index cdc072623b..8f215f3902 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/PaimonLineageDataset.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lineage/PaimonLineageDataset.java
@@ -18,11 +18,19 @@
 
 package org.apache.paimon.flink.lineage;
 
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.RowType;
+
 import org.apache.flink.streaming.api.lineage.DatasetConfigFacet;
+import org.apache.flink.streaming.api.lineage.DatasetSchemaFacet;
+import org.apache.flink.streaming.api.lineage.DatasetSchemaField;
 import org.apache.flink.streaming.api.lineage.LineageDataset;
 import org.apache.flink.streaming.api.lineage.LineageDatasetFacet;
 
+import javax.annotation.Nullable;
+
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
@@ -34,11 +42,21 @@ public class PaimonLineageDataset implements LineageDataset 
{
     private final String name;
     private final String namespace;
     private final Map<String, String> tableOptions;
+    @Nullable private final RowType rowType;
 
     public PaimonLineageDataset(String name, String namespace, Map<String, 
String> tableOptions) {
+        this(name, namespace, tableOptions, null);
+    }
+
+    public PaimonLineageDataset(
+            String name,
+            String namespace,
+            Map<String, String> tableOptions,
+            @Nullable RowType rowType) {
         this.name = name;
         this.namespace = namespace;
         this.tableOptions = tableOptions;
+        this.rowType = rowType;
     }
 
     @Override
@@ -67,6 +85,39 @@ public class PaimonLineageDataset implements LineageDataset {
                         return tableOptions;
                     }
                 });
+        if (rowType != null) {
+            facets.put(
+                    "schema",
+                    new DatasetSchemaFacet() {
+                        @Override
+                        public String name() {
+                            return "schema";
+                        }
+
+                        @Override
+                        public Map<String, DatasetSchemaField<String>> 
fields() {
+                            Map<String, DatasetSchemaField<String>> result = 
new LinkedHashMap<>();
+                            for (DataField field : rowType.getFields()) {
+                                String fieldName = field.name();
+                                String fieldType = field.type().asSQLString();
+                                result.put(
+                                        fieldName,
+                                        new DatasetSchemaField<String>() {
+                                            @Override
+                                            public String name() {
+                                                return fieldName;
+                                            }
+
+                                            @Override
+                                            public String type() {
+                                                return fieldType;
+                                            }
+                                        });
+                            }
+                            return result;
+                        }
+                    });
+        }
         return facets;
     }
 }
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
index 3d133a9cea..a223ad5129 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSink.java
@@ -20,6 +20,7 @@ package org.apache.paimon.flink.sink;
 
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.flink.FlinkRowWrapper;
+import org.apache.paimon.flink.lineage.LineageUtils;
 import org.apache.paimon.table.FormatTable;
 import org.apache.paimon.table.format.FormatTableWrite;
 import org.apache.paimon.table.sink.BatchTableCommit;
@@ -32,6 +33,8 @@ import org.apache.flink.api.connector.sink2.SinkWriter;
 import org.apache.flink.api.connector.sink2.WriterInitContext;
 import org.apache.flink.streaming.api.datastream.DataStream;
 import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.lineage.LineageVertex;
+import org.apache.flink.streaming.api.lineage.LineageVertexProvider;
 import org.apache.flink.table.data.RowData;
 
 import java.util.List;
@@ -55,7 +58,7 @@ public class FlinkFormatTableDataStreamSink {
         return dataStream.sinkTo(new FormatTableSink(table, overwrite, 
staticPartitions));
     }
 
-    private static class FormatTableSink implements Sink<RowData> {
+    private static class FormatTableSink implements Sink<RowData>, 
LineageVertexProvider {
 
         private final FormatTable table;
         private final boolean overwrite;
@@ -68,6 +71,11 @@ public class FlinkFormatTableDataStreamSink {
             this.staticPartitions = staticPartitions;
         }
 
+        @Override
+        public LineageVertex getLineageVertex() {
+            return LineageUtils.sinkLineageVertex(table);
+        }
+
         /**
          * Do not annotate with <code>@override</code> here to maintain 
compatibility with Flink
          * 2.0+.
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
index 959132ad58..9948863c54 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
@@ -40,7 +40,6 @@ import 
org.apache.flink.streaming.api.datastream.DataStreamSink;
 import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
 import org.apache.flink.streaming.api.environment.CheckpointConfig;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
-import org.apache.flink.streaming.api.functions.sink.v2.DiscardingSink;
 import org.apache.flink.streaming.api.operators.OneInputStreamOperatorFactory;
 
 import javax.annotation.Nullable;
@@ -240,7 +239,7 @@ public abstract class FlinkSink<T> implements Serializable {
         }
         configureSlotSharingGroup(
                 committed, options.get(SINK_COMMITTER_CPU), 
options.get(SINK_COMMITTER_MEMORY));
-        return committed.sinkTo(new 
DiscardingSink<>()).name("end").setParallelism(1);
+        return committed.sinkTo(new 
PaimonDiscardingSink<>(table)).name("end").setParallelism(1);
     }
 
     public static void configureSlotSharingGroup(
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/PaimonDiscardingSink.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/PaimonDiscardingSink.java
new file mode 100644
index 0000000000..84c5041669
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/PaimonDiscardingSink.java
@@ -0,0 +1,46 @@
+/*
+ * 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.paimon.flink.sink;
+
+import org.apache.paimon.flink.lineage.LineageUtils;
+import org.apache.paimon.table.FileStoreTable;
+
+import org.apache.flink.streaming.api.functions.sink.v2.DiscardingSink;
+import org.apache.flink.streaming.api.lineage.LineageVertex;
+import org.apache.flink.streaming.api.lineage.LineageVertexProvider;
+
+/**
+ * A {@link DiscardingSink} that implements {@link LineageVertexProvider} so 
Flink's lineage graph
+ * discovers the Paimon sink table when using the DataStream API.
+ */
+public class PaimonDiscardingSink<T> extends DiscardingSink<T> implements 
LineageVertexProvider {
+
+    private static final long serialVersionUID = 1L;
+
+    private final FileStoreTable table;
+
+    public PaimonDiscardingSink(FileStoreTable table) {
+        this.table = table;
+    }
+
+    @Override
+    public LineageVertex getLineageVertex() {
+        return LineageUtils.sinkLineageVertex(table);
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
index 2e9c8ae494..d00d2843ff 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java
@@ -240,7 +240,7 @@ public class FlinkSourceBuilder {
     private DataStream<RowData> toDataStream(Source<RowData, ?, ?> source) {
         DataStreamSource<RowData> dataStream =
                 env.fromSource(
-                        source,
+                        new PaimonDataStreamSource<>(source, table),
                         watermarkStrategy == null
                                 ? WatermarkStrategy.noWatermarks()
                                 : watermarkStrategy,
@@ -360,7 +360,8 @@ public class FlinkSourceBuilder {
                         unordered,
                         outerProject(),
                         isBounded,
-                        limit);
+                        limit,
+                        table);
         if (parallelism != null) {
             dataStream.getTransformation().setParallelism(parallelism);
         }
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/PaimonDataStreamSource.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/PaimonDataStreamSource.java
new file mode 100644
index 0000000000..bfef8bc630
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/PaimonDataStreamSource.java
@@ -0,0 +1,89 @@
+/*
+ * 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.paimon.flink.source;
+
+import org.apache.paimon.flink.lineage.LineageUtils;
+import org.apache.paimon.table.Table;
+
+import org.apache.flink.api.connector.source.Boundedness;
+import org.apache.flink.api.connector.source.Source;
+import org.apache.flink.api.connector.source.SourceReader;
+import org.apache.flink.api.connector.source.SourceReaderContext;
+import org.apache.flink.api.connector.source.SourceSplit;
+import org.apache.flink.api.connector.source.SplitEnumerator;
+import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.lineage.LineageVertex;
+import org.apache.flink.streaming.api.lineage.LineageVertexProvider;
+
+/**
+ * A {@link Source} wrapper that preserves the wrapped source behavior and 
exposes Paimon lineage
+ * for sources built through {@link FlinkSourceBuilder}.
+ */
+public class PaimonDataStreamSource<T, SplitT extends SourceSplit, CheckpointT>
+        implements Source<T, SplitT, CheckpointT>, LineageVertexProvider {
+
+    private static final long serialVersionUID = 1L;
+
+    private final Source<T, SplitT, CheckpointT> source;
+    private final Table table;
+
+    public PaimonDataStreamSource(Source<T, SplitT, CheckpointT> source, Table 
table) {
+        this.source = source;
+        this.table = table;
+    }
+
+    @Override
+    public Boundedness getBoundedness() {
+        return source.getBoundedness();
+    }
+
+    @Override
+    public SourceReader<T, SplitT> createReader(SourceReaderContext 
readerContext)
+            throws Exception {
+        return source.createReader(readerContext);
+    }
+
+    @Override
+    public SplitEnumerator<SplitT, CheckpointT> createEnumerator(
+            SplitEnumeratorContext<SplitT> enumContext) throws Exception {
+        return source.createEnumerator(enumContext);
+    }
+
+    @Override
+    public SplitEnumerator<SplitT, CheckpointT> restoreEnumerator(
+            SplitEnumeratorContext<SplitT> enumContext, CheckpointT 
checkpoint) throws Exception {
+        return source.restoreEnumerator(enumContext, checkpoint);
+    }
+
+    @Override
+    public SimpleVersionedSerializer<SplitT> getSplitSerializer() {
+        return source.getSplitSerializer();
+    }
+
+    @Override
+    public SimpleVersionedSerializer<CheckpointT> 
getEnumeratorCheckpointSerializer() {
+        return source.getEnumeratorCheckpointSerializer();
+    }
+
+    @Override
+    public LineageVertex getLineageVertex() {
+        return LineageUtils.sourceLineageVertex(getBoundedness() == 
Boundedness.BOUNDED, table);
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
index d9b7d054cf..b6de64472b 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
@@ -21,9 +21,12 @@ package org.apache.paimon.flink.source.operator;
 import org.apache.paimon.flink.NestedProjectedRowData;
 import org.apache.paimon.flink.source.AbstractNonCoordinatedSource;
 import org.apache.paimon.flink.source.AbstractNonCoordinatedSourceReader;
+import org.apache.paimon.flink.source.NoOpEnumState;
+import org.apache.paimon.flink.source.PaimonDataStreamSource;
 import org.apache.paimon.flink.source.SimpleSourceSplit;
 import org.apache.paimon.flink.source.SplitListState;
 import org.apache.paimon.flink.utils.JavaTypeInfo;
+import org.apache.paimon.table.Table;
 import org.apache.paimon.table.sink.ChannelComputer;
 import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.table.source.EndOfScanException;
@@ -37,6 +40,7 @@ import 
org.apache.flink.api.common.eventtime.WatermarkStrategy;
 import org.apache.flink.api.common.typeinfo.TypeInformation;
 import org.apache.flink.api.connector.source.Boundedness;
 import org.apache.flink.api.connector.source.ReaderOutput;
+import org.apache.flink.api.connector.source.Source;
 import org.apache.flink.api.connector.source.SourceReader;
 import org.apache.flink.api.connector.source.SourceReaderContext;
 import org.apache.flink.api.java.tuple.Tuple2;
@@ -242,13 +246,43 @@ public class MonitorSource extends 
AbstractNonCoordinatedSource<Split> {
             NestedProjectedRowData nestedProjectedRowData,
             boolean isBounded,
             @Nullable Long limit) {
+        return buildSource(
+                env,
+                name,
+                typeInfo,
+                readBuilder,
+                monitorInterval,
+                emitSnapshotWatermark,
+                shuffleBucketWithPartition,
+                unordered,
+                nestedProjectedRowData,
+                isBounded,
+                limit,
+                null);
+    }
+
+    public static DataStream<RowData> buildSource(
+            StreamExecutionEnvironment env,
+            String name,
+            TypeInformation<RowData> typeInfo,
+            ReadBuilder readBuilder,
+            long monitorInterval,
+            boolean emitSnapshotWatermark,
+            boolean shuffleBucketWithPartition,
+            boolean unordered,
+            NestedProjectedRowData nestedProjectedRowData,
+            boolean isBounded,
+            @Nullable Long limit,
+            @Nullable Table table) {
+        MonitorSource monitorSource =
+                new MonitorSource(readBuilder, monitorInterval, 
emitSnapshotWatermark, isBounded);
+        Source<Split, SimpleSourceSplit, NoOpEnumState> source = monitorSource;
+        if (table != null) {
+            source = new PaimonDataStreamSource<>(monitorSource, table);
+        }
         SingleOutputStreamOperator<Split> operator =
                 env.fromSource(
-                                new MonitorSource(
-                                        readBuilder,
-                                        monitorInterval,
-                                        emitSnapshotWatermark,
-                                        isBounded),
+                                source,
                                 WatermarkStrategy.noWatermarks(),
                                 name + "-Monitor",
                                 new JavaTypeInfo<>(Split.class))
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/lineage/LineageUtilsTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/lineage/LineageUtilsTest.java
index ed06e45430..2686a9e404 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/lineage/LineageUtilsTest.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/lineage/LineageUtilsTest.java
@@ -19,17 +19,18 @@
 package org.apache.paimon.flink.lineage;
 
 import org.apache.paimon.CoreOptions;
-import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.CatalogContext;
-import org.apache.paimon.catalog.CatalogFactory;
 import org.apache.paimon.flink.PaimonDataStreamScanProvider;
-import org.apache.paimon.flink.PaimonDataStreamSinkProvider;
+import org.apache.paimon.flink.sink.PaimonDiscardingSink;
+import org.apache.paimon.flink.source.ContinuousFileStoreSource;
+import org.apache.paimon.flink.source.PaimonDataStreamSource;
+import org.apache.paimon.flink.source.operator.MonitorSource;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
-import org.apache.paimon.options.CatalogOptions;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.table.CatalogEnvironment;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.FileStoreTableFactory;
 import org.apache.paimon.types.IntType;
@@ -38,6 +39,7 @@ import org.apache.paimon.types.VarCharType;
 
 import org.apache.flink.api.connector.source.Boundedness;
 import org.apache.flink.streaming.api.lineage.DatasetConfigFacet;
+import org.apache.flink.streaming.api.lineage.DatasetSchemaFacet;
 import org.apache.flink.streaming.api.lineage.LineageDataset;
 import org.apache.flink.streaming.api.lineage.LineageDatasetFacet;
 import org.apache.flink.streaming.api.lineage.LineageVertex;
@@ -59,14 +61,11 @@ class LineageUtilsTest {
 
     @TempDir java.nio.file.Path temp;
 
-    private Path warehouse;
     private Path tablePath;
 
     @BeforeEach
     void setUp() {
-        // mirror real Paimon layout: <warehouse>/<database>.db/<table>
-        warehouse = new Path(temp.toUri().toString());
-        tablePath = new Path(warehouse, "test_db.db/test_table");
+        tablePath = new Path(temp.toUri().toString());
     }
 
     private FileStoreTable createTable(
@@ -86,51 +85,50 @@ class LineageUtilsTest {
         return FileStoreTableFactory.create(LocalFileIO.create(), tablePath);
     }
 
-    @Test
-    void testGetNamespaceWithNullCatalogContext() throws Exception {
+    private FileStoreTable createTableWithCatalogOptions(Map<String, String> 
catalogOptions)
+            throws Exception {
         FileStoreTable table =
                 createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
-        assertThat(LineageUtils.getNamespace(table, 
null)).isEqualTo(table.options().get("path"));
+        CatalogEnvironment catalogEnvironment =
+                new CatalogEnvironment(
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        CatalogContext.create(Options.fromMap(catalogOptions)),
+                        false,
+                        false);
+        return FileStoreTableFactory.create(
+                LocalFileIO.create(), tablePath, table.schema(), 
catalogEnvironment);
     }
 
     @Test
-    void testGetNamespaceWithCatalogContext() throws Exception {
-        FileStoreTable table =
-                createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
-        Options options = new Options();
-        options.set(CatalogOptions.WAREHOUSE, warehouse.toString());
-        CatalogContext ctx = CatalogContext.create(options);
+    void testGetNamespaceWithWarehouse() throws Exception {
+        Map<String, String> catalogOptions = new HashMap<>();
+        catalogOptions.put("warehouse", "s3://my-bucket/warehouse");
+        FileStoreTable table = createTableWithCatalogOptions(catalogOptions);
+
+        String namespace =
+                LineageUtils.getNamespace(table, 
table.catalogEnvironment().catalogContext());
 
-        assertThat(LineageUtils.getNamespace(table, 
ctx)).isEqualTo(warehouse.toString());
+        assertThat(namespace).isEqualTo("s3://my-bucket/warehouse");
     }
 
     @Test
-    void testGetNamespaceWithCatalogContextNoWarehouse() throws Exception {
+    void testGetNamespaceFallsBackToPath() throws Exception {
         FileStoreTable table =
                 createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
-        CatalogContext ctx = CatalogContext.create(new Options());
-        assertThat(LineageUtils.getNamespace(table, 
ctx)).isEqualTo(table.options().get("path"));
+
+        String namespace = LineageUtils.getNamespace(table, null);
+
+        assertThat(namespace).contains(tablePath.toString());
     }
 
     @Test
     void testSourceLineageVertexBounded() throws Exception {
-        Options catalogOptions = new Options();
-        catalogOptions.set(CatalogOptions.WAREHOUSE, warehouse.toString());
-        Catalog catalog = 
CatalogFactory.createCatalog(CatalogContext.create(catalogOptions));
-        catalog.createDatabase("test_db", true);
-        catalog.createTable(
-                org.apache.paimon.catalog.Identifier.create("test_db", "src"),
-                new Schema(
-                        RowType.of(new IntType(), new VarCharType(100), new 
IntType()).getFields(),
-                        Collections.emptyList(),
-                        Arrays.asList("f0"),
-                        new HashMap<>(),
-                        ""),
-                false);
         FileStoreTable table =
-                (FileStoreTable)
-                        catalog.getTable(
-                                
org.apache.paimon.catalog.Identifier.create("test_db", "src"));
+                createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
 
         SourceLineageVertex vertex = 
LineageUtils.sourceLineageVertex("paimon.db.src", true, table);
 
@@ -140,8 +138,7 @@ class LineageUtilsTest {
 
         LineageDataset dataset = vertex.datasets().get(0);
         assertThat(dataset.name()).isEqualTo("paimon.db.src");
-        assertThat(dataset.namespace()).isEqualTo(warehouse.toString());
-        catalog.close();
+        assertThat(dataset.namespace()).contains(tablePath.toString());
     }
 
     @Test
@@ -155,25 +152,58 @@ class LineageUtilsTest {
         
assertThat(vertex.boundedness()).isEqualTo(Boundedness.CONTINUOUS_UNBOUNDED);
     }
 
+    @Test
+    void testSourceLineageVertexKeepsProvidedNameWhenCatalogKeyExists() throws 
Exception {
+        Map<String, String> catalogOptions = new HashMap<>();
+        catalogOptions.put("catalog-key", "jdbc-warehouse");
+        FileStoreTable table = createTableWithCatalogOptions(catalogOptions);
+
+        SourceLineageVertex vertex = 
LineageUtils.sourceLineageVertex("paimon.db.src", true, table);
+
+        assertThat(vertex.datasets().get(0).name()).isEqualTo("paimon.db.src");
+    }
+
+    @Test
+    void testResolveNameByMetastoreUsesCatalogKeyForJdbcMetastore() throws 
Exception {
+        Map<String, String> catalogOptions = new HashMap<>();
+        catalogOptions.put("metastore", "jdbc");
+        catalogOptions.put("catalog-key", "jdbc-warehouse");
+        FileStoreTable table = createTableWithCatalogOptions(catalogOptions);
+
+        // catalog-key is only used when no explicit name is provided 
(DataStream path)
+        assertThat(LineageUtils.resolveNameByMetastore(table, null))
+                .isEqualTo("jdbc-warehouse." + table.fullName());
+        // when explicit name is provided (Table API path), it takes precedence
+        assertThat(LineageUtils.resolveNameByMetastore(table, 
"my_catalog.db.src"))
+                .isEqualTo("my_catalog.db.src");
+    }
+
+    @Test
+    void testResolveNameByMetastoreIgnoresCatalogKeyForNonJdbcMetastore() 
throws Exception {
+        Map<String, String> catalogOptions = new HashMap<>();
+        catalogOptions.put("catalog-key", "jdbc-warehouse");
+        FileStoreTable table = createTableWithCatalogOptions(catalogOptions);
+
+        assertThat(LineageUtils.resolveNameByMetastore(table, "paimon.db.src"))
+                .isEqualTo("paimon.db.src");
+    }
+
+    @Test
+    void testDataStreamSourceLineageVertexUsesCatalogKey() throws Exception {
+        Map<String, String> catalogOptions = new HashMap<>();
+        catalogOptions.put("metastore", "jdbc");
+        catalogOptions.put("catalog-key", "jdbc-warehouse");
+        FileStoreTable table = createTableWithCatalogOptions(catalogOptions);
+
+        SourceLineageVertex vertex = LineageUtils.sourceLineageVertex(true, 
table);
+
+        
assertThat(vertex.datasets().get(0).name()).isEqualTo("jdbc-warehouse." + 
table.fullName());
+    }
+
     @Test
     void testSinkLineageVertex() throws Exception {
-        Options catalogOptions = new Options();
-        catalogOptions.set(CatalogOptions.WAREHOUSE, warehouse.toString());
-        Catalog catalog = 
CatalogFactory.createCatalog(CatalogContext.create(catalogOptions));
-        catalog.createDatabase("test_db", true);
-        catalog.createTable(
-                org.apache.paimon.catalog.Identifier.create("test_db", "sink"),
-                new Schema(
-                        RowType.of(new IntType(), new VarCharType(100), new 
IntType()).getFields(),
-                        Collections.emptyList(),
-                        Arrays.asList("f0"),
-                        new HashMap<>(),
-                        ""),
-                false);
         FileStoreTable table =
-                (FileStoreTable)
-                        catalog.getTable(
-                                
org.apache.paimon.catalog.Identifier.create("test_db", "sink"));
+                createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
 
         LineageVertex vertex = 
LineageUtils.sinkLineageVertex("paimon.db.sink", table);
 
@@ -182,8 +212,19 @@ class LineageUtilsTest {
 
         LineageDataset dataset = vertex.datasets().get(0);
         assertThat(dataset.name()).isEqualTo("paimon.db.sink");
-        assertThat(dataset.namespace()).isEqualTo(warehouse.toString());
-        catalog.close();
+        assertThat(dataset.namespace()).contains(tablePath.toString());
+    }
+
+    @Test
+    void testDataStreamSinkLineageVertexUsesCatalogKey() throws Exception {
+        Map<String, String> catalogOptions = new HashMap<>();
+        catalogOptions.put("metastore", "jdbc");
+        catalogOptions.put("catalog-key", "jdbc-warehouse");
+        FileStoreTable table = createTableWithCatalogOptions(catalogOptions);
+
+        LineageVertex vertex = LineageUtils.sinkLineageVertex(table);
+
+        
assertThat(vertex.datasets().get(0).name()).isEqualTo("jdbc-warehouse." + 
table.fullName());
     }
 
     @Test
@@ -199,7 +240,6 @@ class LineageUtilsTest {
 
         DatasetConfigFacet configFacet = (DatasetConfigFacet) 
facets.get("config");
         Map<String, String> config = configFacet.config();
-        assertThat(config).containsEntry("type", "paimon");
         assertThat(config).containsEntry("partition-keys", "f2");
         assertThat(config).containsEntry("primary-keys", "f0,f2");
     }
@@ -219,6 +259,27 @@ class LineageUtilsTest {
         assertThat(config).containsEntry(CoreOptions.CHANGELOG_PRODUCER.key(), 
"lookup");
     }
 
+    @Test
+    void testConfigFacetIncludesIcebergOptionsAndExcludesArbitrary() throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put("metadata.iceberg.storage", "hadoop-catalog");
+        options.put("metadata.iceberg.uri", "s3://my-bucket/iceberg");
+        options.put("metadata.iceberg.rest.token", "should-not-appear");
+        options.put("some.arbitrary.secret", "should-not-appear");
+
+        FileStoreTable table = createTable(options, Collections.emptyList(), 
Arrays.asList("f0"));
+
+        LineageVertex vertex = LineageUtils.sinkLineageVertex("paimon.db.t", 
table);
+        LineageDataset dataset = vertex.datasets().get(0);
+
+        DatasetConfigFacet configFacet = (DatasetConfigFacet) 
dataset.facets().get("config");
+        Map<String, String> config = configFacet.config();
+        assertThat(config).containsEntry("metadata.iceberg.storage", 
"hadoop-catalog");
+        assertThat(config).containsEntry("metadata.iceberg.uri", 
"s3://my-bucket/iceberg");
+        assertThat(config).doesNotContainKey("metadata.iceberg.rest.token");
+        assertThat(config).doesNotContainKey("some.arbitrary.secret");
+    }
+
     @Test
     void testConfigFacetWithEmptyKeys() throws Exception {
         FileStoreTable table =
@@ -233,6 +294,24 @@ class LineageUtilsTest {
         assertThat(config).containsEntry("primary-keys", "");
     }
 
+    @Test
+    void testSchemaFacetContainsPaimonFields() throws Exception {
+        FileStoreTable table =
+                createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
+
+        LineageVertex vertex = LineageUtils.sinkLineageVertex("paimon.db.t", 
table);
+        LineageDataset dataset = vertex.datasets().get(0);
+
+        Map<String, LineageDatasetFacet> facets = dataset.facets();
+        assertThat(facets).containsKey("schema");
+
+        DatasetSchemaFacet schemaFacet = (DatasetSchemaFacet) 
facets.get("schema");
+        assertThat(schemaFacet.fields()).containsOnlyKeys("f0", "f1", "f2");
+        assertThat(schemaFacet.fields().get("f0").type()).isEqualTo("INT NOT 
NULL");
+        
assertThat(schemaFacet.fields().get("f1").type()).isEqualTo("VARCHAR(100)");
+        assertThat(schemaFacet.fields().get("f2").type()).isEqualTo("INT");
+    }
+
     @Test
     void testScanProviderImplementsLineageVertexProvider() throws Exception {
         FileStoreTable table =
@@ -249,16 +328,47 @@ class LineageUtilsTest {
     }
 
     @Test
-    void testSinkProviderImplementsLineageVertexProvider() throws Exception {
+    void testSinkLineageViaPaimonDiscardingSink() throws Exception {
         FileStoreTable table =
                 createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
 
-        PaimonDataStreamSinkProvider provider =
-                new PaimonDataStreamSinkProvider(dataStream -> null, 
"paimon.db.sink", table);
+        PaimonDiscardingSink<?> sink = new PaimonDiscardingSink<>(table);
 
-        assertThat(provider).isInstanceOf(LineageVertexProvider.class);
-        LineageVertex vertex = provider.getLineageVertex();
+        assertThat(sink).isInstanceOf(LineageVertexProvider.class);
+        LineageVertex vertex = sink.getLineageVertex();
+        assertThat(vertex.datasets()).hasSize(1);
+    }
+
+    @Test
+    void testPaimonDataStreamSourceWrapsMonitorSourceLineageVertex() throws 
Exception {
+        FileStoreTable table =
+                createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
+
+        PaimonDataStreamSource<?, ?, ?> source =
+                new PaimonDataStreamSource<>(
+                        new MonitorSource(table.newReadBuilder(), 10, false, 
true), table);
+
+        assertThat(source).isInstanceOf(LineageVertexProvider.class);
+        SourceLineageVertex vertex = (SourceLineageVertex) 
source.getLineageVertex();
+        assertThat(vertex.boundedness()).isEqualTo(Boundedness.BOUNDED);
+        assertThat(vertex.datasets()).hasSize(1);
+        assertThat(vertex.datasets().get(0).name()).isEqualTo("paimon." + 
table.fullName());
+    }
+
+    @Test
+    void testPaimonDataStreamSourceWrapsFlinkSourceLineageVertex() throws 
Exception {
+        FileStoreTable table =
+                createTable(new HashMap<>(), Collections.emptyList(), 
Arrays.asList("f0"));
+
+        PaimonDataStreamSource<?, ?, ?> source =
+                new PaimonDataStreamSource<>(
+                        new ContinuousFileStoreSource(
+                                table.newReadBuilder(), table.options(), null),
+                        table);
+
+        SourceLineageVertex vertex = (SourceLineageVertex) 
source.getLineageVertex();
+        
assertThat(vertex.boundedness()).isEqualTo(Boundedness.CONTINUOUS_UNBOUNDED);
         assertThat(vertex.datasets()).hasSize(1);
-        
assertThat(vertex.datasets().get(0).name()).isEqualTo("paimon.db.sink");
+        assertThat(vertex.datasets().get(0).name()).isEqualTo("paimon." + 
table.fullName());
     }
 }
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSinkTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSinkTest.java
new file mode 100644
index 0000000000..9a838bea68
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkFormatTableDataStreamSinkTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.paimon.flink.sink;
+
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.table.FormatTable;
+import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.RowType;
+
+import org.apache.flink.streaming.api.lineage.LineageVertex;
+import org.apache.flink.streaming.api.lineage.LineageVertexProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.lang.reflect.Constructor;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link FlinkFormatTableDataStreamSink}. */
+class FlinkFormatTableDataStreamSinkTest {
+
+    @TempDir java.nio.file.Path temp;
+
+    @Test
+    void testFormatTableSinkLineageVertex() throws Exception {
+        FormatTable table =
+                FormatTable.builder()
+                        .fileIO(LocalFileIO.create())
+                        .identifier(Identifier.create("test_db", "test_table"))
+                        .rowType(RowType.of(new IntType()))
+                        .partitionKeys(Collections.emptyList())
+                        .location(new Path(temp.toUri().toString()).toString())
+                        .format(FormatTable.Format.PARQUET)
+                        .options(Collections.singletonMap("path", 
temp.toUri().toString()))
+                        .catalogContext(CatalogContext.create(new Options()))
+                        .build();
+
+        Class<?> sinkClass =
+                Class.forName(
+                        
"org.apache.paimon.flink.sink.FlinkFormatTableDataStreamSink$FormatTableSink");
+        Constructor<?> constructor =
+                sinkClass.getDeclaredConstructor(FormatTable.class, 
boolean.class, Map.class);
+        constructor.setAccessible(true);
+        Object sink = constructor.newInstance(table, false, 
Collections.emptyMap());
+
+        assertThat(sink).isInstanceOf(LineageVertexProvider.class);
+        LineageVertex vertex = ((LineageVertexProvider) 
sink).getLineageVertex();
+        assertThat(vertex.datasets()).hasSize(1);
+        assertThat(vertex.datasets().get(0).name()).isEqualTo("paimon." + 
table.fullName());
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkSinkBuilderLineageTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkSinkBuilderLineageTest.java
new file mode 100644
index 0000000000..a7ba7f6879
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/FlinkSinkBuilderLineageTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.paimon.flink.sink;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.FileStoreTableFactory;
+import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.RowType;
+
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.transformations.SinkTransformation;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.paimon.flink.LogicalTypeConversion.toLogicalType;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for sink lineage in {@link FlinkSinkBuilder}. */
+class FlinkSinkBuilderLineageTest {
+
+    @TempDir java.nio.file.Path temp;
+
+    @Test
+    void testFlinkSinkBuilderUsesPaimonDiscardingSinkForLineage() throws 
Exception {
+        FileStoreTable table = createTable();
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        DataStream<RowData> input =
+                env.fromCollection(
+                        Collections.singletonList((RowData) 
GenericRowData.of(1)),
+                        InternalTypeInfo.of(toLogicalType(table.rowType())));
+
+        DataStreamSink<?> sink = new 
FlinkSinkBuilder(table).forRowData(input).build();
+
+        
assertThat(sink.getTransformation()).isInstanceOf(SinkTransformation.class);
+        SinkTransformation<?, ?> transformation =
+                (SinkTransformation<?, ?>) sink.getTransformation();
+        
assertThat(transformation.getSink()).isInstanceOf(PaimonDiscardingSink.class);
+    }
+
+    private FileStoreTable createTable() throws Exception {
+        Path tablePath = new Path(temp.toUri().toString());
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "-1");
+        new SchemaManager(LocalFileIO.create(), tablePath)
+                .createTable(
+                        new Schema(
+                                RowType.of(new IntType()).getFields(),
+                                Collections.emptyList(),
+                                Collections.emptyList(),
+                                options,
+                                ""));
+        return FileStoreTableFactory.create(LocalFileIO.create(), tablePath);
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
index bc2ccb0fed..9f6c46c2a7 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
@@ -22,16 +22,25 @@ import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.CatalogContext;
 import org.apache.paimon.catalog.CatalogFactory;
 import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.source.operator.MonitorSource;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.types.DataTypes;
 
+import org.apache.flink.api.dag.Transformation;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.transformations.SourceTransformation;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import java.nio.file.Path;
 
+import static org.apache.paimon.flink.LogicalTypeConversion.toLogicalType;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -114,4 +123,63 @@ public class FlinkSourceBuilderTest {
         builder = new FlinkSourceBuilder(table);
         assertTrue(builder.isUnordered());
     }
+
+    @Test
+    public void testBuildWrapsStaticSourceWithPaimonDataStreamSource() throws 
Exception {
+        Table table = createTable("static_source", false, -1, true);
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+
+        DataStream<RowData> dataStream =
+                new 
FlinkSourceBuilder(table).env(env).sourceBounded(true).build();
+
+        
assertThat(dataStream.getTransformation()).isInstanceOf(SourceTransformation.class);
+        SourceTransformation<?, ?, ?> transformation =
+                (SourceTransformation<?, ?, ?>) dataStream.getTransformation();
+        
assertThat(transformation.getSource()).isInstanceOf(PaimonDataStreamSource.class);
+    }
+
+    @Test
+    public void testBuildWrapsContinuousSourceWithPaimonDataStreamSource() 
throws Exception {
+        Table table = createTable("continuous_source", false, -1, true);
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+
+        DataStream<RowData> dataStream =
+                new 
FlinkSourceBuilder(table).env(env).sourceBounded(false).build();
+
+        
assertThat(dataStream.getTransformation()).isInstanceOf(SourceTransformation.class);
+        SourceTransformation<?, ?, ?> transformation =
+                (SourceTransformation<?, ?, ?>) dataStream.getTransformation();
+        
assertThat(transformation.getSource()).isInstanceOf(PaimonDataStreamSource.class);
+    }
+
+    @Test
+    public void testMonitorSourceBuildSourceWrapsWithPaimonDataStreamSource() 
throws Exception {
+        Table table = createTable("monitor_source", false, -1, true);
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+
+        DataStream<RowData> dataStream =
+                MonitorSource.buildSource(
+                        env,
+                        "source",
+                        InternalTypeInfo.of(toLogicalType(table.rowType())),
+                        table.newReadBuilder(),
+                        10,
+                        false,
+                        false,
+                        false,
+                        null,
+                        true,
+                        null,
+                        table);
+
+        assertThat(dataStream.getTransformation().getTransitivePredecessors())
+                .filteredOn(Transformation.class::isInstance)
+                .filteredOn(transformation -> transformation instanceof 
SourceTransformation)
+                .anySatisfy(
+                        transformation ->
+                                assertThat(
+                                                ((SourceTransformation<?, ?, 
?>) transformation)
+                                                        .getSource())
+                                        
.isInstanceOf(PaimonDataStreamSource.class));
+    }
 }

Reply via email to