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

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


The following commit(s) were added to refs/heads/main by this push:
     new 9092d1b  feat: introduce table scan, split and plan (#99)
9092d1b is described below

commit 9092d1b60061e9526cef4becb59d21aa263b3163
Author: lszskye <[email protected]>
AuthorDate: Mon Jun 22 02:32:18 2026 -0700

    feat: introduce table scan, split and plan (#99)
---
 include/paimon/table/source/data_split.h           |  84 +++++
 include/paimon/table/source/plan.h                 |  38 +++
 include/paimon/table/source/split.h                |  69 ++++
 include/paimon/table/source/startup_mode.h         |  71 +++++
 include/paimon/table/source/table_read.h           |  79 +++++
 include/paimon/table/source/table_scan.h           |  48 +++
 src/paimon/core/table/source/abstract_table_scan.h | 125 ++++++++
 src/paimon/core/table/source/data_split_impl.cpp   | 167 ++++++++++
 src/paimon/core/table/source/data_split_impl.h     | 195 ++++++++++++
 src/paimon/core/table/source/deletion_file.h       | 152 +++++++++
 .../core/table/source/deletion_file_test.cpp       |  64 ++++
 src/paimon/core/table/source/plan_impl.cpp         |  30 ++
 src/paimon/core/table/source/plan_impl.h           |  52 +++
 src/paimon/core/table/source/scan_mode.h           |  35 ++
 src/paimon/core/table/source/split.cpp             | 241 ++++++++++++++
 src/paimon/core/table/source/split_generator.h     |  76 +++++
 .../core/table/source/split_generator_test.cpp     | 351 +++++++++++++++++++++
 src/paimon/core/table/source/startup_mode.cpp      |  81 +++++
 src/paimon/core/table/source/startup_mode_test.cpp |  44 +++
 src/paimon/core/table/source/table_scan.cpp        | 282 +++++++++++++++++
 src/paimon/core/table/source/table_scan_test.cpp   |  63 ++++
 21 files changed, 2347 insertions(+)

diff --git a/include/paimon/table/source/data_split.h 
b/include/paimon/table/source/data_split.h
new file mode 100644
index 0000000..865e785
--- /dev/null
+++ b/include/paimon/table/source/data_split.h
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "paimon/data/timestamp.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/table/source/split.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class MemoryPool;
+
+/// Input data split for reading operation. Needed by most batch computation 
engines.
+class PAIMON_EXPORT DataSplit : public Split {
+ public:
+    /// Metadata structure for simple data files.
+    ///
+    /// Contains essential information about a data file including its 
location,
+    /// size, row count, sequence numbers, schema information, and timestamps.
+    /// This structure is used to track file metadata without loading the 
actual file content.
+    struct SimpleDataFileMeta {
+        SimpleDataFileMeta(const std::string& _file_path, int64_t _file_size, 
int64_t _row_count,
+                           int64_t _min_sequence_number, int64_t 
_max_sequence_number,
+                           int64_t _schema_id, int32_t _level, const 
Timestamp& _creation_time,
+                           const std::optional<int64_t>& _delete_row_count)
+            : file_path(_file_path),
+              file_size(_file_size),
+              row_count(_row_count),
+              min_sequence_number(_min_sequence_number),
+              max_sequence_number(_max_sequence_number),
+              schema_id(_schema_id),
+              level(_level),
+              creation_time(_creation_time),
+              delete_row_count(_delete_row_count) {}
+
+        /// Absolute path of the data file.
+        ///
+        /// If external path is enabled, `file_path` indicates the actual 
location in the external
+        /// storage system.
+        std::string file_path;
+        int64_t file_size;
+        int64_t row_count;
+        int64_t min_sequence_number;
+        int64_t max_sequence_number;
+        int64_t schema_id;
+        int32_t level;
+        Timestamp creation_time;
+        std::optional<int64_t> delete_row_count;
+
+        bool operator==(const SimpleDataFileMeta& other) const;
+
+        std::string ToString() const;
+    };
+
+    /// Get the list of metadata for all data files in this split.
+    /// @note This method will be removed in future versions and is only used 
for append tables.
+    virtual std::vector<SimpleDataFileMeta> GetFileList() const = 0;
+};
+}  // namespace paimon
diff --git a/include/paimon/table/source/plan.h 
b/include/paimon/table/source/plan.h
new file mode 100644
index 0000000..1c71743
--- /dev/null
+++ b/include/paimon/table/source/plan.h
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <vector>
+
+#include "paimon/table/source/split.h"
+
+namespace paimon {
+/// %Result plan of this `TableScan`.
+class PAIMON_EXPORT Plan {
+ public:
+    virtual ~Plan() = default;
+    /// %Result splits.
+    virtual const std::vector<std::shared_ptr<Split>>& Splits() const = 0;
+    /// Snapshot id of this plan, return `std::nullopt` if the table is empty.
+    virtual std::optional<int64_t> SnapshotId() const = 0;
+};
+}  // namespace paimon
diff --git a/include/paimon/table/source/split.h 
b/include/paimon/table/source/split.h
new file mode 100644
index 0000000..a968f02
--- /dev/null
+++ b/include/paimon/table/source/split.h
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class MemoryPool;
+
+/// An input split for reading operation. Needed by most batch computation 
engines. Support
+/// Serialize and Deserialize, compatible with java version.
+/// This split can be either a `DataSplit` (for direct data file reads) or an 
`IndexedSplit`
+/// (for reads leveraging global indexes).
+class PAIMON_EXPORT Split {
+ public:
+    virtual ~Split() = default;
+
+    /// Deserialize a `Split` from a binary buffer.
+    ///
+    /// Creates a `Split` instance from its serialized binary representation.
+    /// This is typically used in distributed computing scenarios where splits
+    /// are transmitted between different nodes or processes.
+    ///
+    /// @param buffer Const pointer to the binary data containing the 
serialized `Split`.
+    /// @param length Size of the buffer in bytes.
+    /// @param pool Memory pool for allocating objects during deserialization.
+    /// @return Result containing the deserialized `Split` or an error status.
+    static Result<std::shared_ptr<Split>> Deserialize(const char* buffer, 
size_t length,
+                                                      const 
std::shared_ptr<MemoryPool>& pool);
+
+    /// Serialize a `Split` to a binary string.
+    ///
+    /// Converts a `Split` instance to its binary representation for storage
+    /// or transmission. The serialized data can later be deserialized using
+    /// the Deserialize method.
+    ///
+    /// @param split The `Split` instance to serialize.
+    /// @param pool Memory pool for allocating temporary objects during 
serialization.
+    /// @return Result containing the serialized binary data as a string or an 
error status.
+    static Result<std::string> Serialize(const std::shared_ptr<Split>& split,
+                                         const std::shared_ptr<MemoryPool>& 
pool);
+};
+}  // namespace paimon
diff --git a/include/paimon/table/source/startup_mode.h 
b/include/paimon/table/source/startup_mode.h
new file mode 100644
index 0000000..e43c243
--- /dev/null
+++ b/include/paimon/table/source/startup_mode.h
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+#include <string>
+
+#include "paimon/result.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+/// Specifies the startup mode for log consumer.
+class PAIMON_EXPORT StartupMode {
+ public:
+    /// Determines actual startup mode according to other table properties. If 
"scan.snapshot-id" is
+    /// set the actual startup mode will be "from-snapshot", otherwise the 
actual startup mode will
+    /// be "latest-full".
+    static const StartupMode Default();
+
+    /// For streaming sources, produces the latest snapshot on the table upon 
first startup, and
+    /// continue to read the latest changes. For batch sources, just produce 
the latest snapshot but
+    /// does not read new changes.
+    static const StartupMode LatestFull();
+
+    /// For streaming sources, continuously reads latest changes without 
producing a snapshot at the
+    /// beginning. For batch sources, behaves the same as the "latest-full" 
startup mode.
+    static const StartupMode Latest();
+
+    /// For streaming sources, continuously reads changes starting from 
snapshot specified by
+    /// "scan.snapshot-id", without producing a snapshot at the beginning. For 
batch sources,
+    /// produces a snapshot specified by "scan.snapshot-id" but does not read 
new changes.
+    static const StartupMode FromSnapshot();
+
+    /// For streaming sources, produces from snapshot specified by 
"scan.snapshot-id" on the table
+    /// upon first startup, and continuously reads changes. For batch sources, 
produces a snapshot
+    /// specified by "scan.snapshot-id" but does not read new changes
+    static const StartupMode FromSnapshotFull();
+
+    /// Starts from a timestamp specified by either "scan.timestamp-millis" or
+    /// "scan.timestamp". For batch sources, produces the latest snapshot whose
+    /// timestamp is <= the specified timestamp. For streaming sources, 
continuously
+    /// reads changes starting from the first snapshot at or after the 
timestamp.
+    static const StartupMode FromTimestamp();
+
+ public:
+    std::string ToString() const;
+    bool operator==(const StartupMode& other) const;
+    static Result<StartupMode> FromString(const std::string& str);
+
+ private:
+    explicit StartupMode(const std::string& value) : value_(value) {}
+
+ private:
+    std::string value_;
+};
+}  // namespace paimon
diff --git a/include/paimon/table/source/table_read.h 
b/include/paimon/table/source/table_read.h
new file mode 100644
index 0000000..157561f
--- /dev/null
+++ b/include/paimon/table/source/table_read.h
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "paimon/executor.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/read_context.h"
+#include "paimon/reader/batch_reader.h"
+#include "paimon/result.h"
+#include "paimon/table/source/split.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class MemoryPool;
+class ReadContext;
+
+/// Given a `Split` or a list of `Split`, generate a reader for batch reading.
+class PAIMON_EXPORT TableRead {
+ public:
+    virtual ~TableRead() = default;
+
+    /// Create an instance of `TableRead`.
+    ///
+    /// @param context A unique pointer to the `ReadContext` used for read 
operations.
+    /// @return A Result containing a unique pointer to the `TableRead` 
instance.
+    static Result<std::unique_ptr<TableRead>> 
Create(std::unique_ptr<ReadContext> context);
+
+    /// Creates a `BatchReader` instance for reading data.
+    ///
+    /// This method creates a BatchReader that will be responsible for reading 
data from the
+    /// provided splits.
+    ///
+    /// @param splits A vector of shared pointers to `Split` instances 
representing the
+    ///                    data to be read.
+    /// @return A Result containing a unique pointer to the `BatchReader` 
instance.
+    /// @note `BatchReader`s created by the same `TableRead` are not 
thread-safe for
+    /// concurrent reading.
+    virtual Result<std::unique_ptr<BatchReader>> CreateReader(
+        const std::vector<std::shared_ptr<Split>>& splits);
+
+    /// Creates a `BatchReader` instance for a single split.
+    ///
+    /// @param split A shared pointer to the `Split` instance that defines the 
data to be
+    ///                   read.
+    /// @return A Result containing a unique pointer to the `BatchReader` 
instance.
+    virtual Result<std::unique_ptr<BatchReader>> CreateReader(
+        const std::shared_ptr<Split>& split) = 0;
+
+ protected:
+    explicit TableRead(const std::shared_ptr<MemoryPool>& memory_pool);
+
+    std::shared_ptr<MemoryPool> GetMemoryPool() const {
+        return pool_;
+    }
+
+ private:
+    std::shared_ptr<MemoryPool> pool_;
+};
+}  // namespace paimon
diff --git a/include/paimon/table/source/table_scan.h 
b/include/paimon/table/source/table_scan.h
new file mode 100644
index 0000000..c9b4291
--- /dev/null
+++ b/include/paimon/table/source/table_scan.h
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include "paimon/result.h"
+#include "paimon/table/source/plan.h"
+#include "paimon/type_fwd.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class ScanContext;
+
+/// A scanner interface for reading table's meta and create a plan.
+class PAIMON_EXPORT TableScan {
+ public:
+    /// Create an instance of `TableScan`.
+    ///
+    /// @param context A unique pointer to the `ScanContext` used for scan 
operations.
+    /// @return A Result containing a unique pointer to the `TableScan` 
instance.
+    static Result<std::unique_ptr<TableScan>> 
Create(std::unique_ptr<ScanContext> context);
+
+    virtual ~TableScan() = default;
+
+    /// Create a scan plan.
+    ///
+    /// @return A Result containing a shared pointer to the created `Plan` or 
an error status.
+    virtual Result<std::shared_ptr<Plan>> CreatePlan() = 0;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/abstract_table_scan.h 
b/src/paimon/core/table/source/abstract_table_scan.h
new file mode 100644
index 0000000..363fe0d
--- /dev/null
+++ b/src/paimon/core/table/source/abstract_table_scan.h
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include "paimon/core/core_options.h"
+#include 
"paimon/core/table/source/snapshot/continuous_from_snapshot_full_starting_scanner.h"
+#include 
"paimon/core/table/source/snapshot/continuous_from_snapshot_starting_scanner.h"
+#include 
"paimon/core/table/source/snapshot/continuous_latest_starting_scanner.h"
+#include "paimon/core/table/source/snapshot/full_starting_scanner.h"
+#include "paimon/core/table/source/snapshot/snapshot_reader.h"
+#include 
"paimon/core/table/source/snapshot/static_from_snapshot_starting_scanner.h"
+#include "paimon/core/table/source/snapshot/static_from_tag_starting_scanner.h"
+#include "paimon/table/source/startup_mode.h"
+#include "paimon/table/source/table_scan.h"
+namespace paimon {
+/// An abstraction layer above `FileStoreScan` to provide input split 
generation.
+class AbstractTableScan : public TableScan {
+ public:
+    AbstractTableScan(const CoreOptions& core_options,
+                      const std::shared_ptr<SnapshotReader>& snapshot_reader)
+        : core_options_(core_options), snapshot_reader_(snapshot_reader) {}
+
+ protected:
+    Result<std::shared_ptr<StartingScanner>> CreateStartingScanner(bool 
is_streaming) const {
+        const auto& snapshot_manager = snapshot_reader_->GetSnapshotManager();
+        auto startup_mode = core_options_.GetStartupMode();
+        std::optional<int64_t> specified_snapshot_id = 
core_options_.GetScanSnapshotId();
+        if (startup_mode == StartupMode::LatestFull()) {
+            return std::make_shared<FullStartingScanner>(snapshot_manager);
+        } else if (startup_mode == StartupMode::Latest()) {
+            if (is_streaming) {
+                PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<StartingScanner> 
starting_scanner,
+                                       
ContinuousLatestStartingScanner::Create(snapshot_manager));
+                return starting_scanner;
+            } else {
+                return std::shared_ptr<StartingScanner>(new 
FullStartingScanner(snapshot_manager));
+            }
+        } else if (startup_mode == StartupMode::FromSnapshot()) {
+            const std::optional<std::string> scan_tag_name = 
core_options_.GetScanTagName();
+            if (specified_snapshot_id != std::nullopt) {
+                return is_streaming
+                           ? std::shared_ptr<StartingScanner>(
+                                 new ContinuousFromSnapshotStartingScanner(
+                                     snapshot_manager, 
specified_snapshot_id.value()))
+                           : std::shared_ptr<StartingScanner>(new 
StaticFromSnapshotStartingScanner(
+                                 snapshot_manager, 
specified_snapshot_id.value()));
+            } else if (scan_tag_name != std::nullopt) {
+                if (is_streaming) {
+                    return Status::Invalid("Cannot scan from tag in streaming 
mode");
+                }
+                return 
std::make_shared<StaticFromTagStartingScanner>(snapshot_manager,
+                                                                      
scan_tag_name.value());
+            } else {
+                return Status::Invalid(
+                    "scan.snapshot-id or scan.tag-name must be set when 
startup mode is "
+                    "FROM_SNAPSHOT");
+            }
+        } else if (startup_mode == StartupMode::FromSnapshotFull()) {
+            if (specified_snapshot_id != std::nullopt) {
+                return is_streaming
+                           ? std::shared_ptr<StartingScanner>(
+                                 new ContinuousFromSnapshotFullStartingScanner(
+                                     snapshot_manager, 
specified_snapshot_id.value()))
+                           : std::shared_ptr<StartingScanner>(new 
StaticFromSnapshotStartingScanner(
+                                 snapshot_manager, 
specified_snapshot_id.value()));
+            } else {
+                return Status::Invalid(
+                    "scan.snapshot-id must be set when startup mode is 
FROM_SNAPSHOT_FULL");
+            }
+        } else if (startup_mode == StartupMode::FromTimestamp()) {
+            std::optional<int64_t> timestamp_millis = 
core_options_.GetScanTimestampMillis();
+            if (timestamp_millis == std::nullopt) {
+                return Status::Invalid(
+                    "scan.timestamp-millis or scan.timestamp must be set when 
startup mode is "
+                    "FROM_TIMESTAMP");
+            }
+            if (is_streaming) {
+                PAIMON_ASSIGN_OR_RAISE(
+                    std::optional<Snapshot> earlier_snapshot,
+                    
snapshot_manager->EarlierThanTimeMillis(timestamp_millis.value()));
+                int64_t start_id =
+                    earlier_snapshot ? earlier_snapshot->Id() + 1 : 
Snapshot::FIRST_SNAPSHOT_ID;
+                return 
std::make_shared<ContinuousFromSnapshotStartingScanner>(snapshot_manager,
+                                                                               
start_id);
+            } else {
+                PAIMON_ASSIGN_OR_RAISE(
+                    std::optional<Snapshot> snapshot,
+                    
snapshot_manager->EarlierOrEqualTimeMillis(timestamp_millis.value()));
+                if (snapshot == std::nullopt) {
+                    return Status::Invalid(fmt::format(
+                        "There is currently no snapshot earlier than or equal 
to timestamp [{}]",
+                        timestamp_millis.value()));
+                }
+                return 
std::make_shared<StaticFromSnapshotStartingScanner>(snapshot_manager,
+                                                                           
snapshot->Id());
+            }
+        }
+        return Status::Invalid(
+            fmt::format("Unsupported snapshot startup mode {}", 
startup_mode.ToString()));
+    }
+
+ protected:
+    CoreOptions core_options_;
+    std::shared_ptr<SnapshotReader> snapshot_reader_;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/data_split_impl.cpp 
b/src/paimon/core/table/source/data_split_impl.cpp
new file mode 100644
index 0000000..020b501
--- /dev/null
+++ b/src/paimon/core/table/source/data_split_impl.cpp
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/core/table/source/data_split_impl.h"
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace paimon {
+
+bool DataSplit::SimpleDataFileMeta::operator==(const SimpleDataFileMeta& 
other) const {
+    if (this == &other) {
+        return true;
+    }
+    return file_path == other.file_path && file_size == other.file_size &&
+           row_count == other.row_count && min_sequence_number == 
other.min_sequence_number &&
+           max_sequence_number == other.max_sequence_number && schema_id == 
other.schema_id &&
+           level == other.level && creation_time == other.creation_time &&
+           delete_row_count == other.delete_row_count;
+}
+std::string DataSplit::SimpleDataFileMeta::ToString() const {
+    return fmt::format(
+        "{{filePath: {}, fileSize: {}, rowCount: {}, minSequenceNumber: {}, "
+        "maxSequenceNumber:{}, schemaId: {}, level: {}, creationTime: {}, 
deleteRowCount: "
+        "{}}}",
+        file_path, file_size, row_count, min_sequence_number, 
max_sequence_number, schema_id, level,
+        creation_time.ToString(),
+        delete_row_count == std::nullopt ? "null" : 
std::to_string(delete_row_count.value()));
+}
+
+Result<std::optional<int64_t>> DataSplitImpl::LatestFileCreationEpochMillis() 
const {
+    if (data_files_.empty()) {
+        return std::optional<int64_t>();
+    }
+    int64_t epoch = INT64_MIN;
+    for (const auto& file : data_files_) {
+        PAIMON_ASSIGN_OR_RAISE(int64_t epoch_milli, 
file->CreationTimeEpochMillis());
+        epoch = std::max(epoch, epoch_milli);
+    }
+    return std::optional<int64_t>(epoch);
+}
+
+int64_t DataSplitImpl::RowCount() const {
+    int64_t row_count = 0;
+    for (const auto& file : data_files_) {
+        row_count += file->row_count;
+    }
+    return row_count;
+}
+
+std::vector<DataSplit::SimpleDataFileMeta> DataSplitImpl::GetFileList() const {
+    std::vector<DataSplit::SimpleDataFileMeta> result_files;
+    result_files.reserve(data_files_.size());
+    for (const auto& file : data_files_) {
+        std::string result_file_path;
+        if (!file->external_path) {
+            result_file_path = PathUtil::JoinPath(bucket_path_, 
file->file_name);
+        } else {
+            result_file_path = file->external_path.value();
+        }
+        result_files.emplace_back(result_file_path, file->file_size, 
file->row_count,
+                                  file->min_sequence_number, 
file->max_sequence_number,
+                                  file->schema_id, file->level, 
file->creation_time,
+                                  file->delete_row_count);
+    }
+    return result_files;
+}
+
+bool DataSplitImpl::operator==(const DataSplitImpl& other) const {
+    if (this == &other) {
+        return true;
+    }
+    return snapshot_id_ == other.snapshot_id_ && partition_ == 
other.partition_ &&
+           bucket_ == other.bucket_ && bucket_path_ == other.bucket_path_ &&
+           total_buckets_ == other.total_buckets_ &&
+           ObjectUtils::Equal(before_files_, other.before_files_) &&
+           before_deletion_files_ == other.before_deletion_files_ &&
+           ObjectUtils::Equal(data_files_, other.data_files_) &&
+           data_deletion_files_ == other.data_deletion_files_ &&
+           is_streaming_ == other.is_streaming_ && raw_convertible_ == 
other.raw_convertible_;
+}
+
+bool DataSplitImpl::TEST_Equal(const DataSplitImpl& other) const {
+    if (this == &other) {
+        return true;
+    }
+    return snapshot_id_ == other.snapshot_id_ && partition_ == 
other.partition_ &&
+           bucket_ == other.bucket_ && bucket_path_ == other.bucket_path_ &&
+           total_buckets_ == other.total_buckets_ &&
+           ObjectUtils::TEST_Equal(before_files_, other.before_files_) &&
+           before_deletion_files_ == other.before_deletion_files_ &&
+           ObjectUtils::TEST_Equal(data_files_, other.data_files_) &&
+           data_deletion_files_ == other.data_deletion_files_ &&
+           is_streaming_ == other.is_streaming_ && raw_convertible_ == 
other.raw_convertible_;
+}
+
+int64_t DataSplitImpl::PartialMergedRowCount() const {
+    if (!raw_convertible_) {
+        return 0;
+    }
+    int64_t sum = 0;
+    for (size_t i = 0; i < data_files_.size(); i++) {
+        const auto& data_file = data_files_[i];
+        if (data_deletion_files_.empty() || data_deletion_files_[i] == 
std::nullopt) {
+            sum += data_file->row_count;
+        } else if (data_deletion_files_[i].value().cardinality != 
std::nullopt) {
+            sum += data_file->row_count - 
data_deletion_files_[i].value().cardinality.value();
+        }
+    }
+    return sum;
+}
+
+Result<std::unique_ptr<ObjectSerializer<std::shared_ptr<DataFileMeta>>>>
+DataSplitImpl::GetFileMetaSerializer(int32_t version, const 
std::shared_ptr<MemoryPool>& pool) {
+    if (version == 1) {
+        // TODO(xinyu.lxy): C++ paimon do not support data file meta 08
+        return Status::NotImplemented("Do not support data file meta 08.");
+    } else if (version == 2) {
+        return std::make_unique<DataFileMeta09Serializer>(pool);
+    } else if (version == 3 || version == 4) {
+        return std::make_unique<DataFileMeta10Serializer>(pool);
+    } else if (version == 5 || version == 6) {
+        return std::make_unique<DataFileMeta12Serializer>(pool);
+    } else if (version == 7) {
+        return std::make_unique<DataFileMetaFirstRowIdLegacySerializer>(pool);
+    } else if (version == VERSION) {
+        return std::make_unique<DataFileMetaSerializer>(pool);
+    } else {
+        return Status::Invalid(
+            fmt::format("Expecting DataSplit version to be smaller or equal 
than {}, but found {}.",
+                        VERSION, version));
+    }
+}
+
+std::string DataSplitImpl::ToString() const {
+    return fmt::format(
+        "snapshotId={}, partition={}, bucket={}, bucketPath={}, 
totalBuckets={}, "
+        "beforeFiles={}, "
+        "beforeDeletionFiles={}, dataFiles={}, dataDeletionFiles={}, 
isStreaming={}, "
+        "rawConvertible={}",
+        snapshot_id_, partition_.ToString(), bucket_, bucket_path_,
+        total_buckets_ == std::nullopt ? "null" : 
std::to_string(total_buckets_.value()),
+        StringUtils::VectorToString(before_files_),
+        StringUtils::VectorToString(before_deletion_files_),
+        StringUtils::VectorToString(data_files_), 
StringUtils::VectorToString(data_deletion_files_),
+        is_streaming_, raw_convertible_);
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/data_split_impl.h 
b/src/paimon/core/table/source/data_split_impl.h
new file mode 100644
index 0000000..ce1124a
--- /dev/null
+++ b/src/paimon/core/table/source/data_split_impl.h
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "paimon/common/utils/object_utils.h"
+#include "paimon/common/utils/path_util.h"
+#include "paimon/common/utils/preconditions.h"
+#include "paimon/common/utils/string_utils.h"
+#include "paimon/core/io/data_file_meta_09_serializer.h"
+#include "paimon/core/io/data_file_meta_10_serializer.h"
+#include "paimon/core/io/data_file_meta_12_serializer.h"
+#include "paimon/core/io/data_file_meta_first_row_id_legacy_serializer.h"
+#include "paimon/core/io/data_file_meta_serializer.h"
+#include "paimon/core/table/source/deletion_file.h"
+#include "paimon/table/source/data_split.h"
+
+namespace paimon {
+/// Input splits. Needed by most batch computation engines.
+class DataSplitImpl : public DataSplit {
+ public:
+    static constexpr int64_t MAGIC = -2394839472490812314L;
+    static constexpr int32_t VERSION = 8;
+
+    int64_t SnapshotId() const {
+        return snapshot_id_;
+    }
+
+    const BinaryRow& Partition() const {
+        return partition_;
+    }
+
+    int32_t Bucket() const {
+        return bucket_;
+    }
+
+    const std::string& BucketPath() const {
+        return bucket_path_;
+    }
+
+    const std::optional<int32_t>& TotalBuckets() const {
+        return total_buckets_;
+    }
+
+    const std::vector<std::shared_ptr<DataFileMeta>>& BeforeFiles() const {
+        return before_files_;
+    }
+
+    const std::vector<std::optional<DeletionFile>>& BeforeDeletionFiles() 
const {
+        return before_deletion_files_;
+    }
+
+    const std::vector<std::shared_ptr<DataFileMeta>>& DataFiles() const {
+        return data_files_;
+    }
+
+    const std::vector<std::optional<DeletionFile>>& DeletionFiles() const {
+        return data_deletion_files_;
+    }
+
+    bool IsStreaming() const {
+        return is_streaming_;
+    }
+
+    bool RawConvertible() const {
+        return raw_convertible_;
+    }
+
+    Result<std::optional<int64_t>> LatestFileCreationEpochMillis() const;
+
+    int64_t RowCount() const;
+
+    std::vector<SimpleDataFileMeta> GetFileList() const override;
+
+    bool operator==(const DataSplitImpl& other) const;
+    bool TEST_Equal(const DataSplitImpl& other) const;
+
+    /// Obtain merged row count as much as possible. There are two scenarios 
where accurate row
+    /// count
+    /// can be calculated:
+    ///
+    /// 1. raw file and no deletion file.
+    ///
+    /// 2. raw file + deletion file with cardinality.
+    int64_t PartialMergedRowCount() const;
+
+    // Builder
+    /// Builder for `DataSplitImpl`.
+    class Builder {
+     public:
+        Builder(const BinaryRow& partition, int32_t bucket, const std::string& 
bucket_path,
+                std::vector<std::shared_ptr<DataFileMeta>>&& data_files)
+            : split_(std::shared_ptr<DataSplitImpl>(
+                  new DataSplitImpl(partition, bucket, bucket_path, 
std::move(data_files)))) {}
+
+        const std::vector<std::shared_ptr<DataFileMeta>>& DataFiles() {
+            return split_->DataFiles();
+        }
+
+        Builder& WithTotalBuckets(const std::optional<int32_t>& total_buckets) 
{
+            split_->total_buckets_ = total_buckets;
+            return *this;
+        }
+
+        Builder& WithSnapshot(int64_t snapshot) {
+            split_->snapshot_id_ = snapshot;
+            return *this;
+        }
+
+        Builder& WithBeforeFiles(std::vector<std::shared_ptr<DataFileMeta>>&& 
before_files) {
+            split_->before_files_ = std::move(before_files);
+            return *this;
+        }
+
+        Builder& WithBeforeDeletionFiles(
+            const std::vector<std::optional<DeletionFile>>& 
before_deletion_files) {
+            split_->before_deletion_files_ = before_deletion_files;
+            return *this;
+        }
+
+        Builder& WithDataDeletionFiles(
+            const std::vector<std::optional<DeletionFile>>& 
data_deletion_files) {
+            split_->data_deletion_files_ = data_deletion_files;
+            return *this;
+        }
+
+        Builder& IsStreaming(bool is_streaming) {
+            split_->is_streaming_ = is_streaming;
+            return *this;
+        }
+
+        Builder& RawConvertible(bool raw_convertible) {
+            split_->raw_convertible_ = raw_convertible;
+            return *this;
+        }
+
+        Result<std::shared_ptr<DataSplitImpl>> Build() const {
+            PAIMON_RETURN_NOT_OK(Preconditions::CheckArgument(split_->bucket_ 
!= -1));
+            return split_;
+        }
+
+     private:
+        std::shared_ptr<DataSplitImpl> split_;
+    };
+
+    static 
Result<std::unique_ptr<ObjectSerializer<std::shared_ptr<DataFileMeta>>>>
+    GetFileMetaSerializer(int32_t version, const std::shared_ptr<MemoryPool>& 
pool);
+
+    std::string ToString() const;
+
+ private:
+    DataSplitImpl(const BinaryRow& partition, int32_t bucket, const 
std::string& bucket_path,
+                  std::vector<std::shared_ptr<DataFileMeta>>&& data_files)
+        : partition_(partition),
+          bucket_(bucket),
+          bucket_path_(bucket_path),
+          data_files_(std::move(data_files)) {}
+
+ private:
+    int64_t snapshot_id_ = 0;
+    BinaryRow partition_ = BinaryRow::EmptyRow();
+    int32_t bucket_ = -1;
+    std::string bucket_path_;
+    std::optional<int32_t> total_buckets_;
+
+    std::vector<std::shared_ptr<DataFileMeta>> before_files_;
+    std::vector<std::optional<DeletionFile>> before_deletion_files_;
+    std::vector<std::shared_ptr<DataFileMeta>> data_files_;
+    std::vector<std::optional<DeletionFile>> data_deletion_files_;
+
+    bool is_streaming_ = false;
+    bool raw_convertible_ = false;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/deletion_file.h 
b/src/paimon/core/table/source/deletion_file.h
new file mode 100644
index 0000000..2f7c5c4
--- /dev/null
+++ b/src/paimon/core/table/source/deletion_file.h
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <cstdint>
+#include <cstdlib>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "fmt/core.h"
+#include "fmt/format.h"
+#include "fmt/ranges.h"
+#include "paimon/common/io/memory_segment_output_stream.h"
+#include "paimon/io/data_input_stream.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+/// Deletion file for data file, the first 4 bytes are length, should, the 
following is the bitmap
+/// content.
+///
+/// The first 4 bytes are length, should equal to `length`.
+/// Next 4 bytes are the magic number, should be equal to 1581511376.
+/// The remaining content should be a RoaringBitmap.
+/// Member path indicates the deletion vector index file name of the 
corresponding data file in
+/// DataSplits.
+struct DeletionFile {
+    DeletionFile(const std::string& _path, int64_t _offset, int64_t _length,
+                 const std::optional<int64_t>& _cardinality)
+        : path(_path), offset(_offset), length(_length), 
cardinality(_cardinality) {}
+
+    bool operator==(const DeletionFile& other) const {
+        if (this == &other) {
+            return true;
+        }
+        return path == other.path && offset == other.offset && length == 
other.length &&
+               cardinality == other.cardinality;
+    }
+
+    std::string ToString() const {
+        return fmt::format(
+            "{{path = {}, offset = {}, length = {}, cardinality = {}}}", path, 
offset, length,
+            cardinality == std::nullopt ? "null" : 
std::to_string(cardinality.value()));
+    }
+
+    static void Serialize(const std::optional<DeletionFile>& file, 
MemorySegmentOutputStream* out) {
+        if (file == std::nullopt) {
+            out->WriteValue<char>(0);
+        } else {
+            out->WriteValue<char>(1);
+            out->WriteString(file.value().path);
+            out->WriteValue<int64_t>(file.value().offset);
+            out->WriteValue<int64_t>(file.value().length);
+            if (file.value().cardinality == std::nullopt) {
+                out->WriteValue<int64_t>(-1);
+            } else {
+                out->WriteValue<int64_t>(file.value().cardinality.value());
+            }
+        }
+    }
+
+    static void SerializeList(const std::vector<std::optional<DeletionFile>>& 
files,
+                              MemorySegmentOutputStream* out) {
+        if (files.empty()) {
+            out->WriteValue<char>(0);
+        } else {
+            out->WriteValue<char>(1);
+            out->WriteValue<int32_t>(files.size());
+            for (const auto& file : files) {
+                Serialize(file, out);
+            }
+        }
+    }
+
+    static Result<std::vector<std::optional<DeletionFile>>> 
DeserializeList(DataInputStream* in,
+                                                                            
int32_t version) {
+        std::vector<std::optional<DeletionFile>> files;
+        PAIMON_ASSIGN_OR_RAISE(char has_deletion_file, in->ReadValue<char>());
+        if (has_deletion_file == static_cast<char>(1)) {
+            PAIMON_ASSIGN_OR_RAISE(int32_t size, in->ReadValue<int32_t>());
+            files.reserve(size);
+            for (int32_t i = 0; i < size; i++) {
+                std::optional<DeletionFile> file;
+                if (version >= 4) {
+                    PAIMON_ASSIGN_OR_RAISE(file, Deserialize(in));
+                } else if (version >= 1 && version <= 3) {
+                    PAIMON_ASSIGN_OR_RAISE(file, DeserializeV3(in));
+                } else {
+                    return Status::Invalid(
+                        fmt::format("Unsupported deletion file version: {}", 
version));
+                }
+                files.emplace_back(std::move(file));
+            }
+        }
+        return files;
+    }
+
+ private:
+    static Result<std::optional<DeletionFile>> Deserialize(DataInputStream* 
in) {
+        char has_deletion_file = 0;
+        PAIMON_ASSIGN_OR_RAISE(has_deletion_file, in->ReadValue<char>());
+        if (has_deletion_file == static_cast<char>(0)) {
+            return std::optional<DeletionFile>();
+        }
+        PAIMON_ASSIGN_OR_RAISE(std::string path, in->ReadString());
+        PAIMON_ASSIGN_OR_RAISE(int64_t offset, in->ReadValue<int64_t>());
+        PAIMON_ASSIGN_OR_RAISE(int64_t length, in->ReadValue<int64_t>());
+        PAIMON_ASSIGN_OR_RAISE(int64_t cardinality, in->ReadValue<int64_t>());
+        return std::optional<DeletionFile>(
+            DeletionFile(path, offset, length,
+                         cardinality == -1 ? std::nullopt : 
std::optional<int64_t>(cardinality)));
+    }
+
+    static Result<std::optional<DeletionFile>> DeserializeV3(DataInputStream* 
in) {
+        PAIMON_ASSIGN_OR_RAISE(char has_deletion_file, in->ReadValue<char>());
+        if (has_deletion_file == static_cast<char>(0)) {
+            return std::optional<DeletionFile>();
+        }
+        PAIMON_ASSIGN_OR_RAISE(std::string path, in->ReadString());
+        PAIMON_ASSIGN_OR_RAISE(int64_t offset, in->ReadValue<int64_t>());
+        PAIMON_ASSIGN_OR_RAISE(int64_t length, in->ReadValue<int64_t>());
+        return std::optional<DeletionFile>(DeletionFile(path, offset, length, 
std::nullopt));
+    }
+
+ public:
+    std::string path = "";
+    int64_t offset = -1;
+    int64_t length = -1;
+    // the number of deleted rows.
+    std::optional<int64_t> cardinality;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/deletion_file_test.cpp 
b/src/paimon/core/table/source/deletion_file_test.cpp
new file mode 100644
index 0000000..b283992
--- /dev/null
+++ b/src/paimon/core/table/source/deletion_file_test.cpp
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/core/table/source/deletion_file.h"
+
+#include <memory>
+
+#include "gtest/gtest.h"
+#include "paimon/common/memory/memory_segment_utils.h"
+#include "paimon/io/byte_array_input_stream.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+TEST(DeletionFileTest, TestSimple) {
+    {
+        DeletionFile df("my_path", 100, 233, std::nullopt);
+        ASSERT_EQ("{path = my_path, offset = 100, length = 233, cardinality = 
null}",
+                  df.ToString());
+    }
+    {
+        DeletionFile df("my_path", 100, 233, 234);
+        ASSERT_EQ("{path = my_path, offset = 100, length = 233, cardinality = 
234}", df.ToString());
+    }
+}
+
+TEST(DeletionFileTest, TestSerializeAndDeserialize) {
+    auto pool = GetDefaultPool();
+    MemorySegmentOutputStream out(/*segment_size=*/8, pool);
+    DeletionFile df("my_path", 100, 233, 234);
+    ASSERT_EQ(df, df);
+    DeletionFile::SerializeList({df, std::nullopt}, &out);
+    auto bytes = MemorySegmentUtils::CopyToBytes(out.Segments(), 0, 
out.CurrentSize(), pool.get());
+    auto byte_array_input_stream =
+        std::make_shared<ByteArrayInputStream>(bytes->data(), bytes->size());
+    DataInputStream in(byte_array_input_stream);
+    ASSERT_OK_AND_ASSIGN(std::vector<std::optional<DeletionFile>> 
deletion_files,
+                         DeletionFile::DeserializeList(&in, /*version=*/4));
+    ASSERT_EQ(2, deletion_files.size());
+    ASSERT_TRUE(deletion_files[0]);
+    ASSERT_EQ(deletion_files[0], df);
+    ASSERT_FALSE(deletion_files[1]);
+    ASSERT_EQ("{path = my_path, offset = 100, length = 233, cardinality = 
234}",
+              deletion_files[0].value().ToString());
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/core/table/source/plan_impl.cpp 
b/src/paimon/core/table/source/plan_impl.cpp
new file mode 100644
index 0000000..1d418c5
--- /dev/null
+++ b/src/paimon/core/table/source/plan_impl.cpp
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/core/table/source/plan_impl.h"
+
+namespace paimon {
+
+const std::shared_ptr<Plan> PlanImpl::EmptyPlan() {
+    static const std::shared_ptr<Plan> empty_plan =
+        std::make_shared<PlanImpl>(std::optional<int64_t>(), 
std::vector<std::shared_ptr<Split>>());
+    return empty_plan;
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/plan_impl.h 
b/src/paimon/core/table/source/plan_impl.h
new file mode 100644
index 0000000..e609ccb
--- /dev/null
+++ b/src/paimon/core/table/source/plan_impl.h
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <vector>
+
+#include "paimon/table/source/plan.h"
+
+namespace paimon {
+
+/// An implementation of `Plan`.
+class PlanImpl : public Plan {
+ public:
+    PlanImpl(const std::optional<int64_t>& snapshot_id,
+             const std::vector<std::shared_ptr<Split>>& splits)
+        : snapshot_id_(snapshot_id), splits_(splits) {}
+
+    std::optional<int64_t> SnapshotId() const override {
+        return snapshot_id_;
+    }
+
+    const std::vector<std::shared_ptr<Split>>& Splits() const override {
+        return splits_;
+    }
+
+    static const std::shared_ptr<Plan> EmptyPlan();
+
+ private:
+    std::optional<int64_t> snapshot_id_;
+    std::vector<std::shared_ptr<Split>> splits_;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/scan_mode.h 
b/src/paimon/core/table/source/scan_mode.h
new file mode 100644
index 0000000..c236aad
--- /dev/null
+++ b/src/paimon/core/table/source/scan_mode.h
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+namespace paimon {
+/// Scan which part of the snapshot.
+enum class ScanMode {
+    /// Scan complete data files of a snapshot.
+    ALL = 0,
+
+    /// Only scan newly changed files of a snapshot.
+    DELTA = 1
+
+    /// Only scan changelog files of a snapshot.
+    /* CHANGELOG = 2 */
+};
+
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/split.cpp 
b/src/paimon/core/table/source/split.cpp
new file mode 100644
index 0000000..007df3c
--- /dev/null
+++ b/src/paimon/core/table/source/split.cpp
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <utility>
+
+#include "fmt/format.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/io/memory_segment_output_stream.h"
+#include "paimon/common/memory/memory_segment_utils.h"
+#include "paimon/common/utils/serialization_utils.h"
+#include "paimon/core/global_index/indexed_split_impl.h"
+#include "paimon/core/io/data_file_meta_serializer.h"
+#include "paimon/core/table/source/data_split_impl.h"
+#include "paimon/core/table/source/deletion_file.h"
+#include "paimon/core/table/source/fallback_data_split.h"
+#include "paimon/core/utils/object_serializer.h"
+#include "paimon/global_index/indexed_split.h"
+#include "paimon/io/byte_array_input_stream.h"
+#include "paimon/io/data_input_stream.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/status.h"
+#include "paimon/table/source/data_split.h"
+namespace paimon {
+struct DataFileMeta;
+namespace {
+Status WriteDataSplit(const std::shared_ptr<DataSplitImpl>& data_split_impl,
+                      MemorySegmentOutputStream* out, const 
std::shared_ptr<MemoryPool>& pool) {
+    out->WriteValue<int64_t>(DataSplitImpl::MAGIC);
+    out->WriteValue<int32_t>(DataSplitImpl::VERSION);
+    out->WriteValue<int64_t>(data_split_impl->SnapshotId());
+
+    
PAIMON_RETURN_NOT_OK(SerializationUtils::SerializeBinaryRow(data_split_impl->Partition(),
 out));
+    out->WriteValue<int32_t>(data_split_impl->Bucket());
+    out->WriteString(data_split_impl->BucketPath());
+
+    std::optional<int32_t> total_buckets = data_split_impl->TotalBuckets();
+    if (total_buckets == std::nullopt) {
+        out->WriteValue<bool>(false);
+    } else {
+        out->WriteValue<bool>(true);
+        out->WriteValue<int32_t>(total_buckets.value());
+    }
+
+    DataFileMetaSerializer serializer(pool);
+    
PAIMON_RETURN_NOT_OK(serializer.SerializeList(data_split_impl->BeforeFiles(), 
out));
+
+    DeletionFile::SerializeList(data_split_impl->BeforeDeletionFiles(), out);
+    
PAIMON_RETURN_NOT_OK(serializer.SerializeList(data_split_impl->DataFiles(), 
out));
+    DeletionFile::SerializeList(data_split_impl->DeletionFiles(), out);
+    out->WriteValue<bool>(data_split_impl->IsStreaming());
+    out->WriteValue<bool>(data_split_impl->RawConvertible());
+    return Status::OK();
+}
+
+Result<std::shared_ptr<DataSplitImpl>> ReadDataSplitWithoutMagicNumber(
+    int64_t magic, DataInputStream* in, const std::shared_ptr<MemoryPool>& 
pool) {
+    int32_t version = 1;
+    if (magic == DataSplitImpl::MAGIC) {
+        PAIMON_ASSIGN_OR_RAISE(version, in->ReadValue<int32_t>());
+    }
+
+    // version 1 does not write magic number in, so the first long is snapshot 
id.
+    int64_t snapshot_id = magic;
+    if (version != 1) {
+        PAIMON_ASSIGN_OR_RAISE(snapshot_id, in->ReadValue<int64_t>());
+    }
+
+    PAIMON_ASSIGN_OR_RAISE(BinaryRow partition,
+                           SerializationUtils::DeserializeBinaryRow(in, 
pool.get()));
+    int32_t bucket = -1;
+    PAIMON_ASSIGN_OR_RAISE(bucket, in->ReadValue<int32_t>());
+    std::string bucket_path;
+    PAIMON_ASSIGN_OR_RAISE(bucket_path, in->ReadString());
+
+    std::optional<int32_t> total_buckets;
+    if (version >= 6) {
+        PAIMON_ASSIGN_OR_RAISE(bool total_buckets_exist, 
in->ReadValue<bool>());
+        if (total_buckets_exist) {
+            PAIMON_ASSIGN_OR_RAISE(total_buckets, in->ReadValue<int32_t>());
+        }
+    }
+
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<ObjectSerializer<std::shared_ptr<DataFileMeta>>> 
data_file_serializer,
+        DataSplitImpl::GetFileMetaSerializer(version, pool));
+    std::vector<std::shared_ptr<DataFileMeta>> before_files;
+    PAIMON_ASSIGN_OR_RAISE(before_files, 
data_file_serializer->DeserializeList(in));
+    // compatible for deletion file
+    std::vector<std::optional<DeletionFile>> before_deletion_files;
+    PAIMON_ASSIGN_OR_RAISE(before_deletion_files, 
DeletionFile::DeserializeList(in, version));
+
+    std::vector<std::shared_ptr<DataFileMeta>> data_files;
+    PAIMON_ASSIGN_OR_RAISE(data_files, 
data_file_serializer->DeserializeList(in));
+    // compatible for deletion file
+    std::vector<std::optional<DeletionFile>> data_deletion_files;
+    PAIMON_ASSIGN_OR_RAISE(data_deletion_files, 
DeletionFile::DeserializeList(in, version));
+
+    bool is_streaming = false;
+    PAIMON_ASSIGN_OR_RAISE(is_streaming, in->ReadValue<bool>());
+    bool raw_convertible = false;
+    PAIMON_ASSIGN_OR_RAISE(raw_convertible, in->ReadValue<bool>());
+
+    DataSplitImpl::Builder builder(partition, bucket, bucket_path, 
std::move(data_files));
+    builder.WithTotalBuckets(total_buckets)
+        .WithSnapshot(snapshot_id)
+        .WithBeforeFiles(std::move(before_files))
+        .IsStreaming(is_streaming)
+        .RawConvertible(raw_convertible);
+    if (!before_deletion_files.empty()) {
+        builder.WithBeforeDeletionFiles(before_deletion_files);
+    }
+    if (!data_deletion_files.empty()) {
+        builder.WithDataDeletionFiles(data_deletion_files);
+    }
+    return builder.Build();
+}
+
+}  // namespace
+
+Result<std::string> Split::Serialize(const std::shared_ptr<Split>& split,
+                                     const std::shared_ptr<MemoryPool>& pool) {
+    MemorySegmentOutputStream 
out(MemorySegmentOutputStream::DEFAULT_SEGMENT_SIZE, pool);
+    if (auto data_split_impl = 
std::dynamic_pointer_cast<DataSplitImpl>(split)) {
+        PAIMON_RETURN_NOT_OK(WriteDataSplit(data_split_impl, &out, pool));
+    } else if (auto indexed_split_impl = 
std::dynamic_pointer_cast<IndexedSplitImpl>(split)) {
+        out.WriteValue<int64_t>(IndexedSplitImpl::MAGIC);
+        out.WriteValue<int32_t>(IndexedSplitImpl::VERSION);
+        auto inner_split_impl =
+            
std::dynamic_pointer_cast<DataSplitImpl>(indexed_split_impl->GetDataSplit());
+        if (!inner_split_impl) {
+            return Status::Invalid("inner split in IndexedSplit is supposed to 
be DataSplit");
+        }
+        PAIMON_RETURN_NOT_OK(WriteDataSplit(inner_split_impl, &out, pool));
+        auto row_ranges = indexed_split_impl->RowRanges();
+        out.WriteValue<int32_t>(row_ranges.size());
+        for (const auto& range : row_ranges) {
+            out.WriteValue<int64_t>(range.from);
+            out.WriteValue<int64_t>(range.to);
+        }
+
+        auto scores = indexed_split_impl->Scores();
+        if (!scores.empty()) {
+            out.WriteValue<bool>(true);
+            out.WriteValue<int32_t>(scores.size());
+            for (const auto& score : scores) {
+                out.WriteValue<float>(score);
+            }
+        } else {
+            out.WriteValue<bool>(false);
+        }
+    } else {
+        return Status::Invalid("invalid split, cannot cast to DataSplit or 
IndexedSplit");
+    }
+    PAIMON_UNIQUE_PTR<Bytes> bytes =
+        MemorySegmentUtils::CopyToBytes(out.Segments(), 0, out.CurrentSize(), 
pool.get());
+    return std::string(bytes->data(), bytes->size());
+}
+
+Result<std::shared_ptr<Split>> Split::Deserialize(const char* buffer, size_t 
length,
+                                                  const 
std::shared_ptr<MemoryPool>& pool) {
+    auto input_stream = std::make_shared<ByteArrayInputStream>(buffer, length);
+    DataInputStream in(input_stream);
+
+    int64_t magic = -1;
+    PAIMON_ASSIGN_OR_RAISE(magic, in.ReadValue<int64_t>());
+
+    if (magic == IndexedSplitImpl::MAGIC) {
+        PAIMON_ASSIGN_OR_RAISE(int32_t version, in.ReadValue<int32_t>());
+        if (version != IndexedSplitImpl::VERSION) {
+            return Status::Invalid(fmt::format("Unsupported IndexedSplit 
version: {}", version));
+        }
+        PAIMON_ASSIGN_OR_RAISE(int64_t data_split_magic, 
in.ReadValue<int64_t>());
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<DataSplitImpl> data_split,
+                               
ReadDataSplitWithoutMagicNumber(data_split_magic, &in, pool));
+        PAIMON_ASSIGN_OR_RAISE(int32_t range_size, in.ReadValue<int32_t>());
+        std::vector<Range> row_ranges;
+        row_ranges.reserve(range_size);
+        for (int32_t i = 0; i < range_size; ++i) {
+            PAIMON_ASSIGN_OR_RAISE(int64_t range_from, 
in.ReadValue<int64_t>());
+            PAIMON_ASSIGN_OR_RAISE(int64_t range_to, in.ReadValue<int64_t>());
+            row_ranges.emplace_back(range_from, range_to);
+        }
+        std::vector<float> scores;
+        PAIMON_ASSIGN_OR_RAISE(bool has_scores, in.ReadValue<bool>());
+        if (has_scores) {
+            PAIMON_ASSIGN_OR_RAISE(int32_t scores_length, 
in.ReadValue<int32_t>());
+            scores.resize(scores_length);
+            for (int32_t i = 0; i < scores_length; ++i) {
+                PAIMON_ASSIGN_OR_RAISE(float score, in.ReadValue<float>());
+                scores[i] = score;
+            }
+        }
+        // TODO(lisizhuo.lsz): support fallback split in IndexedSplit
+        PAIMON_ASSIGN_OR_RAISE(int64_t pos, in.GetPos());
+        PAIMON_ASSIGN_OR_RAISE(int64_t stream_length, in.Length());
+        if (pos == stream_length) {
+            return std::make_shared<IndexedSplitImpl>(data_split, row_ranges, 
scores);
+        } else if (pos == stream_length - 1) {
+            return Status::Invalid(
+                "invalid IndexedSplit, do not support FallbackSplit in 
IndexedSplit");
+        } else {
+            return Status::Invalid(
+                fmt::format("invalid IndexedSplit, remaining {} bytes after 
deserializing",
+                            stream_length - pos));
+        }
+    } else if (magic == DataSplitImpl::MAGIC) {
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<DataSplitImpl> data_split,
+                               ReadDataSplitWithoutMagicNumber(magic, &in, 
pool));
+        PAIMON_ASSIGN_OR_RAISE(int64_t pos, in.GetPos());
+        PAIMON_ASSIGN_OR_RAISE(int64_t stream_length, in.Length());
+        if (pos == stream_length) {
+            return data_split;
+        } else if (pos == stream_length - 1) {
+            PAIMON_ASSIGN_OR_RAISE(bool is_fallback, in.ReadValue<bool>());
+            return std::make_shared<FallbackDataSplit>(data_split, 
is_fallback);
+        } else {
+            return Status::Invalid(fmt::format(
+                "invalid data split byte stream, remaining {} bytes after 
deserializing",
+                stream_length - pos));
+        }
+    }
+    return Status::Invalid("invalid split, must be DataSplit or IndexedSplit");
+}
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/split_generator.h 
b/src/paimon/core/table/source/split_generator.h
new file mode 100644
index 0000000..e7a5661
--- /dev/null
+++ b/src/paimon/core/table/source/split_generator.h
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+#include <cstddef>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/result.h"
+
+namespace paimon {
+struct DataFileMeta;
+
+/// Generate splits from `DataFileMeta`s.
+class SplitGenerator {
+ public:
+    struct SplitGroup {
+        static SplitGroup 
RawConvertibleGroup(std::vector<std::shared_ptr<DataFileMeta>>&& files) {
+            return SplitGroup(std::move(files), true);
+        }
+
+        static SplitGroup NonRawConvertibleGroup(
+            std::vector<std::shared_ptr<DataFileMeta>>&& files) {
+            return SplitGroup(std::move(files), false);
+        }
+
+        bool operator==(const SplitGroup& other) const {
+            if (this == &other) {
+                return true;
+            }
+            if (files.size() != other.files.size()) {
+                return false;
+            }
+            for (size_t i = 0; i < files.size(); ++i) {
+                if (!(*(files[i]) == *(other.files[i]))) {
+                    return false;
+                }
+            }
+            return raw_convertible == other.raw_convertible;
+        }
+
+        std::vector<std::shared_ptr<DataFileMeta>> files;
+        bool raw_convertible;
+
+     private:
+        SplitGroup(std::vector<std::shared_ptr<DataFileMeta>>&& _files, bool 
_raw_convertible)
+            : files(std::move(_files)), raw_convertible(_raw_convertible) {}
+    };
+
+ public:
+    virtual ~SplitGenerator() = default;
+    virtual Result<std::vector<SplitGroup>> SplitForBatch(
+        std::vector<std::shared_ptr<DataFileMeta>>&& files) const = 0;
+
+    virtual Result<std::vector<SplitGroup>> SplitForStreaming(
+        std::vector<std::shared_ptr<DataFileMeta>>&& files) const = 0;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/split_generator_test.cpp 
b/src/paimon/core/table/source/split_generator_test.cpp
new file mode 100644
index 0000000..caf9e70
--- /dev/null
+++ b/src/paimon/core/table/source/split_generator_test.cpp
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/core/table/source/split_generator.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "arrow/type_fwd.h"
+#include "gtest/gtest.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/manifest/file_source.h"
+#include "paimon/core/options/merge_engine.h"
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/core/table/bucket_mode.h"
+#include "paimon/core/table/source/append_only_split_generator.h"
+#include "paimon/core/table/source/merge_tree_split_generator.h"
+#include "paimon/data/timestamp.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/binary_row_generator.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+class SplitGeneratorTest : public testing::Test {
+ public:
+    void SetUp() override {
+        pool_ = GetDefaultPool();
+        key_comparator_ =
+            FieldsComparator::Create({DataField(0, arrow::field("f0", 
arrow::int32(), false))},
+                                     /*is_ascending_order=*/true)
+                .value();
+    }
+    void TearDown() override {}
+
+    std::shared_ptr<DataFileMeta> CreateDataFileMeta(const std::string& 
file_name,
+                                                     int64_t file_size, 
int64_t min_sequence_number,
+                                                     int64_t 
max_sequence_number) {
+        return std::make_shared<DataFileMeta>(
+            file_name, file_size, /*row_count=*/1, 
/*min_key=*/BinaryRow::EmptyRow(),
+            /*max_key=*/BinaryRow::EmptyRow(), 
/*key_stats=*/SimpleStats::EmptyStats(),
+            /*value_stats=*/SimpleStats::EmptyStats(), min_sequence_number, 
max_sequence_number,
+            /*schema_id=*/0,
+            /*level=*/0, 
/*extra_files=*/std::vector<std::optional<std::string>>(),
+            /*creation_time=*/Timestamp(0, 0), /*delete_row_count=*/0,
+            /*embedded_index=*/nullptr, FileSource::Append(), 
/*value_stats_cols=*/std::nullopt,
+            /*external_path=*/std::optional<std::string>(),
+            /*first_row_id=*/std::nullopt,
+            /*write_cols=*/std::nullopt);
+    }
+
+    std::shared_ptr<DataFileMeta> CreateDataFileMeta(const std::string& 
file_name, int32_t level,
+                                                     int32_t min_key, int32_t 
max_key,
+                                                     int64_t 
max_sequence_number) {
+        return CreateDataFileMeta(file_name, level, min_key, max_key, 
max_sequence_number, 0l);
+    }
+
+    std::shared_ptr<DataFileMeta> CreateDataFileMeta(const std::string& 
file_name, int32_t level,
+                                                     int32_t min_key, int32_t 
max_key,
+                                                     int64_t 
max_sequence_number,
+                                                     std::optional<int64_t> 
delete_row_count) {
+        return std::make_shared<DataFileMeta>(
+            file_name, max_key - min_key + 1, /*row_count=*/max_key - min_key 
+ 1,
+            BinaryRowGenerator::GenerateRow({min_key}, pool_.get()),
+            BinaryRowGenerator::GenerateRow({max_key}, pool_.get()),
+            /*key_stats=*/SimpleStats::EmptyStats(),
+            /*value_stats=*/SimpleStats::EmptyStats(), 0, max_sequence_number,
+            /*schema_id=*/0,
+            /*level=*/level, 
/*extra_files=*/std::vector<std::optional<std::string>>(),
+            /*creation_time=*/Timestamp(0, 0), 
/*delete_row_count=*/delete_row_count,
+            /*embedded_index=*/nullptr, FileSource::Append(),
+            /*external_path=*/std::nullopt,
+            /*value_stats_cols=*/std::nullopt, /*first_row_id=*/std::nullopt,
+            /*write_cols=*/std::nullopt);
+    }
+
+    std::shared_ptr<DataFileMeta> CreateDataFileMeta(const std::string& 
file_name, int32_t min_key,
+                                                     int32_t max_key) {
+        return std::make_shared<DataFileMeta>(
+            file_name, /*file_size=*/max_key - min_key + 1,
+            /*row_count=*/max_key - min_key + 1, /*min_key=*/
+            BinaryRowGenerator::GenerateRow({min_key}, pool_.get()),
+            /*max_key=*/BinaryRowGenerator::GenerateRow({max_key}, 
pool_.get()),
+            /*key_stats=*/SimpleStats::EmptyStats(),
+            /*value_stats=*/SimpleStats::EmptyStats(), 
/*min_sequence_number=*/0,
+            /*max_sequence_number=*/0,
+            /*schema_id=*/0,
+            /*level=*/0, 
/*extra_files=*/std::vector<std::optional<std::string>>(),
+            /*creation_time=*/Timestamp(0, 0), /*delete_row_count=*/0,
+            /*embedded_index=*/nullptr, FileSource::Append(), 
/*external_path=*/std::nullopt,
+            /*value_stats_cols=*/std::nullopt, /*first_row_id=*/std::nullopt,
+            /*write_cols=*/std::nullopt);
+    }
+
+    static void CheckResult(const std::vector<SplitGenerator::SplitGroup>& 
result_groups,
+                            const std::vector<std::vector<std::string>>& 
expected_file_names,
+                            const std::vector<bool>& expected_raw_convertible) 
{
+        std::vector<bool> result_raw_convertible;
+        for (const auto& group : result_groups) {
+            result_raw_convertible.push_back(group.raw_convertible);
+        }
+        ASSERT_EQ(result_raw_convertible, expected_raw_convertible);
+        CheckResult(result_groups, expected_file_names);
+    }
+
+    static void CheckResult(const std::vector<SplitGenerator::SplitGroup>& 
result_groups,
+                            const std::vector<std::vector<std::string>>& 
expected_file_names) {
+        std::vector<std::vector<std::string>> expected = expected_file_names;
+        std::vector<std::vector<std::string>> result;
+        for (const auto& group : result_groups) {
+            std::vector<std::string> one_group_files;
+            for (const auto& file : group.files) {
+                one_group_files.push_back(file->file_name);
+            }
+            result.push_back(std::move(one_group_files));
+        }
+        for (auto& re : result) {
+            std::sort(re.begin(), re.end());
+        }
+        for (auto& ep : expected) {
+            std::sort(ep.begin(), ep.end());
+        }
+        std::sort(result.begin(), result.end());
+        std::sort(expected.begin(), expected.end());
+        ASSERT_EQ(result, expected);
+    }
+
+ private:
+    std::shared_ptr<MemoryPool> pool_;
+    std::shared_ptr<FieldsComparator> key_comparator_;
+};
+
+TEST_F(SplitGeneratorTest, TestAppend) {
+    std::vector<std::shared_ptr<DataFileMeta>> files = {
+        CreateDataFileMeta("1", 11, 0, 20),  CreateDataFileMeta("2", 13, 21, 
30),
+        CreateDataFileMeta("3", 46, 31, 40), CreateDataFileMeta("4", 23, 41, 
50),
+        CreateDataFileMeta("5", 4, 51, 60),  CreateDataFileMeta("6", 101, 61, 
100)};
+    {
+        auto tmp_files = files;
+        AppendOnlySplitGenerator split_generator(/*target_split_size=*/40, 
/*open_file_cost=*/2,
+                                                 BucketMode::HASH_FIXED);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator.SplitForBatch(std::move(tmp_files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}, {"3"}, 
{"4", "5"}, {"6"}};
+        CheckResult(split_groups, expected);
+    }
+    {
+        auto tmp_files = files;
+        AppendOnlySplitGenerator split_generator(/*target_split_size=*/70, 
/*open_file_cost=*/2,
+                                                 BucketMode::HASH_FIXED);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator.SplitForBatch(std::move(tmp_files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2", "3"}, 
{"4", "5"}, {"6"}};
+        CheckResult(split_groups, expected);
+    }
+    {
+        auto tmp_files = files;
+        AppendOnlySplitGenerator split_generator(/*target_split_size=*/40, 
/*open_file_cost=*/20,
+                                                 BucketMode::HASH_FIXED);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator.SplitForBatch(std::move(tmp_files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}, {"3"}, 
{"4"}, {"5"}, {"6"}};
+        CheckResult(split_groups, expected);
+    }
+    {
+        auto tmp_files = files;
+        AppendOnlySplitGenerator split_generator(/*target_split_size=*/40, 
/*open_file_cost=*/2,
+                                                 BucketMode::BUCKET_UNAWARE);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator.SplitForStreaming(std::move(tmp_files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}, {"3"}, 
{"4", "5"}, {"6"}};
+        CheckResult(split_groups, expected);
+    }
+    {
+        auto tmp_files = files;
+        AppendOnlySplitGenerator split_generator(/*target_split_size=*/40, 
/*open_file_cost=*/2,
+                                                 BucketMode::HASH_FIXED);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator.SplitForStreaming(std::move(tmp_files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2", "3", "4", 
"5", "6"}};
+        CheckResult(split_groups, expected);
+    }
+}
+
+TEST_F(SplitGeneratorTest, TestMergeTree) {
+    std::vector<std::shared_ptr<DataFileMeta>> files = {
+        CreateDataFileMeta("1", 0, 10),  CreateDataFileMeta("2", 0, 12),
+        CreateDataFileMeta("3", 15, 60), CreateDataFileMeta("4", 18, 40),
+        CreateDataFileMeta("5", 82, 85), CreateDataFileMeta("6", 100, 200)};
+    {
+        auto tmp_files = files;
+        MergeTreeSplitGenerator split_generator(/*target_split_size=*/100, 
/*open_file_cost=*/2,
+                                                
/*deletion_vectors_enabled=*/false,
+                                                MergeEngine::DEDUPLICATE, 
key_comparator_);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator.SplitForBatch(std::move(tmp_files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2", "3", "4", 
"5"}, {"6"}};
+        CheckResult(split_groups, expected);
+    }
+    {
+        auto tmp_files = files;
+        MergeTreeSplitGenerator split_generator(/*target_split_size=*/100, 
/*open_file_cost=*/30,
+                                                
/*deletion_vectors_enabled=*/false,
+                                                MergeEngine::DEDUPLICATE, 
key_comparator_);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator.SplitForBatch(std::move(tmp_files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2", "3", 
"4"}, {"5"}, {"6"}};
+        CheckResult(split_groups, expected);
+    }
+}
+
+TEST_F(SplitGeneratorTest, TestSplitRawConvertible) {
+    MergeTreeSplitGenerator split_generator(/*target_split_size=*/100, 
/*open_file_cost=*/2,
+                                            /*deletion_vectors_enabled=*/false,
+                                            MergeEngine::DEDUPLICATE, 
key_comparator_);
+    {
+        // When level0 exists, should not be rawConvertible
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 0, 0, 10, 10l), CreateDataFileMeta("2", 0, 
10, 20, 20l)};
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             split_generator.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}};
+        std::vector<bool> expected_raw_convertible = {false};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+    {
+        // When deleteRowCount > 0, should not be rawConvertible
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 1, 0, 10, 10l, /*delete_row_count=*/1l),
+            CreateDataFileMeta("2", 1, 10, 20, 20l)};
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             split_generator.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}};
+        std::vector<bool> expected_raw_convertible = {false};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+    {
+        // No level0 and deleteRowCount == 0:
+        // All in one level, should be rawConvertible
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 1, 0, 10, 10l), CreateDataFileMeta("2", 1, 
10, 20, 20l)};
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             split_generator.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}};
+        std::vector<bool> expected_raw_convertible = {true};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+    {
+        // Not all in one level, should not be rawConvertible
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 1, 0, 10, 10l), CreateDataFileMeta("2", 2, 
10, 20, 20l)};
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             split_generator.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}};
+        std::vector<bool> expected_raw_convertible = {false};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+    {
+        // Not all in one level but with deletion vectors enabled, should be 
rawConvertible
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 1, 0, 10, 10l), CreateDataFileMeta("2", 2, 
10, 20, 20l)};
+        MergeTreeSplitGenerator split_generator_dv(/*target_split_size=*/100, 
/*open_file_cost=*/2,
+                                                   
/*deletion_vectors_enabled=*/true,
+                                                   MergeEngine::DEDUPLICATE, 
key_comparator_);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator_dv.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}};
+        std::vector<bool> expected_raw_convertible = {true};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+    {
+        // Not all in one level but with first row merge engine, should be 
rawConvertible
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 1, 0, 10, 10l), CreateDataFileMeta("2", 2, 
10, 20, 20l)};
+        MergeTreeSplitGenerator split_generator_dv(/*target_split_size=*/100, 
/*open_file_cost=*/2,
+                                                   
/*deletion_vectors_enabled=*/false,
+                                                   MergeEngine::FIRST_ROW, 
key_comparator_);
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             
split_generator_dv.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}};
+        std::vector<bool> expected_raw_convertible = {true};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+    {
+        // Split with one file should be rawConvertible
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 1, 0, 10, 10L),  CreateDataFileMeta("2", 
2, 0, 12, 12L),
+            CreateDataFileMeta("3", 3, 15, 60, 60L), CreateDataFileMeta("4", 
4, 18, 40, 40L),
+            CreateDataFileMeta("5", 5, 82, 85, 85L), CreateDataFileMeta("6", 
6, 100, 200, 200L)};
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             split_generator.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2", "3", "4", 
"5"}, {"6"}};
+        std::vector<bool> expected_raw_convertible = {false, true};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+    {
+        // test convertible for old version
+        std::vector<std::shared_ptr<DataFileMeta>> files = {
+            CreateDataFileMeta("1", 1, 0, 10, 10l, std::nullopt),
+            CreateDataFileMeta("2", 1, 10, 20, 20l, std::nullopt)};
+        ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> 
split_groups,
+                             split_generator.SplitForBatch(std::move(files)));
+        std::vector<std::vector<std::string>> expected = {{"1", "2"}};
+        std::vector<bool> expected_raw_convertible = {true};
+        CheckResult(split_groups, expected, expected_raw_convertible);
+    }
+}
+
+TEST_F(SplitGeneratorTest, TestMergeTreeSplitRawConvertible) {
+    MergeTreeSplitGenerator split_generator(/*target_split_size=*/100, 
/*open_file_cost=*/2,
+                                            /*deletion_vectors_enabled=*/false,
+                                            MergeEngine::DEDUPLICATE, 
key_comparator_);
+    std::vector<std::shared_ptr<DataFileMeta>> files = {
+        CreateDataFileMeta("1", 0, 0, 10, 10L),     CreateDataFileMeta("2", 0, 
0, 12, 12L),
+        CreateDataFileMeta("3", 0, 13, 20, 20L),    CreateDataFileMeta("4", 0, 
21, 200, 200L),
+        CreateDataFileMeta("5", 0, 201, 210, 210L), CreateDataFileMeta("6", 0, 
211, 220, 220L)};
+    ASSERT_OK_AND_ASSIGN(std::vector<SplitGenerator::SplitGroup> split_groups,
+                         split_generator.SplitForBatch(std::move(files)));
+    std::vector<std::vector<std::string>> expected = {{"1", "2", "3"}, {"4"}, 
{"5", "6"}};
+    std::vector<bool> expected_raw_convertible = {false, true, false};
+    CheckResult(split_groups, expected, expected_raw_convertible);
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/core/table/source/startup_mode.cpp 
b/src/paimon/core/table/source/startup_mode.cpp
new file mode 100644
index 0000000..f6e3cec
--- /dev/null
+++ b/src/paimon/core/table/source/startup_mode.cpp
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/table/source/startup_mode.h"
+
+#include "fmt/format.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+const StartupMode StartupMode::Default() {
+    static const StartupMode mode = StartupMode("default");
+    return mode;
+}
+
+const StartupMode StartupMode::LatestFull() {
+    static const StartupMode mode = StartupMode("latest-full");
+    return mode;
+}
+const StartupMode StartupMode::Latest() {
+    static const StartupMode mode = StartupMode("latest");
+    return mode;
+}
+const StartupMode StartupMode::FromSnapshot() {
+    static const StartupMode mode = StartupMode("from-snapshot");
+    return mode;
+}
+const StartupMode StartupMode::FromSnapshotFull() {
+    static const StartupMode mode = StartupMode("from-snapshot-full");
+    return mode;
+}
+const StartupMode StartupMode::FromTimestamp() {
+    static const StartupMode mode = StartupMode("from-timestamp");
+    return mode;
+}
+
+std::string StartupMode::ToString() const {
+    return value_;
+}
+
+bool StartupMode::operator==(const StartupMode& other) const {
+    if (this == &other) {
+        return true;
+    }
+    return value_ == other.value_;
+}
+
+Result<StartupMode> StartupMode::FromString(const std::string& str) {
+    if (str == StartupMode::Default().ToString()) {
+        return StartupMode::Default();
+    } else if (str == StartupMode::LatestFull().ToString()) {
+        return StartupMode::LatestFull();
+    } else if (str == StartupMode::Latest().ToString()) {
+        return StartupMode::Latest();
+    } else if (str == StartupMode::FromSnapshot().ToString()) {
+        return StartupMode::FromSnapshot();
+    } else if (str == StartupMode::FromSnapshotFull().ToString()) {
+        return StartupMode::FromSnapshotFull();
+    } else if (str == StartupMode::FromTimestamp().ToString()) {
+        return StartupMode::FromTimestamp();
+    } else {
+        return Status::Invalid(fmt::format("invalid startup mode {}", str));
+    }
+}
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/startup_mode_test.cpp 
b/src/paimon/core/table/source/startup_mode_test.cpp
new file mode 100644
index 0000000..d455c57
--- /dev/null
+++ b/src/paimon/core/table/source/startup_mode_test.cpp
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/table/source/startup_mode.h"
+
+#include "gtest/gtest.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(StartupModeTest, FromString) {
+    ASSERT_OK_AND_ASSIGN(StartupMode mode, StartupMode::FromString("default"));
+    ASSERT_EQ(StartupMode::Default(), mode);
+    ASSERT_OK_AND_ASSIGN(mode, StartupMode::FromString("latest-full"));
+    ASSERT_EQ(StartupMode::LatestFull(), mode);
+    ASSERT_OK_AND_ASSIGN(mode, StartupMode::FromString("latest"));
+    ASSERT_EQ(StartupMode::Latest(), mode);
+    ASSERT_OK_AND_ASSIGN(mode, StartupMode::FromString("from-snapshot"));
+    ASSERT_EQ(StartupMode::FromSnapshot(), mode);
+    ASSERT_OK_AND_ASSIGN(mode, StartupMode::FromString("from-snapshot-full"));
+    ASSERT_EQ(StartupMode::FromSnapshotFull(), mode);
+    ASSERT_OK_AND_ASSIGN(mode, StartupMode::FromString("from-timestamp"));
+    ASSERT_EQ(StartupMode::FromTimestamp(), mode);
+    ASSERT_NOK(StartupMode::FromString("unknown"));
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/core/table/source/table_scan.cpp 
b/src/paimon/core/table/source/table_scan.cpp
new file mode 100644
index 0000000..5afe993
--- /dev/null
+++ b/src/paimon/core/table/source/table_scan.cpp
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/table/source/table_scan.h"
+
+#include <map>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "fmt/format.h"
+#include "paimon/common/predicate/predicate_validator.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/index/index_file_handler.h"
+#include "paimon/core/manifest/index_manifest_file.h"
+#include "paimon/core/manifest/manifest_file.h"
+#include "paimon/core/manifest/manifest_list.h"
+#include "paimon/core/operation/append_only_file_store_scan.h"
+#include "paimon/core/operation/data_evolution_file_store_scan.h"
+#include "paimon/core/operation/file_store_scan.h"
+#include "paimon/core/operation/key_value_file_store_scan.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/schema_validation.h"
+#include "paimon/core/schema/table_schema.h"
+#include "paimon/core/table/bucket_mode.h"
+#include "paimon/core/table/source/abstract_table_scan.h"
+#include "paimon/core/table/source/append_only_split_generator.h"
+#include "paimon/core/table/source/data_evolution_batch_scan.h"
+#include "paimon/core/table/source/data_evolution_split_generator.h"
+#include "paimon/core/table/source/data_table_batch_scan.h"
+#include "paimon/core/table/source/data_table_stream_scan.h"
+#include "paimon/core/table/source/merge_tree_split_generator.h"
+#include "paimon/core/table/source/snapshot/snapshot_reader.h"
+#include "paimon/core/table/source/split_generator.h"
+#include "paimon/core/table/system/system_table.h"
+#include "paimon/core/utils/branch_manager.h"
+#include "paimon/core/utils/field_mapping.h"
+#include "paimon/core/utils/file_store_path_factory.h"
+#include "paimon/core/utils/index_file_path_factories.h"
+#include "paimon/core/utils/snapshot_manager.h"
+#include "paimon/format/file_format.h"
+#include "paimon/result.h"
+#include "paimon/scan_context.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class Schema;
+}  // namespace arrow
+
+namespace paimon {
+class Executor;
+class MemoryPool;
+
+namespace {
+
+class TableScanImpl {
+ public:
+    static Result<std::unique_ptr<FileStoreScan>> CreateFileStoreScan(
+        const std::shared_ptr<FileStorePathFactory>& path_factory,
+        const std::shared_ptr<arrow::Schema>& arrow_schema,
+        const std::shared_ptr<TableSchema>& table_schema, const CoreOptions& 
core_options,
+        const std::shared_ptr<Executor>& executor, const 
std::shared_ptr<MemoryPool>& memory_pool,
+        const ScanContext* context) {
+        auto fs = core_options.GetFileSystem();
+        auto manifest_file_format = core_options.GetManifestFormat();
+        std::string branch = 
BranchManager::NormalizeBranch(core_options.GetBranch());
+        auto snapshot_manager = std::make_shared<SnapshotManager>(fs, 
context->GetPath(), branch);
+        // TODO(liancheng.lsz): support fallback branch in scan
+        auto schema_manager = std::make_shared<SchemaManager>(fs, 
context->GetPath(), branch);
+        PAIMON_ASSIGN_OR_RAISE(
+            std::shared_ptr<ManifestList> manifest_list,
+            ManifestList::Create(fs, manifest_file_format, 
core_options.GetManifestCompression(),
+                                 path_factory, memory_pool));
+        PAIMON_ASSIGN_OR_RAISE(
+            std::shared_ptr<arrow::Schema> partition_schema,
+            FieldMapping::GetPartitionSchema(arrow_schema, 
table_schema->PartitionKeys()));
+        PAIMON_ASSIGN_OR_RAISE(
+            std::shared_ptr<ManifestFile> manifest_file,
+            ManifestFile::Create(fs, manifest_file_format, 
core_options.GetManifestCompression(),
+                                 path_factory, 
core_options.GetManifestTargetFileSize(),
+                                 memory_pool, core_options, partition_schema));
+        if (table_schema->PrimaryKeys().empty()) {
+            if (core_options.DataEvolutionEnabled()) {
+                return DataEvolutionFileStoreScan::Create(
+                    snapshot_manager, schema_manager, manifest_list, 
manifest_file, table_schema,
+                    arrow_schema, context->GetScanFilters(), core_options, 
executor, memory_pool);
+            }
+            return AppendOnlyFileStoreScan::Create(
+                snapshot_manager, schema_manager, manifest_list, 
manifest_file, table_schema,
+                arrow_schema, context->GetScanFilters(), core_options, 
executor, memory_pool);
+        }
+        return KeyValueFileStoreScan::Create(
+            snapshot_manager, schema_manager, manifest_list, manifest_file, 
table_schema,
+            arrow_schema, context->GetScanFilters(), core_options, executor, 
memory_pool);
+    }
+
+    static Result<std::unique_ptr<SplitGenerator>> CreateSplitGenerator(
+        const std::shared_ptr<TableSchema>& table_schema, const CoreOptions& 
core_options,
+        const ScanContext* context) {
+        auto source_split_target_size = 
core_options.GetSourceSplitTargetSize();
+        auto source_split_open_file_cost = 
core_options.GetSourceSplitOpenFileCost();
+        if (table_schema->PrimaryKeys().empty()) {
+            if (core_options.DataEvolutionEnabled()) {
+                return 
std::make_unique<DataEvolutionSplitGenerator>(source_split_target_size,
+                                                                     
source_split_open_file_cost);
+            }
+            BucketMode bucket_mode = (core_options.GetBucket() == -1 ? 
BucketMode::BUCKET_UNAWARE
+                                                                     : 
BucketMode::HASH_FIXED);
+            return std::make_unique<AppendOnlySplitGenerator>(
+                source_split_target_size, source_split_open_file_cost, 
bucket_mode);
+        } else {
+            // TODO(liancheng.lsz): support evolution
+            PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> 
trimmed_primary_keys,
+                                   table_schema->TrimmedPrimaryKeys());
+            PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> trimmed_pk_fields,
+                                   
table_schema->GetFields(trimmed_primary_keys));
+            PAIMON_ASSIGN_OR_RAISE(
+                std::shared_ptr<FieldsComparator> key_comparator,
+                FieldsComparator::Create(trimmed_pk_fields, 
/*is_ascending_order=*/true));
+            return std::make_unique<MergeTreeSplitGenerator>(
+                source_split_target_size, source_split_open_file_cost,
+                core_options.DeletionVectorsEnabled(), 
core_options.GetMergeEngine(),
+                key_comparator);
+        }
+    }
+
+    static Result<std::unique_ptr<IndexFileHandler>> CreateIndexFileHandler(
+        const CoreOptions& core_options, const 
std::shared_ptr<FileStorePathFactory>& path_factory,
+        const std::shared_ptr<MemoryPool>& memory_pool) {
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<IndexManifestFile> 
index_manifest_file,
+                               IndexManifestFile::Create(
+                                   core_options.GetFileSystem(), 
core_options.GetManifestFormat(),
+                                   core_options.GetManifestCompression(), 
path_factory,
+                                   core_options.GetBucket(), memory_pool, 
core_options));
+        return std::make_unique<IndexFileHandler>(
+            core_options.GetFileSystem(), std::move(index_manifest_file),
+            std::make_shared<IndexFilePathFactories>(path_factory),
+            core_options.DeletionVectorsBitmap64(), memory_pool);
+    }
+};
+
+Result<std::unique_ptr<TableScan>> NewDataTableScan(const 
std::shared_ptr<ScanContext>& context);
+
+}  // namespace
+
+Result<std::unique_ptr<TableScan>> 
TableScan::Create(std::unique_ptr<ScanContext> context) {
+    if (context == nullptr) {
+        return Status::Invalid("scan context is null pointer");
+    }
+    if (context->GetMemoryPool() == nullptr) {
+        return Status::Invalid("memory pool is null pointer");
+    }
+    if (context->GetExecutor() == nullptr) {
+        return Status::Invalid("executor is null pointer");
+    }
+
+    std::shared_ptr<ScanContext> shared_context = std::move(context);
+    // load schema
+    PAIMON_ASSIGN_OR_RAISE(CoreOptions tmp_options,
+                           CoreOptions::FromMap(shared_context->GetOptions(),
+                                                
shared_context->GetSpecificFileSystem()));
+    PAIMON_ASSIGN_OR_RAISE(std::optional<SystemTablePath> system_table_path,
+                           
SystemTableLoader::TryParsePath(shared_context->GetPath()));
+    if (system_table_path) {
+        PAIMON_ASSIGN_OR_RAISE(
+            std::shared_ptr<SystemTable> system_table,
+            SystemTableLoader::LoadFromPath(tmp_options.GetFileSystem(), 
shared_context->GetPath(),
+                                            shared_context->GetOptions()));
+        return system_table->NewScan(shared_context);
+    }
+    return NewDataTableScan(shared_context);
+}
+
+namespace {
+
+Result<std::unique_ptr<TableScan>> NewDataTableScan(const 
std::shared_ptr<ScanContext>& context) {
+    PAIMON_ASSIGN_OR_RAISE(
+        CoreOptions tmp_options,
+        CoreOptions::FromMap(context->GetOptions(), 
context->GetSpecificFileSystem()));
+    std::string branch = 
BranchManager::NormalizeBranch(tmp_options.GetBranch());
+    SchemaManager schema_manager(tmp_options.GetFileSystem(), 
context->GetPath(), branch);
+    PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>> 
latest_table_schema,
+                           schema_manager.Latest());
+    if (latest_table_schema == std::nullopt) {
+        return Status::Invalid("not found latest schema");
+    }
+    const auto& table_schema = latest_table_schema.value();
+    if (table_schema->Id() != TableSchema::FIRST_SCHEMA_ID &&
+        !table_schema->PrimaryKeys().empty()) {
+        return Status::NotImplemented(
+            "do not support schema evolution in pk table while scan process");
+    }
+    // merge options
+    auto options = table_schema->Options();
+    for (const auto& [key, value] : context->GetOptions()) {
+        options[key] = value;
+    }
+    PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
+                           CoreOptions::FromMap(options, 
context->GetSpecificFileSystem()));
+    // validate options
+    if (core_options.GetBucket() == -1) {
+        if (!table_schema->PrimaryKeys().empty()) {
+            return Status::NotImplemented(fmt::format(
+                "do not support pk table bucket={} in scan process", 
core_options.GetBucket()));
+        }
+    } else if (core_options.GetBucket() < 1 &&
+               !SchemaValidation::IsPostponeBucketTable(*table_schema, 
core_options.GetBucket())) {
+        return Status::Invalid(
+            fmt::format("do not support bucket={} in scan process", 
core_options.GetBucket()));
+    }
+
+    // validate schema and scan filter
+    auto arrow_schema = 
DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
+    if (context->GetScanFilters() && 
context->GetScanFilters()->GetPredicate()) {
+        PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithSchema(
+            *arrow_schema, context->GetScanFilters()->GetPredicate(),
+            /*validate_field_idx=*/false));
+        PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithLiterals(
+            context->GetScanFilters()->GetPredicate()));
+    }
+    PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> external_paths,
+                           core_options.CreateExternalPaths());
+    PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> 
global_index_external_path,
+                           core_options.CreateGlobalIndexExternalPath());
+
+    PAIMON_ASSIGN_OR_RAISE(
+        std::shared_ptr<FileStorePathFactory> path_factory,
+        FileStorePathFactory::Create(
+            context->GetPath(), arrow_schema, table_schema->PartitionKeys(),
+            core_options.GetPartitionDefaultName(), 
core_options.GetFileFormat()->Identifier(),
+            core_options.DataFilePrefix(), 
core_options.LegacyPartitionNameEnabled(),
+            external_paths, global_index_external_path, 
core_options.IndexFileInDataFileDir(),
+            context->GetMemoryPool()));
+
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileStoreScan> file_store_scan,
+                           TableScanImpl::CreateFileStoreScan(
+                               path_factory, arrow_schema, table_schema, 
core_options,
+                               context->GetExecutor(), 
context->GetMemoryPool(), context.get()));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<SplitGenerator> split_generator,
+        TableScanImpl::CreateSplitGenerator(table_schema, core_options, 
context.get()));
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<IndexFileHandler> 
index_file_handler,
+                           TableScanImpl::CreateIndexFileHandler(core_options, 
path_factory,
+                                                                 
context->GetMemoryPool()));
+    auto snapshot_reader = std::make_shared<SnapshotReader>(
+        file_store_scan, path_factory, std::move(split_generator), 
std::move(index_file_handler));
+    if (context->IsStreamingMode()) {
+        return std::make_unique<DataTableStreamScan>(core_options, 
snapshot_reader);
+    }
+    auto batch_scan =
+        
std::make_unique<DataTableBatchScan>(/*pk_table=*/!table_schema->PrimaryKeys().empty(),
+                                             core_options, snapshot_reader, 
context->GetLimit());
+    if (!core_options.DataEvolutionEnabled()) {
+        return batch_scan;
+    }
+    return std::make_unique<DataEvolutionBatchScan>(
+        context->GetPath(), snapshot_reader, std::move(batch_scan), 
context->GetGlobalIndexResult(),
+        core_options, context->GetMemoryPool(), context->GetExecutor());
+}
+
+}  // namespace
+
+}  // namespace paimon
diff --git a/src/paimon/core/table/source/table_scan_test.cpp 
b/src/paimon/core/table/source/table_scan_test.cpp
new file mode 100644
index 0000000..8e43ad4
--- /dev/null
+++ b/src/paimon/core/table/source/table_scan_test.cpp
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/table/source/table_scan.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "paimon/defs.h"
+#include "paimon/scan_context.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+TEST(TableScanTest, TestNoSnapshot) {
+    std::string path = paimon::test::GetDataDir() +
+                       
"/orc/append_table_with_nested_type.db/append_table_with_nested_type/";
+    ScanContextBuilder builder(path);
+    builder.AddOption(Options::FILE_FORMAT, "orc");
+    ASSERT_OK_AND_ASSIGN(auto context, builder.Finish());
+    ASSERT_OK_AND_ASSIGN(auto table_scan, 
TableScan::Create(std::move(context)));
+    ASSERT_OK_AND_ASSIGN(auto plan, table_scan->CreatePlan());
+    ASSERT_FALSE(plan->SnapshotId());
+    ASSERT_TRUE(plan->Splits().empty());
+}
+
+TEST(TableScanTest, TestNonExistTable) {
+    std::string path = paimon::test::GetDataDir() + "/non-exist.db/non-exist/";
+    ScanContextBuilder builder(path);
+    builder.AddOption(Options::FILE_FORMAT, "orc");
+    ASSERT_OK_AND_ASSIGN(auto context, builder.Finish());
+    ASSERT_NOK_WITH_MSG(TableScan::Create(std::move(context)), "not found 
latest schema");
+}
+
+TEST(TableScanTest, TestNoSchemaEvolution) {
+    // do not bear schema evolution in scan
+    std::string path =
+        paimon::test::GetDataDir() + 
"/orc/pk_table_with_alter_table.db/pk_table_with_alter_table/";
+    ScanContextBuilder builder(path);
+    builder.AddOption(Options::FILE_FORMAT, "orc");
+    ASSERT_OK_AND_ASSIGN(auto context, builder.Finish());
+    ASSERT_NOK_WITH_MSG(TableScan::Create(std::move(context)), "do not support 
schema evolution");
+}
+
+}  // namespace paimon::test

Reply via email to