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

SteNicholas 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 d0c1581b feat(snapshot): add snapshot file listing API (#270)
d0c1581b is described below

commit d0c1581b474c4c9cd917756a9e604bc4915f5daa
Author: lxy <[email protected]>
AuthorDate: Wed Sep 2 18:59:37 2026 +0800

    feat(snapshot): add snapshot file listing API (#270)
---
 include/paimon/api.h                         |  37 +--
 include/paimon/snapshot/snapshot_file_scan.h |  76 +++++
 src/paimon/CMakeLists.txt                    |   2 +
 src/paimon/core/operation/file_store_scan.h  |  14 +-
 src/paimon/core/snapshot_file_scan.cpp       | 412 +++++++++++++++++++++++++++
 src/paimon/core/snapshot_file_scan_test.cpp  | 317 +++++++++++++++++++++
 6 files changed, 833 insertions(+), 25 deletions(-)

diff --git a/include/paimon/api.h b/include/paimon/api.h
index 81f236bc..f188085d 100644
--- a/include/paimon/api.h
+++ b/include/paimon/api.h
@@ -20,24 +20,25 @@
 
 #pragma once
 
-#include "paimon/commit_context.h"           // IWYU pragma: export
-#include "paimon/defs.h"                     // IWYU pragma: export
-#include "paimon/factories/factory.h"        // IWYU pragma: export
-#include "paimon/file_store_commit.h"        // IWYU pragma: export
-#include "paimon/file_store_write.h"         // IWYU pragma: export
-#include "paimon/fs/file_system_factory.h"   // IWYU pragma: export
-#include "paimon/memory/memory_pool.h"       // IWYU pragma: export
-#include "paimon/predicate/predicate.h"      // IWYU pragma: export
-#include "paimon/read_context.h"             // IWYU pragma: export
-#include "paimon/reader/batch_reader.h"      // IWYU pragma: export
-#include "paimon/record_batch.h"             // IWYU pragma: export
-#include "paimon/result.h"                   // IWYU pragma: export
-#include "paimon/scan_context.h"             // IWYU pragma: export
-#include "paimon/statistics_mode.h"          // IWYU pragma: export
-#include "paimon/status.h"                   // IWYU pragma: export
-#include "paimon/table/source/table_read.h"  // IWYU pragma: export
-#include "paimon/table/source/table_scan.h"  // IWYU pragma: export
-#include "paimon/write_context.h"            // IWYU pragma: export
+#include "paimon/commit_context.h"               // IWYU pragma: export
+#include "paimon/defs.h"                         // IWYU pragma: export
+#include "paimon/factories/factory.h"            // IWYU pragma: export
+#include "paimon/file_store_commit.h"            // IWYU pragma: export
+#include "paimon/file_store_write.h"             // IWYU pragma: export
+#include "paimon/fs/file_system_factory.h"       // IWYU pragma: export
+#include "paimon/memory/memory_pool.h"           // IWYU pragma: export
+#include "paimon/predicate/predicate.h"          // IWYU pragma: export
+#include "paimon/read_context.h"                 // IWYU pragma: export
+#include "paimon/reader/batch_reader.h"          // IWYU pragma: export
+#include "paimon/record_batch.h"                 // IWYU pragma: export
+#include "paimon/result.h"                       // IWYU pragma: export
+#include "paimon/scan_context.h"                 // IWYU pragma: export
+#include "paimon/snapshot/snapshot_file_scan.h"  // IWYU pragma: export
+#include "paimon/statistics_mode.h"              // IWYU pragma: export
+#include "paimon/status.h"                       // IWYU pragma: export
+#include "paimon/table/source/table_read.h"      // IWYU pragma: export
+#include "paimon/table/source/table_scan.h"      // IWYU pragma: export
+#include "paimon/write_context.h"                // IWYU pragma: export
 
 // IWYU pragma: begin_exports
 #include "paimon/realtime/realtime_context.h"
diff --git a/include/paimon/snapshot/snapshot_file_scan.h 
b/include/paimon/snapshot/snapshot_file_scan.h
new file mode 100644
index 00000000..7283b2cb
--- /dev/null
+++ b/include/paimon/snapshot/snapshot_file_scan.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 <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <set>
+#include <string>
+
+#include "paimon/result.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class Executor;
+class FileSystem;
+class MemoryPool;
+class ScanFilter;
+
+/// Lists the physical files required by a table snapshot.
+class PAIMON_EXPORT SnapshotFileScan {
+ public:
+    SnapshotFileScan() = delete;
+    ~SnapshotFileScan() = delete;
+
+    /// List the physical files required to materialize a snapshot.
+    ///
+    /// The result includes the snapshot and schema files, manifest lists and 
manifest files,
+    /// live data and changelog files, external file indexes, the index 
manifest and its live index
+    /// files, statistics, and real-time offset files. Mutable snapshot hints 
such as `LATEST` and
+    /// `EARLIEST` are not included.
+    ///
+    /// Partition and bucket filters apply only to files which belong to a 
partition or bucket.
+    /// Snapshot-level shared metadata is always returned. A manifest file is 
returned when it
+    /// cannot be pruned by manifest-level partition and bucket statistics, 
even if entry-level
+    /// filtering later removes all of its entries. Predicate filters are not 
supported.
+    ///
+    /// @param table_path Root path of the table.
+    /// @param branch Branch to list. An empty value selects the main branch. 
This parameter takes
+    ///               precedence over a branch configured in options.
+    /// @param snapshot_id Snapshot to list, or `std::nullopt` to use the 
latest snapshot.
+    /// @param scan_filter Optional partition and bucket filters. Partition 
maps use AND semantics,
+    ///                    while the vector uses OR semantics. Predicate 
filters are rejected.
+    /// @param options User options overriding options stored in the latest 
table schema.
+    /// @param file_system Optional file system implementation. If null, it is 
resolved from
+    /// options.
+    /// @param executor Optional executor used to read manifest files in 
parallel.
+    /// @param memory_pool Optional memory pool. If null, the default pool is 
used.
+    /// @return Physical file paths required by the selected snapshot and 
filters.
+    static Result<std::set<std::string>> ListFiles(
+        const std::string& table_path, const std::string& branch,
+        const std::optional<int64_t>& snapshot_id, const 
std::shared_ptr<ScanFilter>& scan_filter,
+        const std::map<std::string, std::string>& options,
+        const std::shared_ptr<FileSystem>& file_system, const 
std::shared_ptr<Executor>& executor,
+        const std::shared_ptr<MemoryPool>& memory_pool);
+};
+
+}  // namespace paimon
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index 4aca72ab..e8a47d99 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -399,6 +399,7 @@ set(PAIMON_CORE_SRCS
     core/schema/schema_validation.cpp
     core/schema/table_schema.cpp
     core/snapshot.cpp
+    core/snapshot_file_scan.cpp
     core/snapshot_info.cpp
     core/stats/simple_stats_collector.cpp
     core/stats/simple_stats_converter.cpp
@@ -896,6 +897,7 @@ if(PAIMON_BUILD_TESTS)
                     core/schema/schema_validation_test.cpp
                     core/schema/arrow_schema_validator_test.cpp
                     core/schema/table_schema_test.cpp
+                    core/snapshot_file_scan_test.cpp
                     core/snapshot_test.cpp
                     core/stats/simple_stats_evolution_test.cpp
                     core/stats/simple_stats_collector_test.cpp
diff --git a/src/paimon/core/operation/file_store_scan.h 
b/src/paimon/core/operation/file_store_scan.h
index ff53e0c9..ea963a71 100644
--- a/src/paimon/core/operation/file_store_scan.h
+++ b/src/paimon/core/operation/file_store_scan.h
@@ -213,6 +213,13 @@ class FileStoreScan {
 
     Result<std::vector<PartitionEntry>> ReadPartitionEntries() const;
 
+    /// Merge raw manifest entries into the set of currently-live files.
+    ///
+    /// Entries are deduplicated by identifier. A Delete cancels the 
corresponding Add, and
+    /// lingering Delete entries are omitted from the result.
+    static Status MergeLiveEntries(const std::vector<ManifestEntry>& 
unmerged_entries,
+                                   std::vector<ManifestEntry>* live_entries);
+
  protected:
     /// @note Keep this thread-safe.
     virtual Result<bool> FilterByStats(const ManifestEntry& entry) const = 0;
@@ -273,13 +280,6 @@ class FileStoreScan {
                                          int32_t bucket,
                                          std::vector<ManifestEntry>* 
merged_entries) const;
 
-    /// Merge raw manifest entries into the set of currently-live files. 
Entries are deduplicated
-    /// by identifier (matching Add cancels a prior or following Delete), and 
lingering Delete
-    /// entries are dropped so the caller receives Add-only output, matching 
the semantics of
-    /// `ReadAndMergeFileEntries`.
-    static Status MergeLiveEntries(const std::vector<ManifestEntry>& 
unmerged_entries,
-                                   std::vector<ManifestEntry>* live_entries);
-
     Status ReadAndMergeFileEntries(const std::vector<ManifestFileMeta>& 
manifest_metas,
                                    std::vector<ManifestEntry>* merged_entries) 
const;
 
diff --git a/src/paimon/core/snapshot_file_scan.cpp 
b/src/paimon/core/snapshot_file_scan.cpp
new file mode 100644
index 00000000..700f2028
--- /dev/null
+++ b/src/paimon/core/snapshot_file_scan.cpp
@@ -0,0 +1,412 @@
+/*
+ * 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/snapshot/snapshot_file_scan.h"
+
+#include <future>
+#include <iterator>
+#include <set>
+#include <unordered_set>
+#include <utility>
+
+#include "paimon/common/executor/future.h"
+#include "paimon/common/predicate/predicate_filter.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/path_util.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/index/index_file_handler.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/manifest/file_kind.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/commit/realtime_commit_properties.h"
+#include "paimon/core/operation/file_store_scan.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.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/defs.h"
+#include "paimon/executor.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/scan_context.h"
+#include "paimon/status.h"
+
+namespace paimon {
+namespace {
+
+struct ManifestReadResult {
+    ManifestFileMeta meta;
+    std::vector<ManifestEntry> entries;
+};
+
+class SnapshotFileCollector {
+ public:
+    SnapshotFileCollector(const std::shared_ptr<FileStorePathFactory>& 
path_factory,
+                          const std::shared_ptr<ManifestList>& manifest_list,
+                          const std::shared_ptr<ManifestFile>& manifest_file,
+                          std::unique_ptr<IndexFileHandler>&& 
index_file_handler,
+                          const std::shared_ptr<SchemaManager>& schema_manager,
+                          int64_t latest_schema_id,
+                          const std::shared_ptr<SnapshotManager>& 
snapshot_manager,
+                          const std::shared_ptr<Executor>& executor,
+                          const std::shared_ptr<arrow::Schema>& 
partition_schema,
+                          const std::shared_ptr<PredicateFilter>& 
partition_filter,
+                          const std::optional<int32_t>& bucket_id)
+        : path_factory_(path_factory),
+          manifest_list_(manifest_list),
+          manifest_file_(manifest_file),
+          index_file_handler_(std::move(index_file_handler)),
+          schema_manager_(schema_manager),
+          latest_schema_id_(latest_schema_id),
+          snapshot_manager_(snapshot_manager),
+          executor_(executor),
+          partition_schema_(partition_schema),
+          partition_filter_(partition_filter),
+          bucket_id_(bucket_id) {}
+
+    Result<std::set<std::string>> Collect(const Snapshot& snapshot) {
+        AddPath(snapshot_manager_->SnapshotPath(snapshot.Id()));
+        schema_ids_.insert(snapshot.SchemaId());
+        schema_ids_.insert(latest_schema_id_);
+
+        AddManifestList(snapshot.BaseManifestList());
+        AddManifestList(snapshot.DeltaManifestList());
+        if (snapshot.ChangelogManifestList()) {
+            AddManifestList(snapshot.ChangelogManifestList().value());
+        }
+        if (snapshot.IndexManifest()) {
+            
AddPath(path_factory_->ToManifestFilePath(snapshot.IndexManifest().value()));
+        }
+        if (snapshot.Statistics()) {
+            
AddPath(path_factory_->ToStatsFilePath(snapshot.Statistics().value()));
+        }
+        std::optional<std::string> offsets_path =
+            RealtimeCommitProperties::GetOffsetsPath(snapshot);
+        if (offsets_path) {
+            AddPath(offsets_path.value());
+        }
+
+        PAIMON_RETURN_NOT_OK(CollectDataFiles(snapshot));
+        PAIMON_RETURN_NOT_OK(CollectChangelogFiles(snapshot));
+        PAIMON_RETURN_NOT_OK(CollectIndexFiles(snapshot));
+        for (int64_t schema_id : schema_ids_) {
+            AddPath(PathUtil::JoinPath(schema_manager_->SchemaDirectory(),
+                                       "schema-" + std::to_string(schema_id)));
+        }
+
+        return std::move(paths_);
+    }
+
+ private:
+    void AddPath(const std::string& path) {
+        if (!path.empty()) {
+            paths_.insert(path);
+        }
+    }
+
+    void AddManifestList(const std::string& file_name) {
+        if (!file_name.empty()) {
+            AddPath(path_factory_->ToManifestListPath(file_name));
+        }
+    }
+
+    bool MayContainBucket(const ManifestFileMeta& meta) const {
+        if (!bucket_id_) {
+            return true;
+        }
+        const std::optional<int32_t>& min_bucket = meta.MinBucket();
+        const std::optional<int32_t>& max_bucket = meta.MaxBucket();
+        return !min_bucket || !max_bucket ||
+               (bucket_id_.value() >= min_bucket.value() &&
+                bucket_id_.value() <= max_bucket.value());
+    }
+
+    Result<bool> ShouldReadManifest(const ManifestFileMeta& meta) const {
+        if (!MayContainBucket(meta)) {
+            return false;
+        }
+        if (!partition_filter_) {
+            return true;
+        }
+        SimpleStats stats = meta.PartitionStats();
+        return partition_filter_->Test(partition_schema_,
+                                       /*row_count=*/meta.NumAddedFiles() + 
meta.NumDeletedFiles(),
+                                       stats.MinValues(), stats.MaxValues(), 
stats.NullCounts());
+    }
+
+    Status ApplyPartitionFilter(std::vector<ManifestEntry>* entries) const {
+        if (!partition_filter_) {
+            return Status::OK();
+        }
+        std::vector<ManifestEntry> filtered_entries;
+        filtered_entries.reserve(entries->size());
+        for (ManifestEntry& entry : *entries) {
+            PAIMON_ASSIGN_OR_RAISE(bool saved,
+                                   partition_filter_->Test(partition_schema_, 
entry.Partition()));
+            if (saved) {
+                filtered_entries.push_back(std::move(entry));
+            }
+        }
+        *entries = std::move(filtered_entries);
+        return Status::OK();
+    }
+
+    Result<std::vector<ManifestReadResult>> ReadManifests(
+        const std::vector<ManifestFileMeta>& metas) const {
+        std::unordered_set<std::string> submitted_files;
+        std::vector<std::future<Result<ManifestReadResult>>> futures;
+        for (const ManifestFileMeta& meta : metas) {
+            PAIMON_ASSIGN_OR_RAISE(bool should_read, ShouldReadManifest(meta));
+            if (!should_read || 
!submitted_files.insert(meta.FileName()).second) {
+                continue;
+            }
+            futures.push_back(Via(executor_.get(), [this, meta]() -> 
Result<ManifestReadResult> {
+                std::vector<ManifestEntry> entries;
+                if (bucket_id_) {
+                    PAIMON_RETURN_NOT_OK(manifest_file_->ReadBucketEntries(
+                        meta.FileName(), bucket_id_.value(), &entries));
+                } else {
+                    PAIMON_RETURN_NOT_OK(
+                        manifest_file_->Read(meta.FileName(), 
/*filter=*/nullptr, &entries));
+                }
+                PAIMON_RETURN_NOT_OK(ApplyPartitionFilter(&entries));
+                return ManifestReadResult{meta, std::move(entries)};
+            }));
+        }
+
+        std::vector<ManifestReadResult> results;
+        std::vector<Result<ManifestReadResult>> read_results = 
CollectAll(futures);
+        results.reserve(read_results.size());
+        for (Result<ManifestReadResult>& result : read_results) {
+            PAIMON_RETURN_NOT_OK(result);
+            results.push_back(std::move(result).value());
+        }
+        return results;
+    }
+
+    Status CollectDataFiles(const Snapshot& snapshot) {
+        std::vector<ManifestFileMeta> metas;
+        PAIMON_RETURN_NOT_OK(manifest_list_->ReadDataManifests(snapshot, 
&metas));
+        PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestReadResult> 
manifest_results,
+                               ReadManifests(metas));
+
+        std::vector<ManifestEntry> unmerged_entries;
+        for (ManifestReadResult& result : manifest_results) {
+            AddPath(path_factory_->ToManifestFilePath(result.meta.FileName()));
+            unmerged_entries.insert(unmerged_entries.end(),
+                                    
std::make_move_iterator(result.entries.begin()),
+                                    
std::make_move_iterator(result.entries.end()));
+        }
+
+        std::vector<ManifestEntry> live_entries;
+        PAIMON_RETURN_NOT_OK(FileStoreScan::MergeLiveEntries(unmerged_entries, 
&live_entries));
+        return CollectEntryFiles(live_entries, /*only_add=*/false);
+    }
+
+    Status CollectChangelogFiles(const Snapshot& snapshot) {
+        if (!snapshot.ChangelogManifestList()) {
+            return Status::OK();
+        }
+        std::vector<ManifestFileMeta> metas;
+        PAIMON_RETURN_NOT_OK(manifest_list_->ReadChangelogManifests(snapshot, 
&metas));
+        PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestReadResult> 
manifest_results,
+                               ReadManifests(metas));
+
+        for (ManifestReadResult& result : manifest_results) {
+            AddPath(path_factory_->ToManifestFilePath(result.meta.FileName()));
+            PAIMON_RETURN_NOT_OK(CollectEntryFiles(result.entries, 
/*only_add=*/true));
+        }
+        return Status::OK();
+    }
+
+    Status CollectEntryFiles(const std::vector<ManifestEntry>& entries, bool 
only_add) {
+        for (const ManifestEntry& entry : entries) {
+            if (only_add && !(entry.Kind() == FileKind::Add())) {
+                continue;
+            }
+            schema_ids_.insert(entry.File()->schema_id);
+            PAIMON_ASSIGN_OR_RAISE(
+                std::shared_ptr<DataFilePathFactory> data_file_path_factory,
+                path_factory_->CreateDataFilePathFactory(entry.Partition(), 
entry.Bucket()));
+            for (const std::string& path : 
data_file_path_factory->CollectFiles(entry.File())) {
+                AddPath(path);
+            }
+        }
+        return Status::OK();
+    }
+
+    Status CollectIndexFiles(const Snapshot& snapshot) {
+        if (!snapshot.IndexManifest()) {
+            return Status::OK();
+        }
+        auto filter = [this](const IndexManifestEntry& entry) -> Result<bool> {
+            if (!(entry.kind == FileKind::Add()) ||
+                (bucket_id_ && entry.bucket != bucket_id_.value())) {
+                return false;
+            }
+            if (!partition_filter_) {
+                return true;
+            }
+            return partition_filter_->Test(partition_schema_, entry.partition);
+        };
+        PAIMON_ASSIGN_OR_RAISE(std::vector<IndexManifestEntry> entries,
+                               index_file_handler_->Scan(snapshot, filter));
+        for (const IndexManifestEntry& entry : entries) {
+            PAIMON_ASSIGN_OR_RAISE(
+                std::string path,
+                index_file_handler_->FilePath(entry.partition, entry.bucket, 
entry.index_file));
+            AddPath(path);
+        }
+        return Status::OK();
+    }
+
+ private:
+    std::shared_ptr<FileStorePathFactory> path_factory_;
+    std::shared_ptr<ManifestList> manifest_list_;
+    std::shared_ptr<ManifestFile> manifest_file_;
+    std::unique_ptr<IndexFileHandler> index_file_handler_;
+    std::shared_ptr<SchemaManager> schema_manager_;
+    int64_t latest_schema_id_;
+    std::shared_ptr<SnapshotManager> snapshot_manager_;
+    std::shared_ptr<Executor> executor_;
+    std::shared_ptr<arrow::Schema> partition_schema_;
+    std::shared_ptr<PredicateFilter> partition_filter_;
+    std::optional<int32_t> bucket_id_;
+    std::set<int64_t> schema_ids_;
+    std::set<std::string> paths_;
+};
+
+}  // namespace
+
+Result<std::set<std::string>> SnapshotFileScan::ListFiles(
+    const std::string& table_path, const std::string& branch,
+    const std::optional<int64_t>& snapshot_id, const 
std::shared_ptr<ScanFilter>& scan_filter,
+    const std::map<std::string, std::string>& options,
+    const std::shared_ptr<FileSystem>& file_system, const 
std::shared_ptr<Executor>& executor,
+    const std::shared_ptr<MemoryPool>& memory_pool) {
+    if (table_path.empty()) {
+        return Status::Invalid("table path is empty");
+    }
+    if (snapshot_id && snapshot_id.value() < Snapshot::FIRST_SNAPSHOT_ID) {
+        return Status::Invalid("snapshot id must be greater than or equal to 
1");
+    }
+    if (scan_filter && scan_filter->GetPredicate()) {
+        return Status::Invalid("snapshot file scan does not support predicate 
filter");
+    }
+
+    std::shared_ptr<MemoryPool> pool = memory_pool ? memory_pool : 
GetDefaultPool();
+    std::shared_ptr<Executor> final_executor = executor;
+    if (!final_executor) {
+        final_executor = CreateDefaultExecutor();
+    }
+    PAIMON_ASSIGN_OR_RAISE(CoreOptions temporary_options,
+                           CoreOptions::FromMap(options, file_system));
+    std::string normalized_branch = BranchManager::NormalizeBranch(branch);
+    auto snapshot_manager = 
std::make_shared<SnapshotManager>(temporary_options.GetFileSystem(),
+                                                              table_path, 
normalized_branch);
+
+    std::optional<Snapshot> snapshot;
+    if (snapshot_id) {
+        PAIMON_ASSIGN_OR_RAISE(Snapshot loaded_snapshot,
+                               
snapshot_manager->LoadSnapshot(snapshot_id.value()));
+        snapshot = std::move(loaded_snapshot);
+    } else {
+        PAIMON_ASSIGN_OR_RAISE(snapshot, snapshot_manager->LatestSnapshot());
+    }
+    if (!snapshot) {
+        return std::set<std::string>();
+    }
+
+    auto schema_manager = 
std::make_shared<SchemaManager>(temporary_options.GetFileSystem(),
+                                                          table_path, 
normalized_branch);
+    PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>> 
latest_schema,
+                           schema_manager->Latest());
+    if (!latest_schema) {
+        return Status::Invalid("not found latest schema");
+    }
+
+    std::map<std::string, std::string> final_options = 
latest_schema.value()->Options();
+    for (const auto& [key, value] : options) {
+        final_options[key] = value;
+    }
+    final_options[Options::BRANCH] = normalized_branch;
+    PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
+                           CoreOptions::FromMap(final_options, file_system));
+
+    std::shared_ptr<arrow::Schema> arrow_schema =
+        
DataField::ConvertDataFieldsToArrowSchema(latest_schema.value()->Fields());
+    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(
+            table_path, arrow_schema, latest_schema.value()->PartitionKeys(),
+            core_options.GetPartitionDefaultName(), 
core_options.GetFileFormat()->Identifier(),
+            core_options.DataFilePrefix(), 
core_options.LegacyPartitionNameEnabled(),
+            external_paths, global_index_external_path, 
core_options.IndexFileInDataFileDir(),
+            pool));
+
+    PAIMON_ASSIGN_OR_RAISE(
+        std::shared_ptr<ManifestList> manifest_list,
+        ManifestList::Create(core_options.GetFileSystem(), 
core_options.GetManifestFormat(),
+                             core_options.GetManifestCompression(), 
path_factory,
+                             core_options.GetCache(), pool));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::shared_ptr<arrow::Schema> partition_schema,
+        FieldMapping::GetPartitionSchema(arrow_schema, 
latest_schema.value()->PartitionKeys()));
+    std::shared_ptr<PredicateFilter> partition_filter;
+    std::optional<int32_t> bucket_id;
+    if (scan_filter) {
+        bucket_id = scan_filter->GetBucketFilter();
+        PAIMON_ASSIGN_OR_RAISE(
+            partition_filter,
+            FileStoreScan::CreatePartitionPredicate(
+                latest_schema.value()->PartitionKeys(), 
core_options.GetPartitionDefaultName(),
+                arrow_schema, scan_filter->GetPartitionFilters()));
+    }
+    PAIMON_ASSIGN_OR_RAISE(
+        std::shared_ptr<ManifestFile> manifest_file,
+        ManifestFile::Create(core_options.GetFileSystem(), 
core_options.GetManifestFormat(),
+                             core_options.GetManifestCompression(), 
path_factory,
+                             core_options.GetManifestTargetFileSize(), pool, 
core_options,
+                             partition_schema));
+    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(), pool, 
core_options));
+    auto index_file_handler = std::make_unique<IndexFileHandler>(
+        core_options.GetFileSystem(), std::move(index_manifest_file),
+        std::make_shared<IndexFilePathFactories>(path_factory),
+        core_options.DeletionVectorsBitmap64(), pool);
+
+    SnapshotFileCollector collector(path_factory, manifest_list, manifest_file,
+                                    std::move(index_file_handler), 
schema_manager,
+                                    latest_schema.value()->Id(), 
snapshot_manager, final_executor,
+                                    partition_schema, partition_filter, 
bucket_id);
+    return collector.Collect(snapshot.value());
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/snapshot_file_scan_test.cpp 
b/src/paimon/core/snapshot_file_scan_test.cpp
new file mode 100644
index 00000000..751b39aa
--- /dev/null
+++ b/src/paimon/core/snapshot_file_scan_test.cpp
@@ -0,0 +1,317 @@
+/*
+ * 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/snapshot/snapshot_file_scan.h"
+
+#include <algorithm>
+#include <initializer_list>
+#include <map>
+#include <memory>
+#include <optional>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "paimon/common/utils/string_utils.h"
+#include "paimon/defs.h"
+#include "paimon/predicate/predicate_builder.h"
+#include "paimon/scan_context.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+namespace {
+
+Result<std::set<std::string>> ListFiles(const std::string& table_path,
+                                        const std::optional<int64_t>& 
snapshot_id = std::nullopt,
+                                        const std::shared_ptr<ScanFilter>& 
scan_filter = nullptr,
+                                        const std::string& branch = "main") {
+    return SnapshotFileScan::ListFiles(table_path, branch, snapshot_id, 
scan_filter, /*options=*/{},
+                                       /*file_system=*/nullptr, 
/*executor=*/nullptr,
+                                       /*memory_pool=*/nullptr);
+}
+
+std::shared_ptr<ScanFilter> CreateFilter(
+    const std::vector<std::map<std::string, std::string>>& partition_filters,
+    const std::optional<int32_t>& bucket_filter = std::nullopt,
+    const std::shared_ptr<Predicate>& predicate = nullptr) {
+    return std::make_shared<ScanFilter>(predicate, partition_filters, 
bucket_filter);
+}
+
+std::set<std::string> ExpectedFiles(const std::string& table_path,
+                                    std::initializer_list<std::string> 
relative_paths) {
+    std::set<std::string> paths;
+    for (const std::string& relative_path : relative_paths) {
+        paths.insert(table_path + "/" + relative_path);
+    }
+    return paths;
+}
+
+}  // namespace
+
+TEST(SnapshotFileScanTest, TestLatestSnapshotAndBucketFilter) {
+    std::string table_path = GetDataDir() + "/orc/append_09.db/append_09";
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> all_files, 
ListFiles(table_path));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"f1=10/bucket-0/data-d41fd7d1-b3e4-4905-aad9-b20a780e90a2-0.orc",
+                                   
"f1=10/bucket-1/data-b9e7c41f-66e8-4dad-b25a-e6e1963becc4-0.orc",
+                                   
"f1=20/bucket-0/data-b913a160-a4d1-4084-af2a-18333c35668e-0.orc",
+                                   
"f1=20/bucket-0/data-db2b44c0-0d73-449d-82a0-4075bd2cb6e3-0.orc",
+                                   
"manifest/manifest-3a44a0da-1008-463c-914e-28d271375e24-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
+                                   
"manifest/manifest-c5904353-0236-46a2-891f-62a326dd8e5e-0",
+                                   
"manifest/manifest-f8b15cfc-437a-4d21-a6a0-e45b639ae7ed-0",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-2",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-3",
+                                   "schema/schema-0", "snapshot/snapshot-5"}),
+        all_files);
+
+    std::shared_ptr<ScanFilter> bucket_zero_filter =
+        CreateFilter(/*partition_filters=*/{}, /*bucket_filter=*/0);
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> bucket_zero_files,
+                         ListFiles(table_path, std::nullopt, 
bucket_zero_filter));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"f1=10/bucket-0/data-d41fd7d1-b3e4-4905-aad9-b20a780e90a2-0.orc",
+                                   
"f1=20/bucket-0/data-b913a160-a4d1-4084-af2a-18333c35668e-0.orc",
+                                   
"f1=20/bucket-0/data-db2b44c0-0d73-449d-82a0-4075bd2cb6e3-0.orc",
+                                   
"manifest/manifest-3a44a0da-1008-463c-914e-28d271375e24-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
+                                   
"manifest/manifest-c5904353-0236-46a2-891f-62a326dd8e5e-0",
+                                   
"manifest/manifest-f8b15cfc-437a-4d21-a6a0-e45b639ae7ed-0",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-2",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-3",
+                                   "schema/schema-0", "snapshot/snapshot-5"}),
+        bucket_zero_files);
+
+    std::shared_ptr<ScanFilter> bucket_one_filter =
+        CreateFilter(/*partition_filters=*/{}, /*bucket_filter=*/1);
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> bucket_one_files,
+                         ListFiles(table_path, std::nullopt, 
bucket_one_filter));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"f1=10/bucket-1/data-b9e7c41f-66e8-4dad-b25a-e6e1963becc4-0.orc",
+                                   
"manifest/manifest-3a44a0da-1008-463c-914e-28d271375e24-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
+                                   
"manifest/manifest-c5904353-0236-46a2-891f-62a326dd8e5e-0",
+                                   
"manifest/manifest-f8b15cfc-437a-4d21-a6a0-e45b639ae7ed-0",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-2",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-3",
+                                   "schema/schema-0", "snapshot/snapshot-5"}),
+        bucket_one_files);
+}
+
+TEST(SnapshotFileScanTest, TestExplicitSnapshot) {
+    std::string table_path = GetDataDir() + "/orc/append_09.db/append_09";
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files, ListFiles(table_path, 
/*snapshot_id=*/1));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"f1=10/bucket-0/data-d41fd7d1-b3e4-4905-aad9-b20a780e90a2-0.orc",
+                                   
"f1=10/bucket-1/data-4e30d6c0-f109-4300-a010-4ba03047dd9d-0.orc",
+                                   
"f1=20/bucket-0/data-db2b44c0-0d73-449d-82a0-4075bd2cb6e3-0.orc",
+                                   
"manifest/manifest-f8b15cfc-437a-4d21-a6a0-e45b639ae7ed-0",
+                                   
"manifest/manifest-list-616d1847-a02c-495f-9cca-2c8b7def0fec-0",
+                                   
"manifest/manifest-list-616d1847-a02c-495f-9cca-2c8b7def0fec-1",
+                                   "schema/schema-0", "snapshot/snapshot-1"}),
+        files);
+}
+
+TEST(SnapshotFileScanTest, TestExplicitSnapshotIncludesLatestSchema) {
+    std::string table_path =
+        GetDataDir() + 
"/orc/append_table_with_alter_table.db/append_table_with_alter_table";
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files, ListFiles(table_path, 
/*snapshot_id=*/1));
+    ASSERT_EQ(
+        ExpectedFiles(table_path,
+                      
{"key0=0/key1=1/bucket-0/data-2190cec3-ce87-4175-8d19-9268becf4440-0.orc",
+                       
"key0=1/key1=1/bucket-0/data-492ed5ab-4740-4e93-8a0a-79a6893b1770-0.orc",
+                       
"manifest/manifest-f2299c3d-c3f1-400f-ad3d-124e3a342389-0",
+                       
"manifest/manifest-list-83964df9-8a98-4f91-a4e9-05f3e07be3f9-0",
+                       
"manifest/manifest-list-83964df9-8a98-4f91-a4e9-05f3e07be3f9-1",
+                       "schema/schema-0", "schema/schema-1", 
"snapshot/snapshot-1"}),
+        files);
+}
+
+TEST(SnapshotFileScanTest, TestExternalFileIndex) {
+    std::string table_path =
+        GetDataDir() + 
"/orc/append_with_bloomfilter.db/append_with_bloomfilter";
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files, ListFiles(table_path));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"bucket-0/data-34e8acb2-110b-4c32-9dc6-d6435178d0ad-0.orc",
+                                   
"bucket-0/data-34e8acb2-110b-4c32-9dc6-d6435178d0ad-0.orc.index",
+                                   
"manifest/manifest-7a45ff6a-e225-4f4c-8141-dc5c88a2a81f-0",
+                                   
"manifest/manifest-list-13cc1371-7074-42aa-83c4-94ce8b2819c3-0",
+                                   
"manifest/manifest-list-13cc1371-7074-42aa-83c4-94ce8b2819c3-1",
+                                   "schema/schema-0", "snapshot/snapshot-1"}),
+        files);
+}
+
+TEST(SnapshotFileScanTest, TestExternalDataPath) {
+    std::string table_path = GetDataDir() +
+                             "/orc/pk_dv_index_not_in_data_with_external.db/"
+                             "pk_dv_index_not_in_data_with_external";
+    std::shared_ptr<ScanFilter> scan_filter =
+        CreateFilter(/*partition_filters=*/{{{"f1", "20"}}}, 
/*bucket_filter=*/0);
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files,
+                         ListFiles(table_path, /*snapshot_id=*/1, 
scan_filter));
+    const std::string external_file_suffix =
+        
"/external/f1=20/bucket-0/data-8b1ebe94-3177-4805-b74b-ae5e1bb1086f-0.orc";
+    auto external_file =
+        std::find_if(files.begin(), files.end(), [&external_file_suffix](const 
std::string& path) {
+            return StringUtils::EndsWith(path, external_file_suffix);
+        });
+    ASSERT_NE(files.end(), external_file);
+    files.erase(external_file);
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"manifest/manifest-0dff6454-a796-469a-8452-81a7601d1b34-0",
+                                   
"manifest/manifest-list-0ef45e5c-7b8b-42f6-b3a1-6d16bc9f522a-0",
+                                   
"manifest/manifest-list-0ef45e5c-7b8b-42f6-b3a1-6d16bc9f522a-1",
+                                   "schema/schema-0", "snapshot/snapshot-1"}),
+        files);
+}
+
+TEST(SnapshotFileScanTest, TestPartitionAndBucketFilter) {
+    std::string table_path = GetDataDir() + "/orc/append_09.db/append_09";
+    std::shared_ptr<ScanFilter> scan_filter =
+        CreateFilter(/*partition_filters=*/{{{"f1", "10"}}}, 
/*bucket_filter=*/0);
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files,
+                         ListFiles(table_path, std::nullopt, scan_filter));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"f1=10/bucket-0/data-d41fd7d1-b3e4-4905-aad9-b20a780e90a2-0.orc",
+                                   
"manifest/manifest-3a44a0da-1008-463c-914e-28d271375e24-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-0",
+                                   
"manifest/manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
+                                   
"manifest/manifest-c5904353-0236-46a2-891f-62a326dd8e5e-0",
+                                   
"manifest/manifest-f8b15cfc-437a-4d21-a6a0-e45b639ae7ed-0",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-2",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-3",
+                                   "schema/schema-0", "snapshot/snapshot-5"}),
+        files);
+}
+
+TEST(SnapshotFileScanTest, TestPartitionFilterWithoutMatchingEntries) {
+    std::string table_path = GetDataDir() + "/orc/append_09.db/append_09";
+    std::shared_ptr<ScanFilter> scan_filter = 
CreateFilter(/*partition_filters=*/{{{"f1", "999"}}});
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files,
+                         ListFiles(table_path, std::nullopt, scan_filter));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-2",
+                                   
"manifest/manifest-list-f2d59cb8-3ec6-4860-b34b-050b1a533416-3",
+                                   "schema/schema-0", "snapshot/snapshot-5"}),
+        files);
+}
+
+TEST(SnapshotFileScanTest, TestDeletionVectorIndexBucketFilter) {
+    std::string table_path = GetDataDir() +
+                             "/orc/pk_table_with_dv_cardinality.db/"
+                             "pk_table_with_dv_cardinality";
+
+    std::shared_ptr<ScanFilter> bucket_zero_filter =
+        CreateFilter(/*partition_filters=*/{}, /*bucket_filter=*/0);
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> bucket_zero_files,
+                         ListFiles(table_path, /*snapshot_id=*/4, 
bucket_zero_filter));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"f1=10/bucket-0/data-0d0f29cc-63c6-4fab-a594-71bd7d06fcde-0.orc",
+                                   
"f1=10/bucket-0/data-0d0f29cc-63c6-4fab-a594-71bd7d06fcde-1.orc",
+                                   
"index/index-86356766-3238-46e6-990b-656cd7409eaa-0",
+                                   
"manifest/index-manifest-59bcb792-830f-4b57-b838-cfcc50d29266-0",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-0",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-1",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-2",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-3",
+                                   
"manifest/manifest-list-540cf68e-698e-4b66-af30-eb558d8db43d-6",
+                                   
"manifest/manifest-list-540cf68e-698e-4b66-af30-eb558d8db43d-7",
+                                   "schema/schema-0", "snapshot/snapshot-4"}),
+        bucket_zero_files);
+
+    std::shared_ptr<ScanFilter> bucket_one_filter =
+        CreateFilter(/*partition_filters=*/{}, /*bucket_filter=*/1);
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> bucket_one_files,
+                         ListFiles(table_path, /*snapshot_id=*/4, 
bucket_one_filter));
+    ASSERT_EQ(
+        ExpectedFiles(table_path, 
{"f1=10/bucket-1/data-2ffe7ae9-2cf7-41e9-944b-2065585cde31-0.orc",
+                                   
"index/index-86356766-3238-46e6-990b-656cd7409eaa-1",
+                                   
"manifest/index-manifest-59bcb792-830f-4b57-b838-cfcc50d29266-0",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-0",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-1",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-2",
+                                   
"manifest/manifest-1d9dbf4f-667b-4a05-ad5a-f7fed8908aa4-3",
+                                   
"manifest/manifest-list-540cf68e-698e-4b66-af30-eb558d8db43d-6",
+                                   
"manifest/manifest-list-540cf68e-698e-4b66-af30-eb558d8db43d-7",
+                                   "schema/schema-0", "snapshot/snapshot-4"}),
+        bucket_one_files);
+}
+
+TEST(SnapshotFileScanTest, TestGlobalIndex) {
+    std::string table_path =
+        GetDataDir() + 
"/orc/append_with_global_index.db/append_with_global_index";
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files, ListFiles(table_path, 
/*snapshot_id=*/4));
+    ASSERT_EQ(ExpectedFiles(table_path,
+                            
{"bucket-0/data-2430f01c-b947-48dc-82a8-7c60aaa348e4-0.orc",
+                             
"index/bitmap-global-index-0c950cb6-e6e9-46cd-a9a9-cfcd55f870d3.index",
+                             
"index/bitmap-global-index-21ac35d9-200a-489d-b649-ec241f832345.index",
+                             
"index/bitmap-global-index-8b1bed37-31c5-4288-8c46-3d0d30fbd302.index",
+                             
"manifest/index-manifest-795a9d8a-60bf-401d-ab02-6f13f8ca1098-0",
+                             
"manifest/manifest-65b0d403-a1bc-4157-b242-bff73c46596d-0",
+                             
"manifest/manifest-list-2bccccf8-9f5e-48f2-b706-5b33f8c3bfc0-0",
+                             
"manifest/manifest-list-2bccccf8-9f5e-48f2-b706-5b33f8c3bfc0-1",
+                             "schema/schema-0", "snapshot/snapshot-4"}),
+              files);
+}
+
+TEST(SnapshotFileScanTest, TestBranch) {
+    std::string table_path = GetDataDir() +
+                             "/orc/append_table_with_append_pt_branch.db/"
+                             "append_table_with_append_pt_branch";
+
+    ASSERT_OK_AND_ASSIGN(std::set<std::string> files,
+                         ListFiles(table_path, std::nullopt, 
/*scan_filter=*/nullptr, "test"));
+    ASSERT_EQ(
+        ExpectedFiles(table_path,
+                      {"branch/branch-test/schema/schema-0", 
"branch/branch-test/schema/schema-1",
+                       "branch/branch-test/snapshot/snapshot-2",
+                       
"manifest/manifest-4e72f3a9-4ad2-4ce3-a387-febef513ee24-0",
+                       
"manifest/manifest-529f38e6-ae67-4a61-83fa-ede384f720e4-0",
+                       
"manifest/manifest-list-10f6b65d-7580-42b7-9c56-c9c46bf2a3bd-0",
+                       
"manifest/manifest-list-10f6b65d-7580-42b7-9c56-c9c46bf2a3bd-1",
+                       
"pt=2/bucket-0/data-150a7c45-2972-4d8c-983e-a1ab2e382e6a-0.orc",
+                       
"pt=2/bucket-0/data-ea107991-e78c-445a-b225-7af07bcdf8c3-0.orc"}),
+        files);
+}
+
+TEST(SnapshotFileScanTest, TestInvalidArguments) {
+    ASSERT_NOK_WITH_MSG(ListFiles(""), "table path is empty");
+    ASSERT_NOK_WITH_MSG(ListFiles("unused", /*snapshot_id=*/0),
+                        "snapshot id must be greater than or equal to 1");
+    std::shared_ptr<Predicate> predicate =
+        PredicateBuilder::IsNull(/*field_index=*/0, /*field_name=*/"f0", 
FieldType::STRING);
+    std::shared_ptr<ScanFilter> scan_filter =
+        CreateFilter(/*partition_filters=*/{}, /*bucket_filter=*/std::nullopt, 
predicate);
+    ASSERT_NOK_WITH_MSG(ListFiles("unused", std::nullopt, scan_filter),
+                        "snapshot file scan does not support predicate 
filter");
+}
+
+}  // namespace paimon::test

Reply via email to