Repository: alibaba/fluss
Pull request #710: [Datastream] Introduce FlussSource
Author: login: polyzos
Created: 2025-04-02T06:45:25Z | Merged: 2025-04-29T15:38:04Z

This PR introduces the FlussSource for the Datastream API


Diff:
diff --git a/fluss-client/src/main/java/com/alibaba/fluss/client/FlussConnection.java b/fluss-client/src/main/java/com/alibaba/fluss/client/FlussConnection.java
index 1066a08a2c..54a75be20a 100644
--- a/fluss-client/src/main/java/com/alibaba/fluss/client/FlussConnection.java
+++ b/fluss-client/src/main/java/com/alibaba/fluss/client/FlussConnection.java
@@ -40,7 +40,6 @@
 import com.alibaba.fluss.rpc.metrics.ClientMetricGroup;
 
 import java.time.Duration;
-import java.util.Collections;
 import java.util.List;
 
 import static com.alibaba.fluss.client.utils.MetadataUtils.getOneAvailableTabletServerNode;
@@ -88,7 +87,8 @@ public Admin getAdmin() {
 
     @Override
     public Table getTable(TablePath tablePath) {
-        metadataUpdater.checkAndUpdateTableMetadata(Collections.singleton(tablePath));
+        // force to update the table info from server to avoid stale data in cache
+        metadataUpdater.updateTableOrPartitionMetadata(tablePath, null);
         TableInfo tableInfo = metadataUpdater.getTableInfoOrElseThrow(tablePath);
         return new FlussTable(this, tablePath, tableInfo);
     }
diff --git a/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlinkSource.java b/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlinkSource.java
index 2fdebb3edf..5a1e48914f 100644
--- a/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlinkSource.java
+++ b/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlinkSource.java
@@ -32,12 +32,14 @@
 import com.alibaba.fluss.metadata.TablePath;
 import com.alibaba.fluss.types.RowType;
 
+import org.apache.flink.api.common.typeinfo.TypeInformation;
 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.SplitEnumerator;
 import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
 import org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
 import org.apache.flink.connector.base.source.reader.synchronization.FutureCompletingBlockingQueue;
 import org.apache.flink.core.io.SimpleVersionedSerializer;
@@ -45,7 +47,8 @@
 import javax.annotation.Nullable;
 
 /** Flink source for Fluss. */
-public class FlinkSource<OUT> implements Source<OUT, SourceSplitBase, SourceEnumeratorState> {
+public class FlinkSource<OUT>
+        implements Source<OUT, SourceSplitBase, SourceEnumeratorState>, ResultTypeQueryable {
     private static final long serialVersionUID = 1L;
 
     private final Configuration flussConf;
@@ -54,8 +57,8 @@ public class FlinkSource<OUT> implements Source<OUT, SourceSplitBase, SourceEnum
     private final boolean isPartitioned;
     private final RowType sourceOutputType;
     @Nullable private final int[] projectedFields;
-    private final OffsetsInitializer offsetsInitializer;
-    private final long scanPartitionDiscoveryIntervalMs;
+    protected final OffsetsInitializer offsetsInitializer;
+    protected final long scanPartitionDiscoveryIntervalMs;
     private final boolean streaming;
     private final FlussDeserializationSchema<OUT> deserializationSchema;
 
@@ -153,4 +156,9 @@ public SourceReader<OUT, SourceSplitBase> createReader(SourceReaderContext conte
                 flinkSourceReaderMetrics,
                 recordEmitter);
     }
+
+    @Override
+    public TypeInformation<OUT> getProducedType() {
+        return deserializationSchema.getProducedType(sourceOutputType);
+    }
 }
diff --git a/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java b/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
new file mode 100644
index 0000000000..fa3f1c48a3
--- /dev/null
+++ b/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.annotation.VisibleForTesting;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Flink DataStream source implementation for reading data from Fluss tables.
+ *
+ * <p>This class extends the {@code FlinkSource} base class and implements {@code
+ * ResultTypeQueryable} to provide type information for Flink's type system.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> flussSource = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ *
+ * DataStreamSource<Order> stream = env.fromSource(
+ *     flussSource,
+ *     WatermarkStrategy.noWatermarks(),
+ *     "Fluss Source"
+ * );
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by this source
+ */
+public class FlussSource<OUT> extends FlinkSource<OUT> {
+    private static final long serialVersionUID = 1L;
+
+    FlussSource(
+            Configuration flussConf,
+            TablePath tablePath,
+            boolean hasPrimaryKey,
+            boolean isPartitioned,
+            RowType sourceOutputType,
+            @Nullable int[] projectedFields,
+            OffsetsInitializer offsetsInitializer,
+            long scanPartitionDiscoveryIntervalMs,
+            FlussDeserializationSchema<OUT> deserializationSchema,
+            boolean streaming) {
+        super(
+                flussConf,
+                tablePath,
+                hasPrimaryKey,
+                isPartitioned,
+                sourceOutputType,
+                projectedFields,
+                offsetsInitializer,
+                scanPartitionDiscoveryIntervalMs,
+                deserializationSchema,
+                streaming);
+    }
+
+    /**
+     * Get a FlussSourceBuilder to build a {@link FlussSource}.
+     *
+     * @return a Fluss source builder.
+     */
+    public static <T> FlussSourceBuilder<T> builder() {
+        return new FlussSourceBuilder<>();
+    }
+
+    @VisibleForTesting
+    OffsetsInitializer getOffsetsInitializer() {
+        return offsetsInitializer;
+    }
+}
diff --git a/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java b/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
new file mode 100644
index 0000000000..973f443db5
--- /dev/null
+++ b/fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.FlinkConnectorOptions;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<OUT> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<OUT> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+
+    public FlussSourceBuilder<OUT> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setDeserializationSchema(
+            FlussDeserializationSchema<OUT> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    // TODO: refactor this method to use field names instead of indexes
+    //  see: https://github.com/alibaba/fluss/issues/804
+    public FlussSourceBuilder<OUT> setProjectedFields(int[] projectedFields) {
+        this.projectedFields = projectedFields;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setFlussConfig(Configuration flussConf) {
+        this.flussConf = flussConf;
+        return this;
+    }
+
+    public FlussSource<OUT> build() {
+        checkNotNull(bootstrapServers, "BootstrapServers is required but not provided.");
+        checkNotNull(database, "Database is required but not provided.");
+        if (database.isEmpty()) {
+            throw new IllegalArgumentException("Database must not be empty.");
+        }
+        checkNotNull(tableName, "TableName is required but not provided.");
+        if (tableName.isEmpty()) {
+            throw new IllegalArgumentException("TableName must not be empty.");
+        }
+        checkNotNull(deserializationSchema, "Deserialization schema is required but not provided.");
+
+        // if null use the default value:
+        if (offsetsInitializer == null) {
+            offsetsInitializer = OffsetsInitializer.initial();
+        }
+
+        // if null use the default value:
+        if (scanPartitionDiscoveryIntervalMs == null) {
+            scanPartitionDiscoveryIntervalMs =
+                    FlinkConnectorOptions.SCAN_PARTITION_DISCOVERY_INTERVAL
+                            .defaultValue()
+                            .toMillis();
+        }
+
+        if (this.flussConf == null) {
+            this.flussConf = new Configuration();
+        }
+
+        TablePath tablePath = new TablePath(this.database, this.tableName);
+        this.flussConf.setString(ConfigOptions.BOOTSTRAP_SERVERS.key(), bootstrapServers);
+
+        TableInfo tableInfo;
+        try (Connection connection = ConnectionFactory.createConnection(flussConf);
+                Admin admin = connection.getAdmin()) {
+            try {
+                tableInfo = admin.getTableInfo(tablePath).get();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("Interrupted while getting table info", e);
+            } catch (ExecutionException e) {
+                throw new RuntimeException("Failed to get table info", e);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(
+                    "Failed to initialize FlussSource admin connection: " + e.getMessage(), e);
+        }
+
+        flussConf.addAll(tableInfo.getCustomProperties());
+        flussConf.addAll(tableInfo.getProperties());
+
+        boolean isPartitioned = !tableInfo.getPartitionKeys().isEmpty();
+        boolean hasPrimaryKey = !tableInfo.getPrimaryKeys().isEmpty();
+
+        RowType sourceOutputType =
+                projectedFields != null
+                        ? tableInfo.getRowType().project(projectedFields)
+                        : tableInfo.getRowType();
+
+        LOG.info("Creating Fluss Source with Configuration: {}", flussConf);
+
+        return new FlussSource<>(
+                flussConf,
+                tablePath,
+                hasPrimaryKey,
+                isPartitioned,
+                sourceOutputType,
+                projectedFields,
+                offsetsInitializer,
+                scanPartitionDiscoveryIntervalMs,
+                deserializationSchema,
+                true);
+    }
+}
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchema.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchema.java
index aaba2fe133..fe4243cffe 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchema.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchema.java
@@ -17,7 +17,7 @@
 package com.alibaba.fluss.flink.sink.serializer;
 
 import com.alibaba.fluss.annotation.PublicEvolving;
-import com.alibaba.fluss.flink.common.Order;
+import com.alibaba.fluss.flink.source.testutils.Order;
 import com.alibaba.fluss.types.RowType;
 
 import org.apache.flink.table.data.GenericRowData;
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchemaTest.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchemaTest.java
index 379130c1af..56b5d0382e 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchemaTest.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/sink/serializer/OrderSerializationSchemaTest.java
@@ -16,7 +16,7 @@
 
 package com.alibaba.fluss.flink.sink.serializer;
 
-import com.alibaba.fluss.flink.common.Order;
+import com.alibaba.fluss.flink.source.testutils.Order;
 import com.alibaba.fluss.types.BigIntType;
 import com.alibaba.fluss.types.DataField;
 import com.alibaba.fluss.types.IntType;
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceBuilderTest.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceBuilderTest.java
new file mode 100644
index 0000000000..15953b54d4
--- /dev/null
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceBuilderTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.record.LogRecord;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+/** Tests for the {@link FlussSourceBuilder} class. */
+public class FlussSourceBuilderTest extends FlinkTestBase {
+
+    private static String bootstrapServers;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        createTable(DEFAULT_TABLE_PATH, DEFAULT_PK_TABLE_DESCRIPTOR);
+    }
+
+    @Test
+    public void testBuildWithValidConfiguration() {
+        // Given
+        FlussSource<TestRecord> source =
+                FlussSource.<TestRecord>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(DEFAULT_TABLE_PATH.getTableName())
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new TestDeserializationSchema())
+                        .build();
+
+        // Then
+        assertThat(source).isNotNull();
+    }
+
+    @Test
+    public void testMissingBootstrapServers() {
+        // Given
+        Executable executable =
+                () ->
+                        FlussSource.<TestRecord>builder()
+                                .setDatabase(DEFAULT_DB)
+                                .setTable(DEFAULT_TABLE_PATH.getTableName())
+                                .setStartingOffsets(OffsetsInitializer.earliest())
+                                .setScanPartitionDiscoveryIntervalMs(1000L)
+                                .setDeserializationSchema(new TestDeserializationSchema())
+                                .build();
+
+        // Then
+        assertThatThrownBy(executable::execute)
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("BootstrapServers is required but not provided.");
+    }
+
+    @Test
+    public void testEmptyBootstrapServers() {
+        // Given
+        Executable executable =
+                () ->
+                        FlussSource.<TestRecord>builder()
+                                .setBootstrapServers("")
+                                .setDatabase(DEFAULT_DB)
+                                .setTable(DEFAULT_TABLE_PATH.getTableName())
+                                .setStartingOffsets(OffsetsInitializer.earliest())
+                                .setScanPartitionDiscoveryIntervalMs(1000L)
+                                .setDeserializationSchema(new TestDeserializationSchema())
+                                .build();
+
+        assertThatThrownBy(executable::execute)
+                .isInstanceOf(RuntimeException.class)
+                .hasMessage(
+                        "Failed to initialize FlussSource admin connection: No resolvable bootstrap urls given in bootstrap.servers");
+    }
+
+    @Test
+    public void testMissingDatabase() {
+        // Given
+        Executable executable =
+                () ->
+                        FlussSource.<TestRecord>builder()
+                                .setBootstrapServers(bootstrapServers)
+                                .setTable(DEFAULT_TABLE_PATH.getTableName())
+                                .setStartingOffsets(OffsetsInitializer.earliest())
+                                .setScanPartitionDiscoveryIntervalMs(1000L)
+                                .setDeserializationSchema(new TestDeserializationSchema())
+                                .build();
+
+        // Then
+        assertThatThrownBy(executable::execute)
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("Database is required but not provided.");
+    }
+
+    @Test
+    public void testEmptyDatabase() {
+        // Given
+        Executable executable =
+                () ->
+                        FlussSource.<TestRecord>builder()
+                                .setBootstrapServers(bootstrapServers)
+                                .setDatabase("")
+                                .setTable(DEFAULT_TABLE_PATH.getTableName())
+                                .setStartingOffsets(OffsetsInitializer.earliest())
+                                .setScanPartitionDiscoveryIntervalMs(1000L)
+                                .setDeserializationSchema(new TestDeserializationSchema())
+                                .build();
+
+        // Then
+        assertThatThrownBy(executable::execute)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Database must not be empty.");
+    }
+
+    @Test
+    public void testMissingTable() {
+        // Given
+        Executable executable =
+                () ->
+                        FlussSource.<TestRecord>builder()
+                                .setBootstrapServers(bootstrapServers)
+                                .setDatabase(DEFAULT_DB)
+                                .setStartingOffsets(OffsetsInitializer.earliest())
+                                .setScanPartitionDiscoveryIntervalMs(1000L)
+                                .setDeserializationSchema(new TestDeserializationSchema())
+                                .build();
+
+        // Then
+        assertThatThrownBy(executable::execute)
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("TableName is required but not provided.");
+    }
+
+    @Test
+    public void testEmptyTable() {
+        // Given
+        Executable executable =
+                () ->
+                        FlussSource.<TestRecord>builder()
+                                .setBootstrapServers(bootstrapServers)
+                                .setDatabase(DEFAULT_DB)
+                                .setTable("")
+                                .setStartingOffsets(OffsetsInitializer.earliest())
+                                .setScanPartitionDiscoveryIntervalMs(1000L)
+                                .setDeserializationSchema(new TestDeserializationSchema())
+                                .build();
+
+        // Then
+        assertThatThrownBy(executable::execute)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("TableName must not be empty.");
+    }
+
+    @Test
+    public void testMissingScanPartitionDiscoveryInterval() {
+        // Given
+        FlussSource<TestRecord> source =
+                FlussSource.<TestRecord>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(DEFAULT_TABLE_PATH.getTableName())
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setDeserializationSchema(new TestDeserializationSchema())
+                        .build();
+
+        // Then
+        assertThat(source.scanPartitionDiscoveryIntervalMs).isEqualTo(10000L);
+    }
+
+    @Test
+    public void testMissingOffsetsInitializer() {
+        // Given
+        FlussSource<TestRecord> source =
+                FlussSource.<TestRecord>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(DEFAULT_TABLE_PATH.getTableName())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new TestDeserializationSchema())
+                        .build();
+
+        assertThat(source.getOffsetsInitializer().getClass())
+                .isEqualTo(OffsetsInitializer.initial().getClass());
+    }
+
+    @Test
+    public void testMissingDeserializationSchema() {
+        // Given
+        Executable executable =
+                () ->
+                        FlussSource.<TestRecord>builder()
+                                .setBootstrapServers(bootstrapServers)
+                                .setDatabase(DEFAULT_DB)
+                                .setTable(DEFAULT_TABLE_PATH.getTableName())
+                                .setStartingOffsets(OffsetsInitializer.earliest())
+                                .setScanPartitionDiscoveryIntervalMs(10000L)
+                                .build();
+
+        // Then
+        assertThatThrownBy(executable::execute)
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("Deserialization schema is required but not provided.");
+    }
+
+    @Test
+    public void testSetProjectedFields() {
+        // Given
+        int[] projectedFields = new int[] {0, 1};
+        FlussSource<TestRecord> source =
+                FlussSource.<TestRecord>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(DEFAULT_TABLE_PATH.getTableName())
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new TestDeserializationSchema())
+                        .setProjectedFields(projectedFields)
+                        .build();
+
+        // Then
+        assertThat(source).isNotNull();
+    }
+
+    @Test
+    public void testProjectedFields() {
+        // Given
+        int[] projectedFields = new int[] {0, 1}; // Only include orderId and amount fields
+
+        // When
+        FlussSource<TestRecord> source =
+                FlussSource.<TestRecord>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(DEFAULT_TABLE_PATH.getTableName())
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new TestDeserializationSchema())
+                        .setProjectedFields(projectedFields)
+                        .build();
+
+        // Then
+        assertThat(source).isNotNull();
+    }
+
+    // Test record class for tests
+    private static class TestRecord {
+        private int id;
+        private String name;
+
+        public TestRecord(int id, String name) {
+            this.id = id;
+            this.name = name;
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    // Test deserialization schema for tests
+    private static class TestDeserializationSchema
+            implements FlussDeserializationSchema<TestRecord> {
+
+        @Override
+        public void open(InitializationContext context) throws Exception {}
+
+        @Override
+        public TestRecord deserialize(LogRecord record) throws Exception {
+            InternalRow row = record.getRow();
+            return new TestRecord(row.getInt(0), row.getString(1).toString());
+        }
+
+        @Override
+        public TypeInformation<TestRecord> getProducedType(RowType rowSchema) {
+            return TypeInformation.of(TestRecord.class);
+        }
+    }
+}
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceITCase.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceITCase.java
new file mode 100644
index 0000000000..c51a7bcd76
--- /dev/null
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceITCase.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.client.table.writer.AppendWriter;
+import com.alibaba.fluss.client.table.writer.UpsertWriter;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.flink.source.testutils.Order;
+import com.alibaba.fluss.flink.source.testutils.OrderPartial;
+import com.alibaba.fluss.flink.source.testutils.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.utils.PojoToRowConverter;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.flink.source.testutils.MockDataUtils.binaryRowToGenericRow;
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with PK tables. */
+public class FlussSourceITCase extends FlinkTestBase {
+    private static final List<Order> ORDERS = MockDataUtils.ORDERS;
+
+    private static final Schema PK_SCHEMA = MockDataUtils.getOrdersSchemaPK();
+    private static final Schema LOG_SCHEMA = MockDataUtils.getOrdersSchemaLog();
+
+    private static StreamExecutionEnvironment env;
+    private static String bootstrapServers;
+
+    private final String pkTableName = "orders_test_pk";
+    private final String logTableName = "orders_test_log";
+
+    private final TablePath ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+    private final TablePath ordersPKTablePath = new TablePath(DEFAULT_DB, pkTableName);
+
+    private final TableDescriptor logTableDescriptor =
+            TableDescriptor.builder().schema(LOG_SCHEMA).distributedBy(1, "orderId").build();
+    private final TableDescriptor pkTableDescriptor =
+            TableDescriptor.builder().schema(PK_SCHEMA).distributedBy(1, "orderId").build();
+
+    @BeforeAll
+    public static void beforeAll() {
+        FlinkTestBase.beforeAll();
+        env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(4);
+        bootstrapServers =
+                String.join(
+                        ",",
+                        FLUSS_CLUSTER_EXTENSION
+                                .getClientConfig()
+                                .get(ConfigOptions.BOOTSTRAP_SERVERS));
+    }
+
+    @Test
+    public void testTablePKSource() throws Exception {
+        createTable(ordersPKTablePath, pkTableDescriptor);
+        writeRowsToTable(ordersPKTablePath);
+        // Create a DataStream from the FlussSource
+        FlussSource<Order> flussSource =
+                FlussSource.<Order>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(pkTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new MockDataUtils.OrderDeserializationSchema())
+                        .build();
+
+        DataStreamSource<Order> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        List<Order> collectedElements = stream.executeAndCollect(ORDERS.size());
+
+        // Assert result size and elements match
+        assertThat(collectedElements).hasSameElementsAs(ORDERS);
+    }
+
+    @Test
+    public void testTablePKSourceWithProjectionPushdown() throws Exception {
+        createTable(ordersPKTablePath, pkTableDescriptor);
+        writeRowsToTable(ordersPKTablePath);
+        List<OrderPartial> expectedOutput =
+                Arrays.asList(
+                        new OrderPartial(600, 600),
+                        new OrderPartial(700, 601),
+                        new OrderPartial(800, 602),
+                        new OrderPartial(900, 603),
+                        new OrderPartial(1000, 604));
+
+        // Create a DataStream from the FlussSource
+        FlussSource<OrderPartial> flussSource =
+                FlussSource.<OrderPartial>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(pkTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new OrderPartialDeserializationSchema())
+                        .setProjectedFields(new int[] {0, 2})
+                        .build();
+
+        DataStreamSource<OrderPartial> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        List<OrderPartial> collectedElements = stream.executeAndCollect(ORDERS.size());
+
+        // Assert result size and elements match
+        assertThat(collectedElements).hasSameElementsAs(expectedOutput);
+    }
+
+    @Test
+    public void testRowDataPKTableSource() throws Exception {
+        createTable(ordersPKTablePath, pkTableDescriptor);
+        writeRowsToTable(ordersPKTablePath);
+        Table table = conn.getTable(ordersPKTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
+
+        // Create a DataStream from the FlussSource
+        FlussSource<RowData> flussSource =
+                FlussSource.<RowData>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(pkTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new RowDataDeserializationSchema())
+                        .build();
+
+        DataStreamSource<RowData> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        List<InternalRow> updatedRows =
+                Arrays.asList(row(600L, 20L, 800, "addr1"), row(700L, 22L, 801, "addr2"));
+
+        // send some row updates
+        writeRows(ordersPKTablePath, updatedRows, false);
+
+        List<RowData> expectedResult =
+                Arrays.asList(
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT),
+                        createRowData(800L, 23L, 602, "addr3", RowKind.INSERT),
+                        createRowData(900L, 24L, 603, "addr4", RowKind.INSERT),
+                        createRowData(1000L, 25L, 604, "addr5", RowKind.INSERT),
+                        createRowData(600L, 20L, 600, "addr1", RowKind.UPDATE_BEFORE),
+                        createRowData(600L, 20L, 800, "addr1", RowKind.UPDATE_AFTER),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.UPDATE_BEFORE),
+                        createRowData(700L, 22L, 801, "addr2", RowKind.UPDATE_AFTER));
+
+        List<RowData> rawRows = stream.executeAndCollect(expectedResult.size());
+        List<RowData> collectedRows =
+                rawRows.stream()
+                        .map(row -> binaryRowToGenericRow(row, rowType))
+                        .collect(Collectors.toList());
+
+        // Assert result size and elements match
+        assertThat(expectedResult).hasSameElementsAs(collectedRows);
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        createTable(ordersLogTablePath, logTableDescriptor);
+        writeRowsToTable(ordersLogTablePath);
+        Table table = conn.getTable(ordersLogTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
+
+        // Create a DataStream from the FlussSource
+        FlussSource<RowData> flussSource =
+                FlussSource.<RowData>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new RowDataDeserializationSchema())
+                        .build();
+
+        DataStreamSource<RowData> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        // these rows should be interpreted as Inserts
+        List<InternalRow> updatedRows =
+                Arrays.asList(row(600L, 20L, 600, "addr1"), row(700L, 22L, 601, "addr2"));
+
+        // send some row updates
+        writeRows(ordersLogTablePath, updatedRows, true);
+
+        List<RowData> expectedResult =
+                Arrays.asList(
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT),
+                        createRowData(800L, 23L, 602, "addr3", RowKind.INSERT),
+                        createRowData(900L, 24L, 603, "addr4", RowKind.INSERT),
+                        createRowData(1000L, 25L, 604, "addr5", RowKind.INSERT),
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT));
+
+        List<RowData> rawRows = stream.executeAndCollect(expectedResult.size());
+        List<RowData> collectedRows =
+                rawRows.stream()
+                        .map(row -> binaryRowToGenericRow(row, rowType))
+                        .collect(Collectors.toList());
+
+        // Assert result size and elements match
+        assertThat(expectedResult).hasSameElementsAs(collectedRows);
+    }
+
+    @Test
+    public void testTableLogSourceWithProjectionPushdown() throws Exception {
+        createTable(ordersLogTablePath, logTableDescriptor);
+        writeRowsToTable(ordersLogTablePath);
+        List<OrderPartial> expectedOutput =
+                Arrays.asList(
+                        new OrderPartial(600, 600),
+                        new OrderPartial(700, 601),
+                        new OrderPartial(800, 602),
+                        new OrderPartial(900, 603),
+                        new OrderPartial(1000, 604));
+
+        // Create a DataStream from the FlussSource
+        FlussSource<OrderPartial> flussSource =
+                FlussSource.<OrderPartial>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new OrderPartialDeserializationSchema())
+                        .setProjectedFields(new int[] {0, 2})
+                        .build();
+
+        DataStreamSource<OrderPartial> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        List<OrderPartial> collectedElements = stream.executeAndCollect(ORDERS.size());
+
+        // Assert result size and elements match
+        assertThat(collectedElements).hasSameElementsAs(expectedOutput);
+    }
+
+    private static RowData createRowData(
+            Long orderId, Long itemId, Integer amount, String address, RowKind rowKind) {
+        GenericRowData row = new GenericRowData(4);
+        row.setField(0, orderId);
+        row.setField(1, itemId);
+        row.setField(2, amount);
+        row.setField(3, StringData.fromString(address));
+
+        row.setRowKind(rowKind);
+        return row;
+    }
+
+    private void writeRowsToTable(TablePath tablePath) throws Exception {
+        try (Table table = conn.getTable(tablePath)) {
+            RowType rowType = table.getTableInfo().getRowType();
+            boolean isLogTable = !table.getTableInfo().hasPrimaryKey();
+
+            PojoToRowConverter<Order> converter = new PojoToRowConverter<>(Order.class, rowType);
+
+            List<GenericRow> rows =
+                    ORDERS.stream().map(converter::convert).collect(Collectors.toList());
+
+            if (isLogTable) {
+                AppendWriter writer = table.newAppend().createWriter();
+                rows.forEach(writer::append);
+                writer.flush();
+            } else {
+                UpsertWriter writer = table.newUpsert().createWriter();
+                rows.forEach(writer::upsert);
+                writer.flush();
+            }
+        }
+    }
+}
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/deserializer/FlussDeserializationSchemaTest.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/deserializer/FlussDeserializationSchemaTest.java
index adcb154523..ea221dca64 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/deserializer/FlussDeserializationSchemaTest.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/deserializer/FlussDeserializationSchemaTest.java
@@ -17,7 +17,8 @@
 package com.alibaba.fluss.flink.source.deserializer;
 
 import com.alibaba.fluss.client.table.scanner.ScanRecord;
-import com.alibaba.fluss.flink.common.Order;
+import com.alibaba.fluss.flink.source.testutils.Order;
+import com.alibaba.fluss.flink.source.testutils.OrderDeserializationSchema;
 import com.alibaba.fluss.record.ChangeType;
 import com.alibaba.fluss.row.BinaryString;
 import com.alibaba.fluss.row.GenericRow;
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/emitter/FlinkRecordEmitterTest.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/emitter/FlinkRecordEmitterTest.java
index 56d94ef6bd..2e146aa1e9 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/emitter/FlinkRecordEmitterTest.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/emitter/FlinkRecordEmitterTest.java
@@ -17,14 +17,14 @@
 package com.alibaba.fluss.flink.source.emitter;
 
 import com.alibaba.fluss.client.table.scanner.ScanRecord;
-import com.alibaba.fluss.flink.common.Order;
 import com.alibaba.fluss.flink.source.deserializer.InitializationContextImpl;
-import com.alibaba.fluss.flink.source.deserializer.OrderDeserializationSchema;
 import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
 import com.alibaba.fluss.flink.source.reader.RecordAndPos;
 import com.alibaba.fluss.flink.source.split.HybridSnapshotLogSplit;
 import com.alibaba.fluss.flink.source.split.HybridSnapshotLogSplitState;
 import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.Order;
+import com.alibaba.fluss.flink.source.testutils.OrderDeserializationSchema;
 import com.alibaba.fluss.flink.utils.FlussRowToFlinkRowConverter;
 import com.alibaba.fluss.metadata.Schema;
 import com.alibaba.fluss.metadata.TableBucket;
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java
index 109614d5d0..3024e07ac9 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java
@@ -639,8 +639,7 @@ private void checkSplitAssignmentIgnoreSnapshotFiles(
         }
     }
 
-    private static Map<Integer, Integer> putRows(TablePath tablePath, int rowsNum)
-            throws Exception {
+    private Map<Integer, Integer> putRows(TablePath tablePath, int rowsNum) throws Exception {
         CompactedKeyEncoder keyEncoder =
                 new CompactedKeyEncoder(
                         DEFAULT_PK_TABLE_SCHEMA.getRowType(),
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/MockDataUtils.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/MockDataUtils.java
new file mode 100644
index 0000000000..2ecee33834
--- /dev/null
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/MockDataUtils.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source.testutils;
+
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.record.LogRecord;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.DataTypes;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.LogicalType;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.alibaba.fluss.flink.utils.FlinkConversions.toFlinkRowType;
+
+/**
+ * Utility class for generating mock data and test infrastructure for Fluss-Flink integration tests.
+ *
+ * <p>The mock data and utilities in this class are used across various Fluss source connector tests
+ * to verify functionality like schema handling, record deserialization, and data type conversion.
+ */
+public class MockDataUtils {
+    public static final List<Order> ORDERS =
+            Arrays.asList(
+                    new Order(600, 20, 600, "addr1"),
+                    new Order(700, 22, 601, "addr2"),
+                    new Order(800, 23, 602, "addr3"),
+                    new Order(900, 24, 603, "addr4"),
+                    new Order(1000, 25, 604, "addr5"));
+
+    public static Schema getOrdersSchemaPK() {
+        return Schema.newBuilder()
+                .column("orderId", DataTypes.BIGINT())
+                .column("itemId", DataTypes.BIGINT())
+                .column("amount", DataTypes.INT())
+                .column("address", DataTypes.STRING())
+                .primaryKey("orderId")
+                .build();
+    }
+
+    public static Schema getOrdersSchemaLog() {
+        return Schema.newBuilder()
+                .column("orderId", DataTypes.BIGINT())
+                .column("itemId", DataTypes.BIGINT())
+                .column("amount", DataTypes.INT())
+                .column("address", DataTypes.STRING())
+                .build();
+    }
+
+    /**
+     * Utility method to create a readable copy of a BinaryRowData. Creates a GenericRowData with
+     * the same content as the BinaryRowData.
+     *
+     * @param binaryRow The BinaryRowData to copy
+     * @param flussRowType The RowType describing the row structure
+     * @return A GenericRowData with the same content
+     */
+    public static RowData binaryRowToGenericRow(RowData binaryRow, RowType flussRowType) {
+        // Convert Fluss RowType to Flink RowType
+        org.apache.flink.table.types.logical.RowType flinkRowType = toFlinkRowType(flussRowType);
+
+        int fieldCount = binaryRow.getArity();
+        GenericRowData genericRow = new GenericRowData(fieldCount);
+        genericRow.setRowKind(binaryRow.getRowKind());
+
+        for (int i = 0; i < fieldCount; i++) {
+            if (binaryRow.isNullAt(i)) {
+                genericRow.setField(i, null);
+                continue;
+            }
+
+            LogicalType fieldType = flinkRowType.getTypeAt(i);
+            switch (fieldType.getTypeRoot()) {
+                case BIGINT:
+                    genericRow.setField(i, binaryRow.getLong(i));
+                    break;
+                case INTEGER:
+                    genericRow.setField(i, binaryRow.getInt(i));
+                    break;
+                case DOUBLE:
+                    genericRow.setField(i, binaryRow.getDouble(i));
+                    break;
+                case FLOAT:
+                    genericRow.setField(i, binaryRow.getFloat(i));
+                    break;
+                case VARCHAR:
+                case CHAR:
+                    genericRow.setField(i, binaryRow.getString(i));
+                    break;
+                case BOOLEAN:
+                    genericRow.setField(i, binaryRow.getBoolean(i));
+                    break;
+                default:
+                    genericRow.setField(i, binaryRow.getString(i));
+            }
+        }
+
+        return genericRow;
+    }
+
+    /**
+     * A deserialization schema for converting Fluss {@link LogRecord} objects to {@link Order}
+     * POJOs.
+     */
+    public static class OrderDeserializationSchema implements FlussDeserializationSchema<Order> {
+
+        @Override
+        public void open(InitializationContext context) throws Exception {}
+
+        @Override
+        public Order deserialize(LogRecord record) throws Exception {
+            InternalRow row = record.getRow();
+            long orderId = row.getLong(0);
+            long itemId = row.getLong(1);
+            int amount = row.getInt(2);
+            String address = String.valueOf(row.getString(3));
+            return new Order(orderId, itemId, amount, address);
+        }
+
+        @Override
+        public TypeInformation<Order> getProducedType(RowType rowSchema) {
+            return TypeInformation.of(Order.class);
+        }
+    }
+}
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/common/Order.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/Order.java
similarity index 98%
rename from fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/common/Order.java
rename to fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/Order.java
index 43b624fbfa..fc6f6d570d 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/common/Order.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/Order.java
@@ -14,7 +14,7 @@
  *  limitations under the License.
  */
 
-package com.alibaba.fluss.flink.common;
+package com.alibaba.fluss.flink.source.testutils;
 
 import java.util.Objects;
 
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/deserializer/OrderDeserializationSchema.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderDeserializationSchema.java
similarity index 96%
rename from fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/deserializer/OrderDeserializationSchema.java
rename to fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderDeserializationSchema.java
index 1cc721bc9d..f006bff0f6 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/deserializer/OrderDeserializationSchema.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderDeserializationSchema.java
@@ -14,10 +14,10 @@
  *  limitations under the License.
  */
 
-package com.alibaba.fluss.flink.source.deserializer;
+package com.alibaba.fluss.flink.source.testutils;
 
 import com.alibaba.fluss.annotation.PublicEvolving;
-import com.alibaba.fluss.flink.common.Order;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
 import com.alibaba.fluss.record.LogRecord;
 import com.alibaba.fluss.row.InternalRow;
 import com.alibaba.fluss.types.RowType;
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderPartial.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderPartial.java
new file mode 100644
index 0000000000..39410ccab4
--- /dev/null
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderPartial.java
@@ -0,0 +1,67 @@
+/*
+ *  Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ *  Licensed 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 com.alibaba.fluss.flink.source.testutils;
+
+import java.util.Objects;
+
+/** A partial representation of an Order containing only the orderId and amount fields. */
+public class OrderPartial {
+    private long orderId;
+    private int amount;
+
+    public OrderPartial() {}
+
+    public OrderPartial(long orderId, int amount) {
+        this.orderId = orderId;
+        this.amount = amount;
+    }
+
+    public long getOrderId() {
+        return orderId;
+    }
+
+    public void setOrderId(long orderId) {
+        this.orderId = orderId;
+    }
+
+    public int getAmount() {
+        return amount;
+    }
+
+    public void setAmount(int amount) {
+        this.amount = amount;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        OrderPartial order = (OrderPartial) o;
+        return orderId == order.orderId && amount == order.amount;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(orderId, amount);
+    }
+
+    @Override
+    public String toString() {
+        return "Order{" + "order_id=" + orderId + ", amount=" + amount + '\'' + '}';
+    }
+}
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderPartialDeserializationSchema.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderPartialDeserializationSchema.java
new file mode 100644
index 0000000000..6796b3d98f
--- /dev/null
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/OrderPartialDeserializationSchema.java
@@ -0,0 +1,57 @@
+/*
+ *  Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ *  Licensed 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 com.alibaba.fluss.flink.source.testutils;
+
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.record.LogRecord;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.DataTypeRoot;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+
+import static com.alibaba.fluss.utils.Preconditions.checkArgument;
+
+/**
+ * A deserialization schema for partially deserializing Order records to {@link OrderPartial}
+ * objects.
+ */
+public class OrderPartialDeserializationSchema implements FlussDeserializationSchema<OrderPartial> {
+
+    @Override
+    public void open(InitializationContext context) throws Exception {
+        RowType rowSchema = context.getRowSchema();
+        checkArgument(rowSchema.getFieldCount() == 2);
+        checkArgument(rowSchema.getFields().get(0).getType().getTypeRoot() == DataTypeRoot.BIGINT);
+        checkArgument(rowSchema.getFields().get(1).getType().getTypeRoot() == DataTypeRoot.INTEGER);
+    }
+
+    @Override
+    public OrderPartial deserialize(LogRecord record) throws Exception {
+        InternalRow row = record.getRow();
+
+        long orderId = row.getLong(0);
+        int amount = row.getInt(1);
+
+        return new OrderPartial(orderId, amount);
+    }
+
+    @Override
+    public TypeInformation<OrderPartial> getProducedType(RowType rowSchema) {
+        return TypeInformation.of(OrderPartial.class);
+    }
+}
diff --git a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/utils/PojoToRowConverterTest.java b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/utils/PojoToRowConverterTest.java
index a3fad57070..268f9757e5 100644
--- a/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/utils/PojoToRowConverterTest.java
+++ b/fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/utils/PojoToRowConverterTest.java
@@ -16,7 +16,7 @@
 
 package com.alibaba.fluss.flink.utils;
 
-import com.alibaba.fluss.flink.common.Order;
+import com.alibaba.fluss.flink.source.testutils.Order;
 import com.alibaba.fluss.row.GenericRow;
 import com.alibaba.fluss.types.BigIntType;
 import com.alibaba.fluss.types.BinaryType;


Reviews:
user: login: wuchong
body: Thanks for the contribution @polyzos . I left some comments. Please also rebase your branch to the latest `main` branch, but NOT squash commits. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T14:05:38Z
user: login: wuchong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T14:19:16Z
user: login: polyzos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T10:06:51Z
user: login: polyzos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:14:07Z
user: login: wuchong
body: I only left some minor comments and pushed a commit to fix it. 

Will merge it once CI is passed. 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T13:01:09Z

Review comments:
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: `IN` should be `OUT`, as this is the output type instead of input type
created_at: 2025-04-27T13:13:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: Currently, the `FlussSource` only supports unbounded mode, so let's remove the `setIsStreaming` method for now. If we want to support bounded mode, we should introduce `stoppingOffsetsInitializer` for the source. 
created_at: 2025-04-27T13:18:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
+        if (!isStreaming) {
+            this.streaming = false;
+        }
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setProjectedFields(int[] projectedFields) {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: The indexes are not friendly to use for end-users. Let's support projection by field names `setProjectedFields(String... fieldNames)`. 
created_at: 2025-04-27T13:18:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
+        if (!isStreaming) {
+            this.streaming = false;
+        }
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setProjectedFields(int[] projectedFields) {
+        this.projectedFields = projectedFields;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setFlussConfig(Configuration flussConf) {
+        this.flussConf = flussConf;
+        return this;
+    }
+
+    public FlussSource<IN> build() {
+        checkNotNull(bootstrapServers, "BootstrapServers is required but not provided.");
+        checkNotNull(database, "Database is required but not provided.");
+        if (database.isEmpty()) {
+            throw new IllegalArgumentException("Database must not be empty.");
+        }
+        checkNotNull(tableName, "TableName is required but not provided.");
+        if (tableName.isEmpty()) {
+            throw new IllegalArgumentException("TableName must not be empty.");
+        }
+        checkNotNull(deserializationSchema, "Deserialization schema is required but not provided.");
+
+        checkNotNull(
+                scanPartitionDiscoveryIntervalMs,
+                "ScanPartitionDiscoveryIntervalMs is required but not provided.");
+        checkNotNull(offsetsInitializer, "OffsetsInitializer is required but not provided.");
+
+        if (this.flussConf == null) {
+            this.flussConf = new Configuration();
+        }
+
+        this.tablePath = new TablePath(this.database, this.tableName);
+        this.flussConf.setString("bootstrap.servers", bootstrapServers);
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: Use `ConfigOptions.BOOTSTRAP_SERVERS.key()` as the key.
created_at: 2025-04-27T13:20:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: can be local variables
created_at: 2025-04-27T13:21:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.split.SourceSplitBase;
+import com.alibaba.fluss.flink.source.state.SourceEnumeratorState;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.source.SplitEnumerator;
+import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Flink DataStream source implementation for reading data from Fluss tables.
+ *
+ * <p>This class extends the {@code FlinkSource} base class and implements {@code
+ * ResultTypeQueryable} to provide type information for Flink's type system.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> flussSource = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ *
+ * DataStreamSource<Order> stream = env.fromSource(
+ *     flussSource,
+ *     WatermarkStrategy.noWatermarks(),
+ *     "Fluss Source"
+ * );
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by this source
+ */
+public class FlussSource<OUT> extends FlinkSource<OUT> implements ResultTypeQueryable {
+    private static final long serialVersionUID = 1L;
+
+    private final FlussDeserializationSchema<OUT> deserializationSchema;
+    private final RowType sourceOutputType;
+    private final boolean streaming;
+
+    public FlussSource(
+            Configuration flussConf,
+            TablePath tablePath,
+            boolean hasPrimaryKey,
+            boolean isPartitioned,
+            RowType sourceOutputType,
+            @Nullable int[] projectedFields,
+            OffsetsInitializer offsetsInitializer,
+            long scanPartitionDiscoveryIntervalMs,
+            FlussDeserializationSchema<OUT> deserializationSchema,
+            boolean streaming) {
+        super(
+                flussConf,
+                tablePath,
+                hasPrimaryKey,
+                isPartitioned,
+                sourceOutputType,
+                projectedFields,
+                offsetsInitializer,
+                scanPartitionDiscoveryIntervalMs,
+                deserializationSchema,
+                streaming);
+        this.deserializationSchema = deserializationSchema;
+        this.sourceOutputType = sourceOutputType;
+        this.streaming = streaming;
+    }
+
+    @Override
+    public SplitEnumerator createEnumerator(SplitEnumeratorContext splitEnumeratorContext) {
+        return super.createEnumerator(splitEnumeratorContext);
+    }
+
+    @Override
+    public SplitEnumerator<SourceSplitBase, SourceEnumeratorState> restoreEnumerator(
+            SplitEnumeratorContext splitEnumeratorContext,
+            SourceEnumeratorState sourceEnumeratorState) {
+        return super.restoreEnumerator(splitEnumeratorContext, sourceEnumeratorState);
+    }
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
user: login: wuchong
body: nothing changed, we don't need to override them?
created_at: 2025-04-27T13:25:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.split.SourceSplitBase;
+import com.alibaba.fluss.flink.source.state.SourceEnumeratorState;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.source.SplitEnumerator;
+import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Flink DataStream source implementation for reading data from Fluss tables.
+ *
+ * <p>This class extends the {@code FlinkSource} base class and implements {@code
+ * ResultTypeQueryable} to provide type information for Flink's type system.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> flussSource = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ *
+ * DataStreamSource<Order> stream = env.fromSource(
+ *     flussSource,
+ *     WatermarkStrategy.noWatermarks(),
+ *     "Fluss Source"
+ * );
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by this source
+ */
+public class FlussSource<OUT> extends FlinkSource<OUT> implements ResultTypeQueryable {
+    private static final long serialVersionUID = 1L;
+
+    private final FlussDeserializationSchema<OUT> deserializationSchema;
+    private final RowType sourceOutputType;
+    private final boolean streaming;
+
+    public FlussSource(
+            Configuration flussConf,
+            TablePath tablePath,
+            boolean hasPrimaryKey,
+            boolean isPartitioned,
+            RowType sourceOutputType,
+            @Nullable int[] projectedFields,
+            OffsetsInitializer offsetsInitializer,
+            long scanPartitionDiscoveryIntervalMs,
+            FlussDeserializationSchema<OUT> deserializationSchema,
+            boolean streaming) {
+        super(
+                flussConf,
+                tablePath,
+                hasPrimaryKey,
+                isPartitioned,
+                sourceOutputType,
+                projectedFields,
+                offsetsInitializer,
+                scanPartitionDiscoveryIntervalMs,
+                deserializationSchema,
+                streaming);
+        this.deserializationSchema = deserializationSchema;
+        this.sourceOutputType = sourceOutputType;
+        this.streaming = streaming;
+    }
+
+    @Override
+    public SplitEnumerator createEnumerator(SplitEnumeratorContext splitEnumeratorContext) {
+        return super.createEnumerator(splitEnumeratorContext);
+    }
+
+    @Override
+    public SplitEnumerator<SourceSplitBase, SourceEnumeratorState> restoreEnumerator(
+            SplitEnumeratorContext splitEnumeratorContext,
+            SourceEnumeratorState sourceEnumeratorState) {
+        return super.restoreEnumerator(splitEnumeratorContext, sourceEnumeratorState);
+    }
+
+    public static <T> FlussSourceBuilder<T> builder() {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
user: login: wuchong
body: nit, add Javadoc

```java
    /**
     * Get a FlussSourceBuilder to build a {@link FlussSource}.
     *
     * @return a Fluss source builder.
     */
```
created_at: 2025-04-27T13:25:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.split.SourceSplitBase;
+import com.alibaba.fluss.flink.source.state.SourceEnumeratorState;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.source.SplitEnumerator;
+import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Flink DataStream source implementation for reading data from Fluss tables.
+ *
+ * <p>This class extends the {@code FlinkSource} base class and implements {@code
+ * ResultTypeQueryable} to provide type information for Flink's type system.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> flussSource = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ *
+ * DataStreamSource<Order> stream = env.fromSource(
+ *     flussSource,
+ *     WatermarkStrategy.noWatermarks(),
+ *     "Fluss Source"
+ * );
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by this source
+ */
+public class FlussSource<OUT> extends FlinkSource<OUT> implements ResultTypeQueryable {
+    private static final long serialVersionUID = 1L;
+
+    private final FlussDeserializationSchema<OUT> deserializationSchema;
+    private final RowType sourceOutputType;
+    private final boolean streaming;
+
+    public FlussSource(
+            Configuration flussConf,
+            TablePath tablePath,
+            boolean hasPrimaryKey,
+            boolean isPartitioned,
+            RowType sourceOutputType,
+            @Nullable int[] projectedFields,
+            OffsetsInitializer offsetsInitializer,
+            long scanPartitionDiscoveryIntervalMs,
+            FlussDeserializationSchema<OUT> deserializationSchema,
+            boolean streaming) {
+        super(
+                flussConf,
+                tablePath,
+                hasPrimaryKey,
+                isPartitioned,
+                sourceOutputType,
+                projectedFields,
+                offsetsInitializer,
+                scanPartitionDiscoveryIntervalMs,
+                deserializationSchema,
+                streaming);
+        this.deserializationSchema = deserializationSchema;
+        this.sourceOutputType = sourceOutputType;
+        this.streaming = streaming;
+    }
+
+    @Override
+    public SplitEnumerator createEnumerator(SplitEnumeratorContext splitEnumeratorContext) {
+        return super.createEnumerator(splitEnumeratorContext);
+    }
+
+    @Override
+    public SplitEnumerator<SourceSplitBase, SourceEnumeratorState> restoreEnumerator(
+            SplitEnumeratorContext splitEnumeratorContext,
+            SourceEnumeratorState sourceEnumeratorState) {
+        return super.restoreEnumerator(splitEnumeratorContext, sourceEnumeratorState);
+    }
+
+    public static <T> FlussSourceBuilder<T> builder() {
+        return new FlussSourceBuilder<>();
+    }
+
+    @Override
+    public TypeInformation<OUT> getProducedType() {
+        return deserializationSchema.getProducedType(sourceOutputType);
+    }
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
user: login: wuchong
body: We can make `FlinkSource` also implement `ResultTypeQueryable` and move this to `FlinkSource`. This can also benefit for `FlinkSource`.
created_at: 2025-04-27T13:26:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
+        if (!isStreaming) {
+            this.streaming = false;
+        }
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setProjectedFields(int[] projectedFields) {
+        this.projectedFields = projectedFields;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setFlussConfig(Configuration flussConf) {
+        this.flussConf = flussConf;
+        return this;
+    }
+
+    public FlussSource<IN> build() {
+        checkNotNull(bootstrapServers, "BootstrapServers is required but not provided.");
+        checkNotNull(database, "Database is required but not provided.");
+        if (database.isEmpty()) {
+            throw new IllegalArgumentException("Database must not be empty.");
+        }
+        checkNotNull(tableName, "TableName is required but not provided.");
+        if (tableName.isEmpty()) {
+            throw new IllegalArgumentException("TableName must not be empty.");
+        }
+        checkNotNull(deserializationSchema, "Deserialization schema is required but not provided.");
+
+        checkNotNull(
+                scanPartitionDiscoveryIntervalMs,
+                "ScanPartitionDiscoveryIntervalMs is required but not provided.");
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: We can use the default value of  `com.alibaba.fluss.flink.FlinkConnectorOptions#SCAN_PARTITION_DISCOVERY_INTERVAL` for the default value of `scanPartitionDiscoveryIntervalMs`.
created_at: 2025-04-27T13:31:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
+        if (!isStreaming) {
+            this.streaming = false;
+        }
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setProjectedFields(int[] projectedFields) {
+        this.projectedFields = projectedFields;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setFlussConfig(Configuration flussConf) {
+        this.flussConf = flussConf;
+        return this;
+    }
+
+    public FlussSource<IN> build() {
+        checkNotNull(bootstrapServers, "BootstrapServers is required but not provided.");
+        checkNotNull(database, "Database is required but not provided.");
+        if (database.isEmpty()) {
+            throw new IllegalArgumentException("Database must not be empty.");
+        }
+        checkNotNull(tableName, "TableName is required but not provided.");
+        if (tableName.isEmpty()) {
+            throw new IllegalArgumentException("TableName must not be empty.");
+        }
+        checkNotNull(deserializationSchema, "Deserialization schema is required but not provided.");
+
+        checkNotNull(
+                scanPartitionDiscoveryIntervalMs,
+                "ScanPartitionDiscoveryIntervalMs is required but not provided.");
+        checkNotNull(offsetsInitializer, "OffsetsInitializer is required but not provided.");
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: `OffsetsInitializer.full()` as default like default option value `com.alibaba.fluss.flink.FlinkConnectorOptions#SCAN_STARTUP_MODE` ? Besdies, we need to rename `OffsetsInitializer#initial` to `full()` to keep align with config options. 
created_at: 2025-04-27T13:31:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: We don't need to call this as it will be called by parent class. 
created_at: 2025-04-27T13:36:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: we don't need this as it has been defined in parent class. 
created_at: 2025-04-27T13:36:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: use `ordersLogTablePath` instead to avoid duplicate path definition
created_at: 2025-04-27T13:38:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        Table table = conn.getTable(ordersLogTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
+
+        // Create a DataStream from the FlussSource
+        FlussSource<RowData> flussSource =
+                FlussSource.<RowData>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new RowDataDeserializationSchema())
+                        .build();
+
+        DataStreamSource<RowData> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        // these rows should be interpreted as Inserts
+        List<InternalRow> updatedRows =
+                Arrays.asList(row(600L, 20L, 600, "addr1"), row(700L, 22L, 601, "addr2"));
+
+        // send some row updates
+        writeRows(ordersLogTablePath, updatedRows, true);
+
+        List<RowData> expectedResult =
+                Arrays.asList(
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT),
+                        createRowData(800L, 23L, 602, "addr3", RowKind.INSERT),
+                        createRowData(900L, 24L, 603, "addr4", RowKind.INSERT),
+                        createRowData(1000L, 25L, 604, "addr5", RowKind.INSERT),
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT));
+
+        List<RowData> collectedRows = new ArrayList<>();
+        try (CloseableIterator<RowData> iterator = stream.collectAsync()) {
+            env.executeAsync("Test Fluss Log RowData Source");
+            int count = 0;
+            while (iterator.hasNext() && count < expectedResult.size() - 1) {
+                RowData row = iterator.next();
+                RowData genericRow =
+                        MockDataUtils.binaryRowToGenericRow(row, table.getTableInfo().getRowType());
+                collectedRows.add(genericRow);
+                count++;
+            }
+            RowData genericRow =
+                    MockDataUtils.binaryRowToGenericRow(
+                            iterator.next(), table.getTableInfo().getRowType());
+            // at this point the iterator should have one more element
+            collectedRows.add(genericRow);
+        }
+
+        // Assert result size and elements match
+        assertThat(expectedResult.size()).isEqualTo(collectedRows.size());
+        assertThat(expectedResult).isEqualTo(collectedRows);
+
+        assertThat(expectedResult).hasSameElementsAs(collectedRows);
+    }
+
+    @Test
+    public void testTableLogSourceWithProjectionPushdown() throws Exception {
+        //            FlinkTestBase.beforeAll();
+        List<OrderPartial> expectedOutput =
+                Arrays.asList(
+                        new OrderPartial(600, 600),
+                        new OrderPartial(700, 601),
+                        new OrderPartial(800, 602),
+                        new OrderPartial(900, 603),
+                        new OrderPartial(1000, 604));
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        // Create a DataStream from the FlussSource
+        FlussSource<OrderPartial> flussSource =
+                FlussSource.<OrderPartial>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new OrderPartialDeserializationSchema())
+                        .setProjectedFields(new int[] {0, 2})
+                        .build();
+
+        DataStreamSource<OrderPartial> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        List<OrderPartial> collectedElements = new ArrayList<>();
+        try (CloseableIterator<OrderPartial> data = stream.collectAsync()) {
+            env.executeAsync("Test Fluss Log Orders Source With Projection Pushdown");
+            int count = 0;
+            while (data.hasNext() && count < expectedOutput.size() - 1) {
+                collectedElements.add(data.next());
+                count++;
+            }
+            collectedElements.add(data.next());
+        }
+
+        // Assert result size and elements match
+        AssertionsForClassTypes.assertThat(expectedOutput.size())
+                .isEqualTo(collectedElements.size());
+        assertThat(expectedOutput).isEqualTo(collectedElements);
+
+        assertThat(expectedOutput).hasSameElementsAs(collectedElements);
+    }
+
+    private static RowData createRowData(
+            Long orderId, Long itemId, Integer amount, String address, RowKind rowKind) {
+        GenericRowData row = new GenericRowData(4);
+        row.setField(0, orderId);
+        row.setField(1, itemId);
+        row.setField(2, amount);
+        row.setField(3, StringData.fromString(address));
+
+        row.setRowKind(rowKind);
+        return row;
+    }
+
+    private void initTables() {
+        List<GenericRow> rows =
+                orders.stream().map(RowConverters::pojoToGenericRow).collect(Collectors.toList());
+
+        Table logTable = conn.getTable(ordersLogTablePath);
+
+        for (GenericRow row : rows) {
+            try {
+                logTable.newAppend().createWriter().append(row).get();
+            } catch (InterruptedException | ExecutionException e) {
+                throw new RuntimeException("Failed to insert test data", e);
+            }
+        }
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: ```suggestion
         Table logTable = conn.getTable(ordersLogTablePath);
        AppendWriter writer = logTable.newAppend().createWriter();
        for (GenericRow row : rows) {
            writer.append(row);
        }
        writer.flush();
```

for better performance.
created_at: 2025-04-27T13:40:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: remove
created_at: 2025-04-27T13:40:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with PK tables. */
+public class FlussSourcePKITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema pkSchema = MockDataUtils.getOrdersSchemaPK();
+
+    private static TableDescriptor pkTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String pkTableName = "orders_test_pk";
+
+    private static TablePath ordersPKTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        pkTableDescriptor =
+                TableDescriptor.builder().schema(pkSchema).distributedBy(1, "orderId").build();
+
+        TablePath pkTablePath = TablePath.of(DEFAULT_DB, pkTableName);
+
+        createTable(pkTablePath, pkTableDescriptor);
+
+        ordersPKTablePath = new TablePath(DEFAULT_DB, pkTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourcePKITCase.java
user: login: wuchong
body: remove, duplicated code
created_at: 2025-04-27T13:42:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: remove, duplicated code
created_at: 2025-04-27T13:42:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: can be defined as non-static variable
created_at: 2025-04-27T13:42:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: use `4` parallelism, which can reveal some concurrency problems. 
created_at: 2025-04-27T13:43:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: would be better to reuse the env across tests like how it does in `com.alibaba.fluss.flink.source.FlinkTableSourceITCase`.
created_at: 2025-04-27T13:44:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        Table table = conn.getTable(ordersLogTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: remove , unused.
created_at: 2025-04-27T13:44:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        Table table = conn.getTable(ordersLogTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
+
+        // Create a DataStream from the FlussSource
+        FlussSource<RowData> flussSource =
+                FlussSource.<RowData>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new RowDataDeserializationSchema())
+                        .build();
+
+        DataStreamSource<RowData> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        // these rows should be interpreted as Inserts
+        List<InternalRow> updatedRows =
+                Arrays.asList(row(600L, 20L, 600, "addr1"), row(700L, 22L, 601, "addr2"));
+
+        // send some row updates
+        writeRows(ordersLogTablePath, updatedRows, true);
+
+        List<RowData> expectedResult =
+                Arrays.asList(
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT),
+                        createRowData(800L, 23L, 602, "addr3", RowKind.INSERT),
+                        createRowData(900L, 24L, 603, "addr4", RowKind.INSERT),
+                        createRowData(1000L, 25L, 604, "addr5", RowKind.INSERT),
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT));
+
+        List<RowData> collectedRows = new ArrayList<>();
+        try (CloseableIterator<RowData> iterator = stream.collectAsync()) {
+            env.executeAsync("Test Fluss Log RowData Source");
+            int count = 0;
+            while (iterator.hasNext() && count < expectedResult.size() - 1) {
+                RowData row = iterator.next();
+                RowData genericRow =
+                        MockDataUtils.binaryRowToGenericRow(row, table.getTableInfo().getRowType());
+                collectedRows.add(genericRow);
+                count++;
+            }
+            RowData genericRow =
+                    MockDataUtils.binaryRowToGenericRow(
+                            iterator.next(), table.getTableInfo().getRowType());
+            // at this point the iterator should have one more element
+            collectedRows.add(genericRow);
+        }
+
+        // Assert result size and elements match
+        assertThat(expectedResult.size()).isEqualTo(collectedRows.size());
+        assertThat(expectedResult).isEqualTo(collectedRows);
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: remove these 2 assertions, as it has been covered by `hasSameElementsAs`, and `hasSameElementsAs` provides more fail information than the size mismatch error. 
created_at: 2025-04-27T13:49:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        Table table = conn.getTable(ordersLogTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
+
+        // Create a DataStream from the FlussSource
+        FlussSource<RowData> flussSource =
+                FlussSource.<RowData>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new RowDataDeserializationSchema())
+                        .build();
+
+        DataStreamSource<RowData> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        // these rows should be interpreted as Inserts
+        List<InternalRow> updatedRows =
+                Arrays.asList(row(600L, 20L, 600, "addr1"), row(700L, 22L, 601, "addr2"));
+
+        // send some row updates
+        writeRows(ordersLogTablePath, updatedRows, true);
+
+        List<RowData> expectedResult =
+                Arrays.asList(
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT),
+                        createRowData(800L, 23L, 602, "addr3", RowKind.INSERT),
+                        createRowData(900L, 24L, 603, "addr4", RowKind.INSERT),
+                        createRowData(1000L, 25L, 604, "addr5", RowKind.INSERT),
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT));
+
+        List<RowData> collectedRows = new ArrayList<>();
+        try (CloseableIterator<RowData> iterator = stream.collectAsync()) {
+            env.executeAsync("Test Fluss Log RowData Source");
+            int count = 0;
+            while (iterator.hasNext() && count < expectedResult.size() - 1) {
+                RowData row = iterator.next();
+                RowData genericRow =
+                        MockDataUtils.binaryRowToGenericRow(row, table.getTableInfo().getRowType());
+                collectedRows.add(genericRow);
+                count++;
+            }
+            RowData genericRow =
+                    MockDataUtils.binaryRowToGenericRow(
+                            iterator.next(), table.getTableInfo().getRowType());
+            // at this point the iterator should have one more element
+            collectedRows.add(genericRow);
+        }
+
+        // Assert result size and elements match
+        assertThat(expectedResult.size()).isEqualTo(collectedRows.size());
+        assertThat(expectedResult).isEqualTo(collectedRows);
+
+        assertThat(expectedResult).hasSameElementsAs(collectedRows);
+    }
+
+    @Test
+    public void testTableLogSourceWithProjectionPushdown() throws Exception {
+        //            FlinkTestBase.beforeAll();
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: remove
created_at: 2025-04-27T13:57:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        Table table = conn.getTable(ordersLogTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
+
+        // Create a DataStream from the FlussSource
+        FlussSource<RowData> flussSource =
+                FlussSource.<RowData>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new RowDataDeserializationSchema())
+                        .build();
+
+        DataStreamSource<RowData> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        // these rows should be interpreted as Inserts
+        List<InternalRow> updatedRows =
+                Arrays.asList(row(600L, 20L, 600, "addr1"), row(700L, 22L, 601, "addr2"));
+
+        // send some row updates
+        writeRows(ordersLogTablePath, updatedRows, true);
+
+        List<RowData> expectedResult =
+                Arrays.asList(
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT),
+                        createRowData(800L, 23L, 602, "addr3", RowKind.INSERT),
+                        createRowData(900L, 24L, 603, "addr4", RowKind.INSERT),
+                        createRowData(1000L, 25L, 604, "addr5", RowKind.INSERT),
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT));
+
+        List<RowData> collectedRows = new ArrayList<>();
+        try (CloseableIterator<RowData> iterator = stream.collectAsync()) {
+            env.executeAsync("Test Fluss Log RowData Source");
+            int count = 0;
+            while (iterator.hasNext() && count < expectedResult.size() - 1) {
+                RowData row = iterator.next();
+                RowData genericRow =
+                        MockDataUtils.binaryRowToGenericRow(row, table.getTableInfo().getRowType());
+                collectedRows.add(genericRow);
+                count++;
+            }
+            RowData genericRow =
+                    MockDataUtils.binaryRowToGenericRow(
+                            iterator.next(), table.getTableInfo().getRowType());
+            // at this point the iterator should have one more element
+            collectedRows.add(genericRow);
+        }
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: can simplify to 
```suggestion
List<RowData> rawRows = stream.executeAndCollect(expectedResult.size());
        List<RowData> collectedRows =
                rawRows.stream()
                        .map(row -> binaryRowToGenericRow(row, rowType))
                        .collect(Collectors.toList());
```
created_at: 2025-04-27T13:58:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with log tables. */
+public class FlussSourceLogITCase extends FlinkTestBase {
+    private static final List<Order> orders = MockDataUtils.ORDERS;
+
+    private static final Schema logSchema = MockDataUtils.getOrdersSchemaLog();
+
+    private static TableDescriptor logTableDescriptor;
+
+    private static String bootstrapServers;
+
+    private static String logTableName = "orders_test_log";
+
+    private static TablePath ordersLogTablePath;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        FlinkTestBase.beforeAll();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+
+        logTableDescriptor =
+                TableDescriptor.builder().schema(logSchema).distributedBy(1, "orderId").build();
+
+        TablePath logTablePath = TablePath.of(DEFAULT_DB, logTableName);
+
+        createTable(logTablePath, logTableDescriptor);
+
+        ordersLogTablePath = new TablePath(DEFAULT_DB, logTableName);
+
+        initTables();
+        bootstrapServers = conn.getConfiguration().get(ConfigOptions.BOOTSTRAP_SERVERS).get(0);
+    }
+
+    @AfterEach
+    protected void afterEach() throws Exception {
+        admin.dropTable(ordersLogTablePath, false);
+    }
+
+    @AfterAll
+    protected static void afterAll() throws Exception {
+        conn.close();
+    }
+
+    @Test
+    public void testRowDataLogTableSource() throws Exception {
+        //        FlinkTestBase.beforeAll();
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        Table table = conn.getTable(ordersLogTablePath);
+        RowType rowType = table.getTableInfo().getRowType();
+
+        // Create a DataStream from the FlussSource
+        FlussSource<RowData> flussSource =
+                FlussSource.<RowData>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new RowDataDeserializationSchema())
+                        .build();
+
+        DataStreamSource<RowData> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        // these rows should be interpreted as Inserts
+        List<InternalRow> updatedRows =
+                Arrays.asList(row(600L, 20L, 600, "addr1"), row(700L, 22L, 601, "addr2"));
+
+        // send some row updates
+        writeRows(ordersLogTablePath, updatedRows, true);
+
+        List<RowData> expectedResult =
+                Arrays.asList(
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT),
+                        createRowData(800L, 23L, 602, "addr3", RowKind.INSERT),
+                        createRowData(900L, 24L, 603, "addr4", RowKind.INSERT),
+                        createRowData(1000L, 25L, 604, "addr5", RowKind.INSERT),
+                        createRowData(600L, 20L, 600, "addr1", RowKind.INSERT),
+                        createRowData(700L, 22L, 601, "addr2", RowKind.INSERT));
+
+        List<RowData> collectedRows = new ArrayList<>();
+        try (CloseableIterator<RowData> iterator = stream.collectAsync()) {
+            env.executeAsync("Test Fluss Log RowData Source");
+            int count = 0;
+            while (iterator.hasNext() && count < expectedResult.size() - 1) {
+                RowData row = iterator.next();
+                RowData genericRow =
+                        MockDataUtils.binaryRowToGenericRow(row, table.getTableInfo().getRowType());
+                collectedRows.add(genericRow);
+                count++;
+            }
+            RowData genericRow =
+                    MockDataUtils.binaryRowToGenericRow(
+                            iterator.next(), table.getTableInfo().getRowType());
+            // at this point the iterator should have one more element
+            collectedRows.add(genericRow);
+        }
+
+        // Assert result size and elements match
+        assertThat(expectedResult.size()).isEqualTo(collectedRows.size());
+        assertThat(expectedResult).isEqualTo(collectedRows);
+
+        assertThat(expectedResult).hasSameElementsAs(collectedRows);
+    }
+
+    @Test
+    public void testTableLogSourceWithProjectionPushdown() throws Exception {
+        //            FlinkTestBase.beforeAll();
+        List<OrderPartial> expectedOutput =
+                Arrays.asList(
+                        new OrderPartial(600, 600),
+                        new OrderPartial(700, 601),
+                        new OrderPartial(800, 602),
+                        new OrderPartial(900, 603),
+                        new OrderPartial(1000, 604));
+
+        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+
+        // Create a DataStream from the FlussSource
+        FlussSource<OrderPartial> flussSource =
+                FlussSource.<OrderPartial>builder()
+                        .setBootstrapServers(bootstrapServers)
+                        .setDatabase(DEFAULT_DB)
+                        .setTable(logTableName)
+                        .setStartingOffsets(OffsetsInitializer.earliest())
+                        .setScanPartitionDiscoveryIntervalMs(1000L)
+                        .setDeserializationSchema(new OrderPartialDeserializationSchema())
+                        .setProjectedFields(new int[] {0, 2})
+                        .build();
+
+        DataStreamSource<OrderPartial> stream =
+                env.fromSource(flussSource, WatermarkStrategy.noWatermarks(), "Fluss Source");
+
+        List<OrderPartial> collectedElements = new ArrayList<>();
+        try (CloseableIterator<OrderPartial> data = stream.collectAsync()) {
+            env.executeAsync("Test Fluss Log Orders Source With Projection Pushdown");
+            int count = 0;
+            while (data.hasNext() && count < expectedOutput.size() - 1) {
+                collectedElements.add(data.next());
+                count++;
+            }
+            collectedElements.add(data.next());
+        }
+
+        // Assert result size and elements match
+        AssertionsForClassTypes.assertThat(expectedOutput.size())
+                .isEqualTo(collectedElements.size());
+        assertThat(expectedOutput).isEqualTo(collectedElements);
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourceLogITCase.java
user: login: wuchong
body: ditto
created_at: 2025-04-27T13:59:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.table.Table;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartial;
+import com.alibaba.fluss.flink.source.deserializer.OrderPartialDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.RowDataDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.flink.source.testutils.FlinkTestBase;
+import com.alibaba.fluss.flink.source.testutils.MockDataUtils;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.metadata.TableDescriptor;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.streaming.api.datastream.DataStreamSource;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.CloseableIterator;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.alibaba.fluss.testutils.DataTestUtils.row;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+/** Integration tests for the {@link FlussSource} class with PK tables. */
+public class FlussSourcePKITCase extends FlinkTestBase {
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/FlussSourcePKITCase.java
user: login: wuchong
body: We don't need a separate ITCase class for testing PK table. Please merge it with `FlussSourceLogITCase` into `FlussSourceITCase`. This can help us reduce testing time. 

The `BeforeEach` method can be extracted as a private method and be called in the beginning of testing method. 
created_at: 2025-04-27T14:03:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source.testutils;
+
+import com.alibaba.fluss.flink.row.RowConverters;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.deserializer.Order;
+import com.alibaba.fluss.metadata.Schema;
+import com.alibaba.fluss.record.LogRecord;
+import com.alibaba.fluss.row.InternalRow;
+import com.alibaba.fluss.types.DataTypes;
+import com.alibaba.fluss.types.RowType;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.LogicalType;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility class for generating mock data and test infrastructure for Fluss-Flink integration tests.
+ *
+ * <p>The mock data and utilities in this class are used across various Fluss source connector tests
+ * to verify functionality like schema handling, record deserialization, and data type conversion.
+ */
+public class MockDataUtils {
+    public static final List<Order> ORDERS =
+            Arrays.asList(
+                    new Order(600, 20, 600, "addr1"),
+                    new Order(700, 22, 601, "addr2"),
+                    new Order(800, 23, 602, "addr3"),
+                    new Order(900, 24, 603, "addr4"),
+                    new Order(1000, 25, 604, "addr5"));
+
+    public static Schema getOrdersSchemaPK() {
+        return Schema.newBuilder()
+                .column("orderId", DataTypes.BIGINT())
+                .column("itemId", DataTypes.BIGINT())
+                .column("amount", DataTypes.INT())
+                .column("address", DataTypes.STRING())
+                .primaryKey("orderId")
+                .build();
+    }
+
+    public static Schema getOrdersSchemaLog() {
+        return Schema.newBuilder()
+                .column("orderId", DataTypes.BIGINT())
+                .column("itemId", DataTypes.BIGINT())
+                .column("amount", DataTypes.INT())
+                .column("address", DataTypes.STRING())
+                .build();
+    }
+
+    /**
+     * Utility method to create a readable copy of a BinaryRowData. Creates a GenericRowData with
+     * the same content as the BinaryRowData.
+     *
+     * @param binaryRow The BinaryRowData to copy
+     * @param flussRowType The RowType describing the row structure
+     * @return A GenericRowData with the same content
+     */
+    public static RowData binaryRowToGenericRow(RowData binaryRow, RowType flussRowType) {
+        // Convert Fluss RowType to Flink RowType
+        org.apache.flink.table.types.logical.RowType flinkRowType =
+                RowConverters.flussRowTypeToFlinkRowType(flussRowType);
path: fluss-flink/fluss-flink-common/src/test/java/com/alibaba/fluss/flink/source/testutils/MockDataUtils.java
user: login: wuchong
body: use `com.alibaba.fluss.flink.utils.FlinkConversions.toFlinkRowType` instead, so that we can remove `RowConverters`.
created_at: 2025-04-27T14:18:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.row;
+
+import com.alibaba.fluss.row.BinaryString;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.TimestampNtz;
+
+import org.apache.flink.table.types.logical.LogicalType;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.sql.Date;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility class for converting between different row representations in Fluss.
+ *
+ * <p>It's primarily used for testing and data conversion operations when interacting with the Fluss
+ * storage system.
+ */
+public class RowConverters {
+    /**
+     * Converts a POJO to a Fluss GenericRow. Special handling is provided for timestamp types.
+     *
+     * @param pojo The POJO to convert
+     * @return A GenericRow containing the POJO's field values
+     * @throws IllegalArgumentException if there's an error accessing the fields
+     */
+    public static GenericRow pojoToGenericRow(Object pojo) {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/row/RowConverters.java
user: login: wuchong
body: I just merged https://github.com/alibaba/fluss/issues/723 which already provides this util. We can remove this method. 
created_at: 2025-04-27T14:18:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.row;
+
+import com.alibaba.fluss.row.BinaryString;
+import com.alibaba.fluss.row.GenericRow;
+import com.alibaba.fluss.row.TimestampNtz;
+
+import org.apache.flink.table.types.logical.LogicalType;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.sql.Date;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility class for converting between different row representations in Fluss.
+ *
+ * <p>It's primarily used for testing and data conversion operations when interacting with the Fluss
+ * storage system.
+ */
+public class RowConverters {
+    /**
+     * Converts a POJO to a Fluss GenericRow. Special handling is provided for timestamp types.
+     *
+     * @param pojo The POJO to convert
+     * @return A GenericRow containing the POJO's field values
+     * @throws IllegalArgumentException if there's an error accessing the fields
+     */
+    public static GenericRow pojoToGenericRow(Object pojo) {
+        if (pojo == null) {
+            throw new IllegalArgumentException("Input POJO cannot be null");
+        }
+
+        Field[] allFields = pojo.getClass().getDeclaredFields();
+
+        // Filter out synthetic fields (JaCoCo's $jacocoData)
+        Field[] fields =
+                Arrays.stream(allFields)
+                        .filter(
+                                field ->
+                                        !field.isSynthetic()
+                                                && !Modifier.isStatic(field.getModifiers()))
+                        .toArray(Field[]::new);
+
+        Object[] values = new Object[fields.length];
+
+        for (int i = 0; i < fields.length; i++) {
+            Field field = fields[i];
+            field.setAccessible(true);
+            try {
+                Object value = field.get(pojo);
+
+                // Handle various field types
+                if (value instanceof String) {
+                    values[i] = BinaryString.fromString((String) value);
+                } else if (value instanceof LocalDateTime) {
+                    values[i] = TimestampNtz.fromLocalDateTime((LocalDateTime) value);
+                } else if (value instanceof Date) {
+                    values[i] = TimestampNtz.fromMillis(((Date) value).getTime());
+                } else if (value instanceof Instant) {
+                    LocalDateTime ldt =
+                            LocalDateTime.ofInstant((Instant) value, ZoneId.systemDefault());
+                    values[i] = TimestampNtz.fromLocalDateTime(ldt);
+                } else {
+                    values[i] = value;
+                }
+            } catch (IllegalAccessException e) {
+                throw new IllegalArgumentException("Unable to access field: " + field.getName(), e);
+            }
+        }
+
+        return row(values);
+    }
+
+    public static GenericRow row(Object... objects) {
+        GenericRow row = new GenericRow(objects.length);
+        for (int i = 0; i < objects.length; i++) {
+            if (objects[i] instanceof String) {
+                row.setField(i, BinaryString.fromString((String) objects[i]));
+            } else {
+                row.setField(i, objects[i]);
+            }
+        }
+        return row;
+    }
+
+    /**
+     * Converts a Fluss RowType to a Flink RowType.
+     *
+     * @param flussRowType The Fluss RowType to convert
+     * @return The equivalent Flink RowType
+     */
+    public static org.apache.flink.table.types.logical.RowType flussRowTypeToFlinkRowType(
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/row/RowConverters.java
user: login: wuchong
body: `com.alibaba.fluss.flink.utils.FlinkConversions.toFlinkRowType` already covers this.
created_at: 2025-04-27T14:19:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
+        if (!isStreaming) {
+            this.streaming = false;
+        }
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setProjectedFields(int[] projectedFields) {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: polyzos
body: @wuchong currently changing to fieldNames might break compatibility with the `FlinkTableSource` as it uses the same `FlinkSource` constructor, but it uses the indexes.. maybe leave it as is and introduce a separate ticket/PR to address this properly so its easier to track later?
created_at: 2025-04-28T10:06:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
+        if (!isStreaming) {
+            this.streaming = false;
+        }
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setProjectedFields(int[] projectedFields) {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: polyzos
body: @wuchong i created [this](https://github.com/alibaba/fluss/issues/569?issue=alibaba%7Cfluss%7C804) for tracking.
created_at: 2025-04-28T13:14:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,7 +118,7 @@ public SplitEnumerator<SourceSplitBase, SourceEnumeratorState> restoreEnumerator
                 sourceEnumeratorState.getAssignedPartitions(),
                 offsetsInitializer,
                 scanPartitionDiscoveryIntervalMs,
-                streaming);
+                true);
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlinkSource.java
user: login: wuchong
body: Why hard-cord this to true? 
created_at: 2025-04-29T08:02:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.annotation.VisibleForTesting;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Flink DataStream source implementation for reading data from Fluss tables.
+ *
+ * <p>This class extends the {@code FlinkSource} base class and implements {@code
+ * ResultTypeQueryable} to provide type information for Flink's type system.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> flussSource = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ *
+ * DataStreamSource<Order> stream = env.fromSource(
+ *     flussSource,
+ *     WatermarkStrategy.noWatermarks(),
+ *     "Fluss Source"
+ * );
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by this source
+ */
+public class FlussSource<OUT> extends FlinkSource<OUT> {
+    private static final long serialVersionUID = 1L;
+
+    public FlussSource(
+            Configuration flussConf,
+            TablePath tablePath,
+            boolean hasPrimaryKey,
+            boolean isPartitioned,
+            RowType sourceOutputType,
+            @Nullable int[] projectedFields,
+            OffsetsInitializer offsetsInitializer,
+            long scanPartitionDiscoveryIntervalMs,
+            FlussDeserializationSchema<OUT> deserializationSchema,
+            boolean streaming) {
+        super(
+                flussConf,
+                tablePath,
+                hasPrimaryKey,
+                isPartitioned,
+                sourceOutputType,
+                projectedFields,
+                offsetsInitializer,
+                scanPartitionDiscoveryIntervalMs,
+                deserializationSchema,
+                streaming);
+    }
+
+    /**
+     * Get a FlussSourceBuilder to build a {@link FlussSource}.
+     *
+     * @return a Fluss source builder.
+     */
+    public static <T> FlussSourceBuilder<T> builder() {
+        return new FlussSourceBuilder<>();
+    }
+
+    @VisibleForTesting
+    public OffsetsInitializer getOffsetsInitializer() {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
user: login: wuchong
body: can be package visible
created_at: 2025-04-29T08:03:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.annotation.VisibleForTesting;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Flink DataStream source implementation for reading data from Fluss tables.
+ *
+ * <p>This class extends the {@code FlinkSource} base class and implements {@code
+ * ResultTypeQueryable} to provide type information for Flink's type system.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> flussSource = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ *
+ * DataStreamSource<Order> stream = env.fromSource(
+ *     flussSource,
+ *     WatermarkStrategy.noWatermarks(),
+ *     "Fluss Source"
+ * );
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by this source
+ */
+public class FlussSource<OUT> extends FlinkSource<OUT> {
+    private static final long serialVersionUID = 1L;
+
+    public FlussSource(
+            Configuration flussConf,
+            TablePath tablePath,
+            boolean hasPrimaryKey,
+            boolean isPartitioned,
+            RowType sourceOutputType,
+            @Nullable int[] projectedFields,
+            OffsetsInitializer offsetsInitializer,
+            long scanPartitionDiscoveryIntervalMs,
+            FlussDeserializationSchema<OUT> deserializationSchema,
+            boolean streaming) {
+        super(
+                flussConf,
+                tablePath,
+                hasPrimaryKey,
+                isPartitioned,
+                sourceOutputType,
+                projectedFields,
+                offsetsInitializer,
+                scanPartitionDiscoveryIntervalMs,
+                deserializationSchema,
+                streaming);
+    }
+
+    /**
+     * Get a FlussSourceBuilder to build a {@link FlussSource}.
+     *
+     * @return a Fluss source builder.
+     */
+    public static <T> FlussSourceBuilder<T> builder() {
+        return new FlussSourceBuilder<>();
+    }
+
+    @VisibleForTesting
+    public OffsetsInitializer getOffsetsInitializer() {
+        return offsetsInitializer;
+    }
+
+    @VisibleForTesting
+    public long getScanPartitionDiscoveryIntervalMs() {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
user: login: wuchong
body: never used, can remove
created_at: 2025-04-29T08:03:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.ConfigOptions;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<OUT> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<OUT> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true; // currently only streaming is supported
+
+    public FlussSourceBuilder<OUT> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setDeserializationSchema(
+            FlussDeserializationSchema<OUT> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<OUT> setIsStreaming(boolean isStreaming) {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: let's remove it as we don't support it yet. 
created_at: 2025-04-29T08:06:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.annotation.VisibleForTesting;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Flink DataStream source implementation for reading data from Fluss tables.
+ *
+ * <p>This class extends the {@code FlinkSource} base class and implements {@code
+ * ResultTypeQueryable} to provide type information for Flink's type system.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> flussSource = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ *
+ * DataStreamSource<Order> stream = env.fromSource(
+ *     flussSource,
+ *     WatermarkStrategy.noWatermarks(),
+ *     "Fluss Source"
+ * );
+ * }</pre>
+ *
+ * @param <OUT> The type of records produced by this source
+ */
+public class FlussSource<OUT> extends FlinkSource<OUT> {
+    private static final long serialVersionUID = 1L;
+
+    public FlussSource(
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSource.java
user: login: wuchong
body: Change to package visibility to avoid users direct use this constructor. As this constructor is evolving frequently. 
created_at: 2025-04-29T12:55:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2025 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.fluss.flink.source;
+
+import com.alibaba.fluss.client.Connection;
+import com.alibaba.fluss.client.ConnectionFactory;
+import com.alibaba.fluss.client.admin.Admin;
+import com.alibaba.fluss.config.Configuration;
+import com.alibaba.fluss.flink.source.deserializer.FlussDeserializationSchema;
+import com.alibaba.fluss.flink.source.enumerator.initializer.OffsetsInitializer;
+import com.alibaba.fluss.metadata.TableInfo;
+import com.alibaba.fluss.metadata.TablePath;
+import com.alibaba.fluss.types.RowType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/**
+ * Builder class for creating {@link FlussSource} instances.
+ *
+ * <p>The builder llows for step-by-step configuration of a Fluss source connector. It handles the
+ * setup of connection parameters, table metadata retrieval, and source configuration.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>{@code
+ * FlussSource<Order> source = FlussSource.<Order>builder()
+ *     .setBootstrapServers("localhost:9092")
+ *     .setDatabase("mydb")
+ *     .setTable("orders")
+ *     .setScanPartitionDiscoveryIntervalMs(1000L)
+ *     .setStartingOffsets(OffsetsInitializer.earliest())
+ *     .setDeserializationSchema(new OrderDeserializationSchema())
+ *     .build();
+ * }</pre>
+ *
+ * @param <IN> The type of records produced by the source being built
+ */
+public class FlussSourceBuilder<IN> {
+    private static final Logger LOG = LoggerFactory.getLogger(FlussSourceBuilder.class);
+
+    private Configuration flussConf;
+    private TablePath tablePath;
+    private boolean hasPrimaryKey;
+    private boolean isPartitioned;
+    private RowType sourceOutputType;
+    private int[] projectedFields;
+    private Long scanPartitionDiscoveryIntervalMs;
+    private OffsetsInitializer offsetsInitializer;
+    private FlussDeserializationSchema<IN> deserializationSchema;
+
+    private String bootstrapServers;
+
+    private String database;
+    private String tableName;
+    private boolean streaming = true;
+
+    public FlussSourceBuilder<IN> setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDatabase(String database) {
+        this.database = database;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setTable(String table) {
+        this.tableName = table;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setScanPartitionDiscoveryIntervalMs(
+            long scanPartitionDiscoveryIntervalMs) {
+        this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setStartingOffsets(OffsetsInitializer offsetsInitializer) {
+        this.offsetsInitializer = offsetsInitializer;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setDeserializationSchema(
+            FlussDeserializationSchema<IN> deserializationSchema) {
+        this.deserializationSchema = deserializationSchema;
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setIsStreaming(boolean isStreaming) {
+        if (!isStreaming) {
+            this.streaming = false;
+        }
+        return this;
+    }
+
+    public FlussSourceBuilder<IN> setProjectedFields(int[] projectedFields) {
path: fluss-flink/fluss-flink-common/src/main/java/com/alibaba/fluss/flink/source/FlussSourceBuilder.java
user: login: wuchong
body: I mean we only expose fieldNames projection in the `FlussSourceBuilder`, but still use field index projection in the underlying `FlinkSource`. We can do the mapping from fieldNames to field index in the `build()` method, as we have got the schema in it. 

We can implement it in a follow-up PR. 
created_at: 2025-04-29T12:58:22Z
author_association: MEMBER

PR comments:
user: login: polyzos
created_at: 2025-04-02T09:10:38Z
body: @wuchong  [RowConverters](https://github.com/alibaba/fluss/pull/710/files#diff-ecd3113126114d33c87a5af74a2f0e3bd6fc00c650f6ca628bf1bb88668f9e2b) are used for testing now and can be improved and exposed as helper functions to users in another PR.
But let's discuss and keep track of the required improvements and overall thoughts.
author_association: CONTRIBUTOR
user: login: polyzos
created_at: 2025-04-28T11:43:03Z
body: @wuchong thank you for your time and detailed feedback. PTAL as all comments should have been addressed.. let me know if I missed anything 
author_association: CONTRIBUTOR