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 e0db538  feat(commit): support dropping delete file stats (#190)
e0db538 is described below

commit e0db538a7ee7e930d3c1a3bea15a64f4f93c6a76
Author: Joey <[email protected]>
AuthorDate: Tue Aug 11 13:54:26 2026 +0800

    feat(commit): support dropping delete file stats (#190)
    
    * feat(commit): support dropping delete file stats
    
    Honor manifest.delete-file-drop-stats during commit scans and manifest 
entry creation while preserving statistics for ADD entries and writer restore.
    
    Co-Authored-By: Codex <[email protected]>
    AI-Model: gpt-5
    Co-Authored-By: Claude Code <[email protected]>
    
    * fix(commit): refine delete file stats support
    
    Require callers to pass drop_delete_file_stats explicitly, document the 
old-reader compatibility default, and cover PK metadata-only level upgrades end 
to end.
    
    Co-Authored-By: Codex <[email protected]>
    AI-Model: gpt-5
    
    ---------
    
    Co-authored-by: Codex <[email protected]>
    Co-authored-by: Claude Code <[email protected]>
---
 include/paimon/defs.h                              |   4 +
 src/paimon/common/defs.cpp                         |   1 +
 .../core/append/append_compact_coordinator.cpp     |   3 +
 .../append/append_compact_coordinator_test.cpp     |  17 ++-
 src/paimon/core/core_options.cpp                   |   8 ++
 src/paimon/core/core_options.h                     |   6 +
 src/paimon/core/core_options_test.cpp              |   3 +
 src/paimon/core/io/data_file_meta.cpp              |   8 ++
 src/paimon/core/io/data_file_meta.h                |   5 +
 src/paimon/core/io/data_file_meta_test.cpp         |  35 ++++++
 src/paimon/core/manifest/manifest_entry.cpp        |   4 +
 src/paimon/core/manifest/manifest_entry.h          |   5 +
 .../core/operation/abstract_file_store_write.cpp   |   4 +
 .../operation/append_only_file_store_scan_test.cpp |  42 ++++++-
 .../core/operation/commit/commit_scanner.cpp       |  19 +++-
 src/paimon/core/operation/commit/commit_scanner.h  |   4 +-
 .../operation/commit/manifest_entry_changes.cpp    |   8 +-
 .../core/operation/commit/manifest_entry_changes.h |   7 +-
 .../commit/manifest_entry_changes_test.cpp         |  56 +++++++++-
 .../core/operation/file_store_commit_impl.cpp      |  18 +--
 .../core/operation/file_store_commit_impl_test.cpp | 124 +++++++++++++++++++--
 src/paimon/core/operation/file_store_scan.cpp      |   5 +
 src/paimon/core/operation/file_store_scan.h        |   9 ++
 .../core/operation/file_system_write_restore.h     |   6 +-
 .../operation/key_value_file_store_write_test.cpp  |  54 +++++++++
 test/inte/pk_compaction_inte_test.cpp              |  57 ++++++++++
 26 files changed, 473 insertions(+), 39 deletions(-)

diff --git a/include/paimon/defs.h b/include/paimon/defs.h
index ff5b466..46b20c3 100644
--- a/include/paimon/defs.h
+++ b/include/paimon/defs.h
@@ -173,6 +173,10 @@ struct PAIMON_EXPORT Options {
     /// compaction of manifest, default value is 16MB.
     static const char MANIFEST_FULL_COMPACTION_FILE_SIZE[];
 
+    /// "manifest.delete-file-drop-stats" - Whether final DELETE manifest 
entries should omit
+    /// value statistics. Default is false only for compatibility with old 
readers.
+    static const char MANIFEST_DELETE_FILE_DROP_STATS[];
+
     /// "source.split.target-size" - Target size of a source split when 
scanning a bucket. Default
     /// value is 128MB.
     static const char SOURCE_SPLIT_TARGET_SIZE[];
diff --git a/src/paimon/common/defs.cpp b/src/paimon/common/defs.cpp
index 2d9a876..53f18c3 100644
--- a/src/paimon/common/defs.cpp
+++ b/src/paimon/common/defs.cpp
@@ -52,6 +52,7 @@ const char Options::MANIFEST_COMPRESSION[] = 
"manifest.compression";
 const char Options::MANIFEST_MERGE_MIN_COUNT[] = "manifest.merge-min-count";
 const char Options::MANIFEST_FULL_COMPACTION_FILE_SIZE[] =
     "manifest.full-compaction-threshold-size";
+const char Options::MANIFEST_DELETE_FILE_DROP_STATS[] = 
"manifest.delete-file-drop-stats";
 const char Options::SOURCE_SPLIT_TARGET_SIZE[] = "source.split.target-size";
 const char Options::SOURCE_SPLIT_OPEN_FILE_COST[] = 
"source.split.open-file-cost";
 const char Options::SCAN_SNAPSHOT_ID[] = "scan.snapshot-id";
diff --git a/src/paimon/core/append/append_compact_coordinator.cpp 
b/src/paimon/core/append/append_compact_coordinator.cpp
index 37693a5..f840e9c 100644
--- a/src/paimon/core/append/append_compact_coordinator.cpp
+++ b/src/paimon/core/append/append_compact_coordinator.cpp
@@ -244,6 +244,9 @@ Result<LinkedHashMap<BinaryRow, 
std::vector<std::shared_ptr<DataFileMeta>>>> Sca
                            CreateFileStoreScan(snapshot_manager, 
schema_manager, table_schema,
                                                arrow_schema, partition_schema, 
core_options,
                                                path_factory, scan_filter, 
executor, pool));
+    if (core_options.ManifestDeleteFileDropStats()) {
+        scan->EnableDropStats();
+    }
 
     PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileStoreScan::RawPlan> plan, 
scan->CreatePlan());
     std::vector<ManifestEntry> add_entries = plan->Files(FileKind::Add());
diff --git a/src/paimon/core/append/append_compact_coordinator_test.cpp 
b/src/paimon/core/append/append_compact_coordinator_test.cpp
index f1b82a9..2d1ff12 100644
--- a/src/paimon/core/append/append_compact_coordinator_test.cpp
+++ b/src/paimon/core/append/append_compact_coordinator_test.cpp
@@ -141,7 +141,7 @@ class AppendCompactCoordinatorTest : public ::testing::Test 
{
     }
 
     void CheckCommitMessage(const std::shared_ptr<CommitMessage>& msg, size_t 
expected_before_files,
-                            int64_t expected_total_rows) {
+                            int64_t expected_total_rows, bool 
expect_dropped_stats = false) {
         auto impl = dynamic_cast<CommitMessageImpl*>(msg.get());
         ASSERT_TRUE(impl);
         ASSERT_EQ(impl->Bucket(), 0);
@@ -154,9 +154,17 @@ class AppendCompactCoordinatorTest : public 
::testing::Test {
         int64_t total_before_rows = 0;
         for (const auto& file : compact_before) {
             total_before_rows += file->row_count;
+            if (expect_dropped_stats) {
+                ASSERT_EQ(SimpleStats::EmptyStats(), file->value_stats);
+                ASSERT_TRUE(file->value_stats_cols.has_value());
+                ASSERT_TRUE(file->value_stats_cols->empty());
+            }
         }
         ASSERT_EQ(total_before_rows, expected_total_rows);
         ASSERT_EQ(compact_after[0]->row_count, expected_total_rows);
+        if (expect_dropped_stats) {
+            ASSERT_FALSE(compact_after[0]->value_stats == 
SimpleStats::EmptyStats());
+        }
     }
 
  private:
@@ -179,6 +187,7 @@ TEST_F(AppendCompactCoordinatorTest, 
TestRunCompactsAllPartitions) {
         {Options::BUCKET, "-1"},
         {Options::FILE_SYSTEM, "local"},
         {Options::COMPACTION_MIN_FILE_NUM, "2"},
+        {Options::MANIFEST_DELETE_FILE_DROP_STATS, "true"},
     };
 
     arrow::FieldVector fields = {
@@ -246,11 +255,13 @@ TEST_F(AppendCompactCoordinatorTest, 
TestRunCompactsAllPartitions) {
     // f1=10: 2 files compacted into 1, total 7 rows
     CheckCommitMessage(compact_messages[0],
                        /*expected_before_files=*/2,
-                       /*expected_total_rows=*/7);
+                       /*expected_total_rows=*/7,
+                       /*expect_dropped_stats=*/true);
     // f1=20: 2 files compacted into 1, total 3 rows
     CheckCommitMessage(compact_messages[1],
                        /*expected_before_files=*/2,
-                       /*expected_total_rows=*/3);
+                       /*expected_total_rows=*/3,
+                       /*expect_dropped_stats=*/true);
 
     // Commit compact results
     ASSERT_OK(Commit(table_path, compact_messages));
diff --git a/src/paimon/core/core_options.cpp b/src/paimon/core/core_options.cpp
index 965c3b1..e3366cd 100644
--- a/src/paimon/core/core_options.cpp
+++ b/src/paimon/core/core_options.cpp
@@ -448,6 +448,7 @@ struct CoreOptions::Impl {
     int64_t write_buffer_spill_max_disk_size = 
std::numeric_limits<int64_t>::max();
 
     bool ignore_delete = false;
+    bool manifest_delete_file_drop_stats = false;
     bool write_buffer_spillable = true;
     bool write_only = false;
     bool bucket_append_ordered = false;
@@ -657,6 +658,9 @@ struct CoreOptions::Impl {
         // Parse manifest.full-compaction-threshold-size - size threshold for 
full compaction
         
PAIMON_RETURN_NOT_OK(parser.ParseMemorySize(Options::MANIFEST_FULL_COMPACTION_FILE_SIZE,
                                                     
&manifest_full_compaction_file_size));
+        // Parse manifest.delete-file-drop-stats - drop stats from DELETE 
entries, default false
+        
PAIMON_RETURN_NOT_OK(parser.Parse(Options::MANIFEST_DELETE_FILE_DROP_STATS,
+                                          &manifest_delete_file_drop_stats));
         return Status::OK();
     }
 
@@ -1172,6 +1176,10 @@ int64_t 
CoreOptions::GetManifestFullCompactionThresholdSize() const {
     return impl_->manifest_full_compaction_file_size;
 }
 
+bool CoreOptions::ManifestDeleteFileDropStats() const {
+    return impl_->manifest_delete_file_drop_stats;
+}
+
 const std::string& CoreOptions::GetManifestCompression() const {
     return impl_->manifest_compression;
 }
diff --git a/src/paimon/core/core_options.h b/src/paimon/core/core_options.h
index 3367f45..ebf4ece 100644
--- a/src/paimon/core/core_options.h
+++ b/src/paimon/core/core_options.h
@@ -96,6 +96,12 @@ class PAIMON_EXPORT CoreOptions {
     const std::string& GetManifestCompression() const;
     int32_t GetManifestMergeMinCount() const;
     int64_t GetManifestFullCompactionThresholdSize() const;
+
+    /// Return whether final DELETE manifest entries should omit value 
statistics.
+    ///
+    /// @return True when DELETE entries should omit value statistics.
+    bool ManifestDeleteFileDropStats() const;
+
     int64_t GetSourceSplitTargetSize() const;
     int64_t GetSourceSplitOpenFileCost() const;
     std::optional<int64_t> GetScanSnapshotId() const;
diff --git a/src/paimon/core/core_options_test.cpp 
b/src/paimon/core/core_options_test.cpp
index 0fea674..ab6fc69 100644
--- a/src/paimon/core/core_options_test.cpp
+++ b/src/paimon/core/core_options_test.cpp
@@ -62,6 +62,7 @@ TEST(CoreOptionsTest, TestDefaultValue) {
     ASSERT_EQ(8 * 1024 * 1024L, core_options.GetManifestTargetFileSize());
     ASSERT_EQ(16 * 1024 * 1024L, 
core_options.GetManifestFullCompactionThresholdSize());
     ASSERT_EQ(30, core_options.GetManifestMergeMinCount());
+    ASSERT_FALSE(core_options.ManifestDeleteFileDropStats());
     ASSERT_EQ(0, core_options.GetScanManifestEntryCacheMaxSnapshots());
     ASSERT_EQ(nullptr, core_options.GetCache());
     ASSERT_EQ(128 * 1024 * 1024L, core_options.GetSourceSplitTargetSize());
@@ -194,6 +195,7 @@ TEST(CoreOptionsTest, TestFromMap) {
         {Options::MANIFEST_TARGET_FILE_SIZE, "16MB"},
         {Options::MANIFEST_FULL_COMPACTION_FILE_SIZE, "32MB"},
         {Options::MANIFEST_MERGE_MIN_COUNT, "2"},
+        {Options::MANIFEST_DELETE_FILE_DROP_STATS, "true"},
         {Options::SOURCE_SPLIT_TARGET_SIZE, "24MB"},
         {Options::SOURCE_SPLIT_OPEN_FILE_COST, "32MB"},
         {Options::READ_BATCH_SIZE, "2048"},
@@ -328,6 +330,7 @@ TEST(CoreOptionsTest, TestFromMap) {
     ASSERT_EQ(16 * 1024 * 1024L, core_options.GetManifestTargetFileSize());
     ASSERT_EQ(32 * 1024 * 1024L, 
core_options.GetManifestFullCompactionThresholdSize());
     ASSERT_EQ(2, core_options.GetManifestMergeMinCount());
+    ASSERT_TRUE(core_options.ManifestDeleteFileDropStats());
     ASSERT_EQ(nullptr, core_options.GetCache());
     ASSERT_EQ(24 * 1024 * 1024L, core_options.GetSourceSplitTargetSize());
     ASSERT_EQ(32 * 1024 * 1024L, core_options.GetSourceSplitOpenFileCost());
diff --git a/src/paimon/core/io/data_file_meta.cpp 
b/src/paimon/core/io/data_file_meta.cpp
index 86b4d62..7ce767f 100644
--- a/src/paimon/core/io/data_file_meta.cpp
+++ b/src/paimon/core/io/data_file_meta.cpp
@@ -96,6 +96,14 @@ std::shared_ptr<DataFileMeta> 
DataFileMeta::CopyWithExtraFiles(
         first_row_id, write_cols);
 }
 
+std::shared_ptr<DataFileMeta> DataFileMeta::CopyWithoutStats() const {
+    return std::make_shared<DataFileMeta>(
+        file_name, file_size, row_count, min_key, max_key, key_stats, 
SimpleStats::EmptyStats(),
+        min_sequence_number, max_sequence_number, schema_id, level, 
extra_files, creation_time,
+        delete_row_count, embedded_index, file_source, 
std::vector<std::string>(), external_path,
+        first_row_id, write_cols);
+}
+
 DataFileMeta::DataFileMeta(
     const std::string& _file_name, int64_t _file_size, int64_t _row_count,
     const BinaryRow& _min_key, const BinaryRow& _max_key, const SimpleStats& 
_key_stats,
diff --git a/src/paimon/core/io/data_file_meta.h 
b/src/paimon/core/io/data_file_meta.h
index 443e9c4..98aaa0e 100644
--- a/src/paimon/core/io/data_file_meta.h
+++ b/src/paimon/core/io/data_file_meta.h
@@ -83,6 +83,11 @@ struct DataFileMeta {
     std::shared_ptr<DataFileMeta> CopyWithExtraFiles(
         const std::vector<std::optional<std::string>>& new_extra_files) const;
 
+    /// Create a copy without value statistics. All other metadata is 
preserved.
+    ///
+    /// @return A new metadata object with empty value statistics and 
value-stat columns.
+    std::shared_ptr<DataFileMeta> CopyWithoutStats() const;
+
     std::optional<int64_t> AddRowCount() const {
         return delete_row_count == std::nullopt ? std::optional<int64_t>()
                                                 : row_count - 
delete_row_count.value();
diff --git a/src/paimon/core/io/data_file_meta_test.cpp 
b/src/paimon/core/io/data_file_meta_test.cpp
index 78b27b8..f7283f1 100644
--- a/src/paimon/core/io/data_file_meta_test.cpp
+++ b/src/paimon/core/io/data_file_meta_test.cpp
@@ -20,9 +20,44 @@
 
 #include "gtest/gtest.h"
 #include "paimon/status.h"
+#include "paimon/testing/utils/binary_row_generator.h"
 #include "paimon/testing/utils/testharness.h"
 
 namespace paimon::test {
+TEST(DataFileMetaTest, TestCopyWithoutStats) {
+    std::shared_ptr<MemoryPool> pool = GetDefaultPool();
+    SimpleStats value_stats = BinaryRowGenerator::GenerateStats(
+        {1, std::string("a")}, {5, std::string("z")}, {0, 1}, pool.get());
+    auto file_meta = std::make_shared<DataFileMeta>(
+        "data-0.orc", /*file_size=*/645, /*row_count=*/5, 
BinaryRow::EmptyRow(),
+        BinaryRow::EmptyRow(), SimpleStats::EmptyStats(), value_stats,
+        /*min_sequence_number=*/0, /*max_sequence_number=*/4, /*schema_id=*/0,
+        /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+        /*creation_time=*/Timestamp(1737111915429ll, 0),
+        /*delete_row_count=*/2, /*embedded_index=*/nullptr, 
FileSource::Append(),
+        /*value_stats_cols=*/std::vector<std::string>({"f0", "f1"}),
+        /*external_path=*/"file:/tmp/bucket-0/data-0.orc", 
/*first_row_id=*/100,
+        /*write_cols=*/std::vector<std::string>({"f0"}));
+
+    std::shared_ptr<DataFileMeta> result = file_meta->CopyWithoutStats();
+
+    ASSERT_NE(file_meta.get(), result.get());
+    DataFileMeta expected = *file_meta;
+    expected.value_stats = SimpleStats::EmptyStats();
+    expected.value_stats_cols = std::vector<std::string>();
+    ASSERT_EQ(expected, *result);
+    ASSERT_EQ(value_stats, file_meta->value_stats);
+    ASSERT_EQ(std::vector<std::string>({"f0", "f1"}), 
file_meta->value_stats_cols.value());
+
+    // Upgrade cannot restore stats once they have been dropped. Writer 
restore must therefore
+    // retain full stats for metadata-only ADD entries; see the Paimon Java 
bug at
+    // https://github.com/apache/paimon/issues/7026.
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<DataFileMeta> upgraded, 
result->Upgrade(/*new_level=*/1));
+    ASSERT_EQ(SimpleStats::EmptyStats(), upgraded->value_stats);
+    ASSERT_TRUE(upgraded->value_stats_cols.has_value());
+    ASSERT_TRUE(upgraded->value_stats_cols->empty());
+}
+
 TEST(DataFileMetaTest, TestAddRowCount) {
     DataFileMeta file_meta("data-80110e15-97b5-4bcf-ac09-6ca2659a4950-0.orc", 
/*file_size=*/645,
                            /*row_count=*/5, BinaryRow::EmptyRow(), 
BinaryRow::EmptyRow(),
diff --git a/src/paimon/core/manifest/manifest_entry.cpp 
b/src/paimon/core/manifest/manifest_entry.cpp
index c0fbac0..849407b 100644
--- a/src/paimon/core/manifest/manifest_entry.cpp
+++ b/src/paimon/core/manifest/manifest_entry.cpp
@@ -24,6 +24,10 @@ class DataType;
 }  // namespace arrow
 
 namespace paimon {
+ManifestEntry ManifestEntry::CopyWithoutStats() const {
+    return ManifestEntry(kind_, partition_, bucket_, total_buckets_, 
file_->CopyWithoutStats());
+}
+
 const std::shared_ptr<arrow::DataType>& ManifestEntry::DataType() {
     static std::shared_ptr<arrow::DataType> data_type =
         arrow::struct_({arrow::field("_KIND", arrow::int8(), 
/*nullable=*/false),
diff --git a/src/paimon/core/manifest/manifest_entry.h 
b/src/paimon/core/manifest/manifest_entry.h
index e39fa51..ca6e8ff 100644
--- a/src/paimon/core/manifest/manifest_entry.h
+++ b/src/paimon/core/manifest/manifest_entry.h
@@ -130,6 +130,11 @@ class ManifestEntry : public FileEntry {
         return file_;
     }
 
+    /// Create a copy whose data file has empty value statistics.
+    ///
+    /// @return A new manifest entry preserving all metadata except value 
statistics.
+    ManifestEntry CopyWithoutStats() const;
+
     bool operator==(const ManifestEntry& other) const {
         if (this == &other) {
             return true;
diff --git a/src/paimon/core/operation/abstract_file_store_write.cpp 
b/src/paimon/core/operation/abstract_file_store_write.cpp
index d7dafa1..653e6d9 100644
--- a/src/paimon/core/operation/abstract_file_store_write.cpp
+++ b/src/paimon/core/operation/abstract_file_store_write.cpp
@@ -302,6 +302,10 @@ Result<std::shared_ptr<RestoreFiles>> 
AbstractFileStoreWrite::ScanExistingFileMe
     if (dv_maintainer_factory_) {
         index_file_handler = dv_maintainer_factory_->GetIndexFileHandler();
     }
+    // Paimon Java currently drops value stats during writer restore. This is 
a known bug: a
+    // restored file can become a compact-after ADD via metadata-only level 
upgrade and lose its
+    // stats (https://github.com/apache/paimon/issues/7026). C++ intentionally 
does not align with
+    // that behavior; stats are dropped later only when the final entry kind 
is DELETE.
     FileSystemWriteRestore restore(snapshot_manager_, std::move(scan), 
index_file_handler);
     PAIMON_ASSIGN_OR_RAISE(
         std::shared_ptr<RestoreFiles> restore_files,
diff --git a/src/paimon/core/operation/append_only_file_store_scan_test.cpp 
b/src/paimon/core/operation/append_only_file_store_scan_test.cpp
index 1fe7533..f319498 100644
--- a/src/paimon/core/operation/append_only_file_store_scan_test.cpp
+++ b/src/paimon/core/operation/append_only_file_store_scan_test.cpp
@@ -40,6 +40,7 @@
 #include "paimon/fs/local/local_file_system.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/metrics.h"
+#include "paimon/predicate/literal.h"
 #include "paimon/predicate/predicate_builder.h"
 #include "paimon/scan_context.h"
 #include "paimon/status.h"
@@ -184,7 +185,8 @@ namespace {
 
 std::shared_ptr<FileStoreScan> BuildScan(const std::string& table_path,
                                          const std::shared_ptr<Cache>& cache,
-                                         const std::optional<int32_t>& bucket 
= std::nullopt) {
+                                         const std::optional<int32_t>& bucket 
= std::nullopt,
+                                         const std::shared_ptr<Predicate>& 
predicate = nullptr) {
     ScanContextBuilder context_builder(table_path);
     context_builder.AddOption(Options::FILE_FORMAT, "orc")
         .AddOption(Options::MANIFEST_FORMAT, "orc")
@@ -193,6 +195,9 @@ std::shared_ptr<FileStoreScan> BuildScan(const std::string& 
table_path,
     if (bucket) {
         context_builder.SetBucketFilter(bucket.value());
     }
+    if (predicate) {
+        context_builder.SetPredicate(predicate);
+    }
     EXPECT_OK_AND_ASSIGN(auto scan_context, context_builder.Finish());
     EXPECT_OK_AND_ASSIGN(auto table_scan, 
TableScan::Create(std::move(scan_context)));
     auto typed_table_scan = dynamic_cast<AbstractTableScan*>(table_scan.get());
@@ -202,6 +207,41 @@ std::shared_ptr<FileStoreScan> BuildScan(const 
std::string& table_path,
 
 }  // namespace
 
+TEST(AppendOnlyFileStoreScanTest, TestDropStatsAfterFiltering) {
+    TimezoneGuard guard("Asia/Shanghai");
+    std::string table_path = paimon::test::GetDataDir() + 
"/orc/append_09.db/append_09/";
+    std::shared_ptr<Predicate> predicate = PredicateBuilder::Equal(
+        /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
+        Literal(FieldType::STRING, "David", 5));
+
+    std::shared_ptr<FileStoreScan> scan_with_stats =
+        BuildScan(table_path, /*cache=*/nullptr, /*bucket=*/std::nullopt, 
predicate);
+    ASSERT_OK_AND_ASSIGN(Snapshot snapshot,
+                         
scan_with_stats->GetSnapshotManager()->LoadSnapshot(/*snapshot_id=*/3));
+    scan_with_stats->WithSnapshot(snapshot);
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<FileStoreScan::RawPlan> 
plan_with_stats,
+                         scan_with_stats->CreatePlan());
+    std::vector<ManifestEntry> entries_with_stats = plan_with_stats->Files();
+    ASSERT_FALSE(entries_with_stats.empty());
+
+    std::shared_ptr<FileStoreScan> scan_without_stats =
+        BuildScan(table_path, /*cache=*/nullptr, /*bucket=*/std::nullopt, 
predicate);
+    scan_without_stats->WithSnapshot(snapshot)->EnableDropStats();
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<FileStoreScan::RawPlan> 
plan_without_stats,
+                         scan_without_stats->CreatePlan());
+    std::vector<ManifestEntry> entries_without_stats = 
plan_without_stats->Files();
+
+    ASSERT_EQ(entries_with_stats.size(), entries_without_stats.size());
+    for (size_t i = 0; i < entries_with_stats.size(); ++i) {
+        ASSERT_EQ(entries_with_stats[i].CreateIdentifier(),
+                  entries_without_stats[i].CreateIdentifier());
+        ASSERT_FALSE(entries_with_stats[i].File()->value_stats == 
SimpleStats::EmptyStats());
+        ASSERT_EQ(SimpleStats::EmptyStats(), 
entries_without_stats[i].File()->value_stats);
+        
ASSERT_TRUE(entries_without_stats[i].File()->value_stats_cols.has_value());
+        
ASSERT_TRUE(entries_without_stats[i].File()->value_stats_cols->empty());
+    }
+}
+
 TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCachePath) {
     TimezoneGuard guard("Asia/Shanghai");
     std::string table_path = paimon::test::GetDataDir() + 
"/orc/append_09.db/append_09/";
diff --git a/src/paimon/core/operation/commit/commit_scanner.cpp 
b/src/paimon/core/operation/commit/commit_scanner.cpp
index abee769..5590dd0 100644
--- a/src/paimon/core/operation/commit/commit_scanner.cpp
+++ b/src/paimon/core/operation/commit/commit_scanner.cpp
@@ -96,7 +96,8 @@ Result<std::vector<ManifestEntry>> 
CommitScanner::ReadAllEntriesFromChangedParti
     PAIMON_ASSIGN_OR_RAISE(partition_filters, 
ToPartitionFilters(changed_partitions));
 
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreScan> scan,
-                           NewScan(partition_filters, 
/*for_overwrite=*/false));
+                           NewScan(partition_filters, /*for_overwrite=*/false,
+                                   /*drop_stats=*/false));
     PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileStoreScan::RawPlan> plan,
                            
scan->WithSnapshot(snapshot)->WithKind(ScanMode::ALL)->CreatePlan());
     return plan->Files();
@@ -120,7 +121,9 @@ Result<std::vector<ManifestEntry>> 
CommitScanner::ReadIncrementalEntries(
             manifest_file_->Read(manifest_meta.FileName(), /*filter=*/nullptr, 
&manifest_entries));
         for (const ManifestEntry& entry : manifest_entries) {
             if (changed_partition_set.find(entry.Partition()) != 
changed_partition_set.end()) {
-                incremental_entries.push_back(entry);
+                const bool drop_stats = 
core_options_.ManifestDeleteFileDropStats() &&
+                                        entry.Kind() == FileKind::Delete();
+                incremental_entries.push_back(drop_stats ? 
entry.CopyWithoutStats() : entry);
             }
         }
     }
@@ -132,14 +135,16 @@ Result<std::vector<ManifestEntry>> 
CommitScanner::ReadAllEntriesFromPartitions(
     const Snapshot& snapshot,
     const std::vector<std::map<std::string, std::string>>& partitions) const {
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreScan> scan,
-                           NewScan(partitions, /*for_overwrite=*/false));
+                           NewScan(partitions, /*for_overwrite=*/false,
+                                   /*drop_stats=*/false));
     PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileStoreScan::RawPlan> plan,
                            
scan->WithSnapshot(snapshot)->WithKind(ScanMode::ALL)->CreatePlan());
     return plan->Files();
 }
 
 Result<std::unique_ptr<FileStoreScan>> CommitScanner::NewScan(
-    const std::vector<std::map<std::string, std::string>>& partitions, bool 
for_overwrite) const {
+    const std::vector<std::map<std::string, std::string>>& partitions, bool 
for_overwrite,
+    bool drop_stats) const {
     auto scan_filter = std::make_shared<ScanFilter>(/*predicate=*/nullptr, 
partitions,
                                                     
/*bucket_filter=*/std::nullopt);
     if (!scan_supplier_) {
@@ -147,6 +152,9 @@ Result<std::unique_ptr<FileStoreScan>> 
CommitScanner::NewScan(
     }
 
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreScan> scan, 
scan_supplier_(scan_filter));
+    if (drop_stats && core_options_.ManifestDeleteFileDropStats()) {
+        scan->EnableDropStats();
+    }
     if (for_overwrite && core_options_.GetBucket() != 
BucketModeDefine::POSTPONE_BUCKET) {
         scan->OnlyReadRealBuckets();
     }
@@ -203,7 +211,8 @@ std::shared_ptr<CommitChangesProvider> 
CommitScanner::OverwriteChangesProvider(
         changes, index_entries,
         [this, partitions](const Snapshot& snapshot) -> 
Result<std::vector<ManifestEntry>> {
             PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreScan> scan,
-                                   NewScan(partitions, 
/*for_overwrite=*/true));
+                                   NewScan(partitions, /*for_overwrite=*/true,
+                                           /*drop_stats=*/true));
             PAIMON_ASSIGN_OR_RAISE(
                 std::shared_ptr<FileStoreScan::RawPlan> plan,
                 
scan->WithSnapshot(snapshot)->WithKind(ScanMode::ALL)->CreatePlan());
diff --git a/src/paimon/core/operation/commit/commit_scanner.h 
b/src/paimon/core/operation/commit/commit_scanner.h
index 552cc64..ef6f6a9 100644
--- a/src/paimon/core/operation/commit/commit_scanner.h
+++ b/src/paimon/core/operation/commit/commit_scanner.h
@@ -97,8 +97,8 @@ class CommitScanner {
         const std::vector<BinaryRow>& changed_partitions) const;
 
     Result<std::unique_ptr<FileStoreScan>> NewScan(
-        const std::vector<std::map<std::string, std::string>>& partitions,
-        bool for_overwrite) const;
+        const std::vector<std::map<std::string, std::string>>& partitions, 
bool for_overwrite,
+        bool drop_stats) const;
 
  private:
     std::shared_ptr<SnapshotManager> snapshot_manager_;
diff --git a/src/paimon/core/operation/commit/manifest_entry_changes.cpp 
b/src/paimon/core/operation/commit/manifest_entry_changes.cpp
index 1ca54a4..6b31a93 100644
--- a/src/paimon/core/operation/commit/manifest_entry_changes.cpp
+++ b/src/paimon/core/operation/commit/manifest_entry_changes.cpp
@@ -29,8 +29,8 @@
 
 namespace paimon {
 
-ManifestEntryChanges::ManifestEntryChanges(int32_t default_num_bucket)
-    : default_num_bucket_(default_num_bucket) {}
+ManifestEntryChanges::ManifestEntryChanges(int32_t default_num_bucket, bool 
drop_delete_file_stats)
+    : default_num_bucket_(default_num_bucket), 
drop_delete_file_stats_(drop_delete_file_stats) {}
 
 Status ManifestEntryChanges::Collect(const std::shared_ptr<CommitMessage>& 
message) {
     auto commit_message = 
std::dynamic_pointer_cast<CommitMessageImpl>(message);
@@ -142,8 +142,10 @@ ManifestEntry ManifestEntryChanges::MakeEntry(
     int32_t total_buckets = commit_message->TotalBuckets() == std::nullopt
                                 ? default_num_bucket_
                                 : commit_message->TotalBuckets().value();
+    std::shared_ptr<DataFileMeta> entry_file =
+        drop_delete_file_stats_ && kind == FileKind::Delete() ? 
file->CopyWithoutStats() : file;
     return ManifestEntry(kind, commit_message->Partition(), 
commit_message->Bucket(), total_buckets,
-                         file);
+                         entry_file);
 }
 
 }  // namespace paimon
diff --git a/src/paimon/core/operation/commit/manifest_entry_changes.h 
b/src/paimon/core/operation/commit/manifest_entry_changes.h
index bafb8bf..da2329f 100644
--- a/src/paimon/core/operation/commit/manifest_entry_changes.h
+++ b/src/paimon/core/operation/commit/manifest_entry_changes.h
@@ -39,7 +39,11 @@ namespace paimon {
 /// Detailed changes from `CommitMessage`s.
 class ManifestEntryChanges {
  public:
-    explicit ManifestEntryChanges(int32_t default_num_bucket);
+    /// Create a change collector.
+    ///
+    /// @param default_num_bucket Bucket count used when a commit message 
omits it.
+    /// @param drop_delete_file_stats Whether DELETE data-file entries should 
omit value stats.
+    explicit ManifestEntryChanges(int32_t default_num_bucket, bool 
drop_delete_file_stats);
 
     Status Collect(const std::shared_ptr<CommitMessage>& message);
 
@@ -70,6 +74,7 @@ class ManifestEntryChanges {
 
  private:
     int32_t default_num_bucket_;
+    bool drop_delete_file_stats_;
 };
 
 }  // namespace paimon
diff --git a/src/paimon/core/operation/commit/manifest_entry_changes_test.cpp 
b/src/paimon/core/operation/commit/manifest_entry_changes_test.cpp
index 434f7b4..8680ff2 100644
--- a/src/paimon/core/operation/commit/manifest_entry_changes_test.cpp
+++ b/src/paimon/core/operation/commit/manifest_entry_changes_test.cpp
@@ -40,6 +40,7 @@
 #include "paimon/data/timestamp.h"
 #include "paimon/defs.h"
 #include "paimon/memory/bytes.h"
+#include "paimon/testing/utils/binary_row_generator.h"
 #include "paimon/testing/utils/testharness.h"
 
 namespace paimon::test {
@@ -102,7 +103,8 @@ TEST_F(ManifestEntryChangesTest, TestCollectAndSummary) {
     std::shared_ptr<CommitMessage> message = 
std::make_shared<CommitMessageImpl>(
         partition, /*bucket=*/0, /*total_buckets=*/4, data_increment, 
compact_increment);
 
-    ManifestEntryChanges changes(/*default_num_bucket=*/8);
+    ManifestEntryChanges changes(/*default_num_bucket=*/8,
+                                 /*drop_delete_file_stats=*/false);
     ASSERT_OK(changes.Collect(message));
 
     ASSERT_EQ(2u, changes.append_table_files.size());
@@ -126,6 +128,52 @@ TEST_F(ManifestEntryChangesTest, TestCollectAndSummary) {
     ASSERT_NE(std::string::npos, summary.find("2 compact index files"));
 }
 
+TEST_F(ManifestEntryChangesTest, TestDropStatsOnlyForDeleteEntries) {
+    SimpleStats value_stats =
+        BinaryRowGenerator::GenerateStats({1}, {8}, {0}, 
GetDefaultPool().get());
+    std::shared_ptr<DataFileMeta> before = std::make_shared<DataFileMeta>(
+        "compact-file", /*file_size=*/1024, /*row_count=*/8, 
DataFileMeta::EmptyMinKey(),
+        DataFileMeta::EmptyMaxKey(), SimpleStats::EmptyStats(), value_stats,
+        /*min_sequence_number=*/16, /*max_sequence_number=*/32, 
/*schema_id=*/1,
+        /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+        /*creation_time=*/Timestamp(0, 0), /*delete_row_count=*/std::nullopt,
+        /*embedded_index=*/nullptr, /*file_source=*/std::nullopt,
+        /*value_stats_cols=*/std::vector<std::string>({"f0"}),
+        /*external_path=*/std::nullopt, /*first_row_id=*/std::nullopt,
+        /*write_cols=*/std::nullopt);
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<DataFileMeta> after, 
before->Upgrade(/*new_level=*/1));
+
+    CompactIncrement compact_increment(/*compact_before=*/{before}, 
/*compact_after=*/{after},
+                                       /*changelog_files=*/{});
+    std::shared_ptr<CommitMessage> message = 
std::make_shared<CommitMessageImpl>(
+        CreateIntRow(10), /*bucket=*/0, /*total_buckets=*/4,
+        DataIncrement(/*new_files=*/{}, /*deleted_files=*/{}, 
/*changelog_files=*/{}),
+        compact_increment);
+
+    ManifestEntryChanges changes(/*default_num_bucket=*/8,
+                                 /*drop_delete_file_stats=*/true);
+    ASSERT_OK(changes.Collect(message));
+
+    ASSERT_EQ(2u, changes.compact_table_files.size());
+    const ManifestEntry& delete_entry = changes.compact_table_files[0];
+    ASSERT_EQ(FileKind::Delete(), delete_entry.Kind());
+    ASSERT_EQ(SimpleStats::EmptyStats(), delete_entry.File()->value_stats);
+    ASSERT_TRUE(delete_entry.File()->value_stats_cols.has_value());
+    ASSERT_TRUE(delete_entry.File()->value_stats_cols->empty());
+
+    const ManifestEntry& add_entry = changes.compact_table_files[1];
+    ASSERT_EQ(FileKind::Add(), add_entry.Kind());
+    ASSERT_EQ(value_stats, add_entry.File()->value_stats);
+    ASSERT_TRUE(add_entry.File()->value_stats_cols.has_value());
+    ASSERT_EQ(std::vector<std::string>({"f0"}), 
add_entry.File()->value_stats_cols.value());
+    ASSERT_EQ(value_stats, before->value_stats);
+
+    ManifestEntryChanges keep_stats(/*default_num_bucket=*/8,
+                                    /*drop_delete_file_stats=*/false);
+    ASSERT_OK(keep_stats.Collect(message));
+    ASSERT_EQ(value_stats, 
keep_stats.compact_table_files[0].File()->value_stats);
+}
+
 TEST_F(ManifestEntryChangesTest, TestHasGlobalIndexFileAdditions) {
     const BinaryRow partition = CreateIntRow(10);
 
@@ -141,14 +189,16 @@ TEST_F(ManifestEntryChangesTest, 
TestHasGlobalIndexFileAdditions) {
     std::shared_ptr<CommitMessage> message = 
std::make_shared<CommitMessageImpl>(
         partition, /*bucket=*/0, /*total_buckets=*/4, data_increment, 
compact_increment);
 
-    ManifestEntryChanges changes(/*default_num_bucket=*/8);
+    ManifestEntryChanges changes(/*default_num_bucket=*/8,
+                                 /*drop_delete_file_stats=*/false);
     ASSERT_OK(changes.Collect(message));
 
     ASSERT_TRUE(changes.HasGlobalIndexFileAdditions());
 }
 
 TEST_F(ManifestEntryChangesTest, TestCollectInvalidCommitMessageType) {
-    ManifestEntryChanges changes(/*default_num_bucket=*/8);
+    ManifestEntryChanges changes(/*default_num_bucket=*/8,
+                                 /*drop_delete_file_stats=*/false);
     std::shared_ptr<CommitMessage> invalid_message = 
std::make_shared<CommitMessage>();
     ASSERT_NOK_WITH_MSG(changes.Collect(invalid_message),
                         "fail to cast commit message to commit message impl");
diff --git a/src/paimon/core/operation/file_store_commit_impl.cpp 
b/src/paimon/core/operation/file_store_commit_impl.cpp
index d70f84e..b9d8811 100644
--- a/src/paimon/core/operation/file_store_commit_impl.cpp
+++ b/src/paimon/core/operation/file_store_commit_impl.cpp
@@ -42,7 +42,6 @@
 #include "paimon/common/utils/binary_row_partition_computer.h"
 #include "paimon/common/utils/date_time_utils.h"
 #include "paimon/common/utils/fields_comparator.h"
-#include "paimon/common/utils/options_utils.h"
 #include "paimon/common/utils/path_util.h"
 #include "paimon/common/utils/scope_guard.h"
 #include "paimon/core/catalog/catalog_snapshot_commit.h"
@@ -92,7 +91,6 @@ class MemoryPool;
 namespace {
 
 constexpr const char* kCommitStrictModeLastSafeSnapshot = 
"commit.strict-mode.last-safe-snapshot";
-constexpr const char* kManifestDeleteFileDropStats = 
"manifest.delete-file-drop-stats";
 constexpr const char* kSequenceSnapshotOrdering = "sequence.snapshot-ordering";
 constexpr const char* kPkClusteringOverride = "pk-clustering-override";
 
@@ -116,14 +114,6 @@ Status FileStoreCommitImpl::ValidateCommitOptions(const 
CoreOptions& options) {
     if (raw_options.find(kCommitStrictModeLastSafeSnapshot) != 
raw_options.end()) {
         unsupported_options.emplace_back(kCommitStrictModeLastSafeSnapshot);
     }
-    if (raw_options.find(kManifestDeleteFileDropStats) != raw_options.end()) {
-        PAIMON_ASSIGN_OR_RAISE(
-            bool manifest_delete_file_drop_stats,
-            OptionsUtils::GetValueFromMap<bool>(raw_options, 
kManifestDeleteFileDropStats));
-        if (manifest_delete_file_drop_stats) {
-            unsupported_options.emplace_back(kManifestDeleteFileDropStats);
-        }
-    }
     if (raw_options.find(kSequenceSnapshotOrdering) != raw_options.end()) {
         unsupported_options.emplace_back(kSequenceSnapshotOrdering);
     }
@@ -318,8 +308,10 @@ Result<bool> 
FileStoreCommitImpl::RollbackToAsLatest(int64_t target_snapshot_id)
     std::vector<ManifestEntry> delta_files;
     for (const auto& entry : latest_entries) {
         if (target_identifiers.find(entry.CreateIdentifier()) == 
target_identifiers.end()) {
-            delta_files.emplace_back(FileKind::Delete(), entry.Partition(), 
entry.Bucket(),
-                                     entry.TotalBuckets(), entry.File());
+            delta_files.emplace_back(
+                FileKind::Delete(), entry.Partition(), entry.Bucket(), 
entry.TotalBuckets(),
+                options_.ManifestDeleteFileDropStats() ? 
entry.File()->CopyWithoutStats()
+                                                       : entry.File());
         }
     }
     for (const auto& entry : target_entries) {
@@ -1332,7 +1324,7 @@ std::shared_ptr<ManifestCommittable> 
FileStoreCommitImpl::CreateManifestCommitta
 
 Result<ManifestEntryChanges> FileStoreCommitImpl::CollectChanges(
     const std::vector<std::shared_ptr<CommitMessage>>& commit_messages) {
-    ManifestEntryChanges changes(num_bucket_);
+    ManifestEntryChanges changes(num_bucket_, 
options_.ManifestDeleteFileDropStats());
     for (const auto& message : commit_messages) {
         PAIMON_RETURN_NOT_OK(changes.Collect(message));
     }
diff --git a/src/paimon/core/operation/file_store_commit_impl_test.cpp 
b/src/paimon/core/operation/file_store_commit_impl_test.cpp
index 309766d..fadb1dd 100644
--- a/src/paimon/core/operation/file_store_commit_impl_test.cpp
+++ b/src/paimon/core/operation/file_store_commit_impl_test.cpp
@@ -59,6 +59,7 @@
 #include "paimon/core/manifest/index_manifest_file.h"
 #include "paimon/core/manifest/manifest_committable.h"
 #include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/manifest/manifest_file.h"
 #include "paimon/core/manifest/manifest_file_meta.h"
 #include "paimon/core/manifest/manifest_list.h"
 #include "paimon/core/operation/metrics/commit_metrics.h"
@@ -2535,9 +2536,9 @@ TEST_F(FileStoreCommitImplTest, 
TestFixedBucketPKTableCommitAllowed) {
 }
 
 TEST_F(FileStoreCommitImplTest, 
ValidateCommitOptionsRejectsUnsupportedOptions) {
-    const std::vector<std::string> unsupported_keys = {
-        "commit.strict-mode.last-safe-snapshot", 
"manifest.delete-file-drop-stats",
-        "sequence.snapshot-ordering", "pk-clustering-override"};
+    const std::vector<std::string> unsupported_keys = 
{"commit.strict-mode.last-safe-snapshot",
+                                                       
"sequence.snapshot-ordering",
+                                                       
"pk-clustering-override"};
     for (const auto& key : unsupported_keys) {
         ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap({{key, 
"true"}}));
         
ASSERT_NOK_WITH_MSG(FileStoreCommitImpl::ValidateCommitOptions(options),
@@ -2550,10 +2551,119 @@ TEST_F(FileStoreCommitImplTest, 
ValidateCommitOptionsRejectsUnsupportedOptions)
     ASSERT_OK(FileStoreCommitImpl::ValidateCommitOptions(ok_options));
 }
 
-TEST_F(FileStoreCommitImplTest, 
ValidateCommitOptionsAllowsDisabledManifestDeleteFileDropStats) {
-    ASSERT_OK_AND_ASSIGN(CoreOptions options,
-                         
CoreOptions::FromMap({{"manifest.delete-file-drop-stats", "false"}}));
-    ASSERT_OK(FileStoreCommitImpl::ValidateCommitOptions(options));
+TEST_F(FileStoreCommitImplTest, 
ValidateCommitOptionsAllowsManifestDeleteFileDropStats) {
+    for (const std::string value : {"false", "true"}) {
+        ASSERT_OK_AND_ASSIGN(
+            CoreOptions options,
+            CoreOptions::FromMap({{Options::MANIFEST_DELETE_FILE_DROP_STATS, 
value}}));
+        ASSERT_OK(FileStoreCommitImpl::ValidateCommitOptions(options));
+    }
+}
+
+TEST_F(FileStoreCommitImplTest, TestGetAllFilesKeepsValueStats) {
+    CommitContextBuilder context_builder(table_path_, "commit_user_1");
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context,
+                         context_builder.AddOption(Options::MANIFEST_FORMAT, 
"orc")
+                             .AddOption(Options::MANIFEST_TARGET_FILE_SIZE, 
"8mb")
+                             
.AddOption(Options::MANIFEST_DELETE_FILE_DROP_STATS, "true")
+                             .AddOption(Options::FILE_SYSTEM, "local")
+                             .Finish());
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
+                         FileStoreCommit::Create(std::move(commit_context)));
+    auto commit_impl = dynamic_cast<FileStoreCommitImpl*>(commit.get());
+    ASSERT_TRUE(commit_impl);
+
+    std::vector<std::shared_ptr<CommitMessage>> messages =
+        GetCommitMessages(paimon::test::GetDataDir() +
+                              
"/orc/append_09.db/append_09/commit_messages/commit_messages-01",
+                          /*version=*/3);
+    ASSERT_OK(commit_impl->Commit(messages, /*commit_identifier=*/1));
+
+    ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> latest,
+                         commit_impl->snapshot_manager_->LatestSnapshot());
+    ASSERT_TRUE(latest.has_value());
+    ASSERT_OK_AND_ASSIGN(std::vector<ManifestEntry> entries,
+                         commit_impl->GetAllFiles(latest.value(), 
/*partitions=*/{}));
+    ASSERT_FALSE(entries.empty());
+    for (const ManifestEntry& entry : entries) {
+        ASSERT_FALSE(entry.File()->value_stats == SimpleStats::EmptyStats());
+    }
+}
+
+TEST_F(FileStoreCommitImplTest, TestOverwriteDropsDeleteFileStats) {
+    CommitContextBuilder context_builder(table_path_, "commit_user_1");
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context,
+                         context_builder.AddOption(Options::MANIFEST_FORMAT, 
"orc")
+                             .AddOption(Options::MANIFEST_TARGET_FILE_SIZE, 
"8mb")
+                             
.AddOption(Options::MANIFEST_DELETE_FILE_DROP_STATS, "true")
+                             .AddOption(Options::FILE_SYSTEM, "local")
+                             .Finish());
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
+                         FileStoreCommit::Create(std::move(commit_context)));
+    auto commit_impl = dynamic_cast<FileStoreCommitImpl*>(commit.get());
+    ASSERT_TRUE(commit_impl);
+
+    std::vector<std::shared_ptr<CommitMessage>> first_commit =
+        GetCommitMessages(paimon::test::GetDataDir() +
+                              
"/orc/append_09.db/append_09/commit_messages/commit_messages-01",
+                          /*version=*/3);
+    ASSERT_OK(commit_impl->Commit(first_commit, /*commit_identifier=*/1));
+
+    std::vector<std::shared_ptr<CommitMessage>> overwrite_commit =
+        GetCommitMessages(paimon::test::GetDataDir() +
+                              
"/orc/append_09.db/append_09/commit_messages/commit_messages-02",
+                          /*version=*/3);
+    ASSERT_OK(commit_impl->Overwrite({}, overwrite_commit, 
/*commit_identifier=*/2));
+
+    ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> latest,
+                         commit_impl->snapshot_manager_->LatestSnapshot());
+    ASSERT_TRUE(latest.has_value());
+    std::vector<ManifestFileMeta> delta_manifests;
+    ASSERT_OK(commit_impl->manifest_list_->ReadDeltaManifests(latest.value(), 
&delta_manifests));
+    std::vector<ManifestEntry> delta_entries;
+    for (const ManifestFileMeta& manifest : delta_manifests) {
+        ASSERT_OK(commit_impl->manifest_file_->Read(manifest.FileName(), 
/*filter=*/nullptr,
+                                                    &delta_entries));
+    }
+
+    int32_t add_count = 0;
+    int32_t delete_count = 0;
+    for (const ManifestEntry& entry : delta_entries) {
+        if (entry.Kind() == FileKind::Delete()) {
+            ++delete_count;
+            ASSERT_EQ(SimpleStats::EmptyStats(), entry.File()->value_stats);
+            ASSERT_TRUE(entry.File()->value_stats_cols.has_value());
+            ASSERT_TRUE(entry.File()->value_stats_cols->empty());
+        } else if (entry.Kind() == FileKind::Add()) {
+            ++add_count;
+            ASSERT_FALSE(entry.File()->value_stats == 
SimpleStats::EmptyStats());
+        }
+    }
+    ASSERT_GT(delete_count, 0);
+    ASSERT_GT(add_count, 0);
+
+    std::vector<BinaryRow> changed_partitions;
+    changed_partitions.reserve(delta_entries.size());
+    for (const ManifestEntry& entry : delta_entries) {
+        changed_partitions.push_back(entry.Partition());
+    }
+    ASSERT_OK_AND_ASSIGN(
+        std::vector<ManifestEntry> incremental_entries,
+        commit_impl->commit_scanner_->ReadIncrementalEntries(latest.value(), 
changed_partitions));
+
+    add_count = 0;
+    delete_count = 0;
+    for (const ManifestEntry& entry : incremental_entries) {
+        if (entry.Kind() == FileKind::Delete()) {
+            ++delete_count;
+            ASSERT_EQ(SimpleStats::EmptyStats(), entry.File()->value_stats);
+        } else if (entry.Kind() == FileKind::Add()) {
+            ++add_count;
+            ASSERT_FALSE(entry.File()->value_stats == 
SimpleStats::EmptyStats());
+        }
+    }
+    ASSERT_GT(delete_count, 0);
+    ASSERT_GT(add_count, 0);
 }
 
 TEST_F(FileStoreCommitImplTest, DropPartitionWithEmptyPartitionsFails) {
diff --git a/src/paimon/core/operation/file_store_scan.cpp 
b/src/paimon/core/operation/file_store_scan.cpp
index 681e772..865e006 100644
--- a/src/paimon/core/operation/file_store_scan.cpp
+++ b/src/paimon/core/operation/file_store_scan.cpp
@@ -187,6 +187,11 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
             }
         }
     }
+    if (drop_stats_) {
+        for (ManifestEntry& entry : manifest_entries) {
+            entry = entry.CopyWithoutStats();
+        }
+    }
     const int64_t all_data_files = std::accumulate(
         all_manifest_file_metas.begin(), all_manifest_file_metas.end(), 
int64_t{0},
         [](const int64_t sum, const ManifestFileMeta& manifest_file_meta) {
diff --git a/src/paimon/core/operation/file_store_scan.h 
b/src/paimon/core/operation/file_store_scan.h
index 53d7825..669dc07 100644
--- a/src/paimon/core/operation/file_store_scan.h
+++ b/src/paimon/core/operation/file_store_scan.h
@@ -132,6 +132,14 @@ class FileStoreScan {
         return this;
     }
 
+    /// Drop value statistics from entries after all scan filters have been 
applied.
+    ///
+    /// @return This scan for chained configuration.
+    FileStoreScan* EnableDropStats() {
+        drop_stats_ = true;
+        return this;
+    }
+
     const std::shared_ptr<SnapshotManager>& GetSnapshotManager() const {
         return snapshot_manager_;
     }
@@ -310,5 +318,6 @@ class FileStoreScan {
     std::optional<Snapshot> specified_snapshot_;
     std::shared_ptr<Metrics> metrics_;
     std::string table_path_;
+    bool drop_stats_ = false;
 };
 }  // namespace paimon
diff --git a/src/paimon/core/operation/file_system_write_restore.h 
b/src/paimon/core/operation/file_system_write_restore.h
index 81bb669..630ea83 100644
--- a/src/paimon/core/operation/file_system_write_restore.h
+++ b/src/paimon/core/operation/file_system_write_restore.h
@@ -24,7 +24,6 @@
 #include <string>
 #include <vector>
 
-#include "paimon/core/core_options.h"
 #include "paimon/core/index/index_file_handler.h"
 #include "paimon/core/operation/file_store_scan.h"
 #include "paimon/core/operation/restore_files.h"
@@ -36,6 +35,11 @@ namespace paimon {
 /// `WriteRestore` to restore files directly from file system.
 class FileSystemWriteRestore : public WriteRestore {
  public:
+    /// Create a write restore backed by a file store scan.
+    ///
+    /// @param snapshot_manager Snapshot manager used to locate restore state.
+    /// @param scan Scan used to load existing files.
+    /// @param index_file_handler Handler used to restore deletion-vector 
indexes.
     FileSystemWriteRestore(const std::shared_ptr<SnapshotManager>& 
snapshot_manager,
                            std::unique_ptr<FileStoreScan>&& scan,
                            const std::shared_ptr<IndexFileHandler>& 
index_file_handler)
diff --git a/src/paimon/core/operation/key_value_file_store_write_test.cpp 
b/src/paimon/core/operation/key_value_file_store_write_test.cpp
index 60f2d95..62dfd71 100644
--- a/src/paimon/core/operation/key_value_file_store_write_test.cpp
+++ b/src/paimon/core/operation/key_value_file_store_write_test.cpp
@@ -42,6 +42,8 @@
 #include "paimon/common/table/special_fields.h"
 #include "paimon/common/utils/path_util.h"
 #include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/operation/restore_files.h"
+#include "paimon/core/stats/simple_stats.h"
 #include "paimon/core/table/sink/commit_message_impl.h"
 #include "paimon/file_store_commit.h"
 #include "paimon/file_store_write.h"
@@ -423,6 +425,58 @@ TEST_F(KeyValueFileStoreWriteTest, TestSpillSimple) {
     ASSERT_EQ(get_writer(1)->GetMemoryUsage(), 0);
 }
 
+TEST_F(KeyValueFileStoreWriteTest, TestWriterRestoreKeepsValueStats) {
+    auto fields = {arrow::field("f0", arrow::utf8(), /*nullable=*/false)};
+    arrow::Schema typed_schema(fields);
+    ::ArrowSchema schema;
+    ASSERT_TRUE(arrow::ExportSchema(typed_schema, &schema).ok());
+
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    std::map<std::string, std::string> options = {
+        {Options::BUCKET, "1"},
+        {Options::FILE_FORMAT, "orc"},
+        {Options::MANIFEST_FORMAT, "orc"},
+        {Options::MANIFEST_DELETE_FILE_DROP_STATS, "true"}};
+    ASSERT_OK_AND_ASSIGN(auto catalog, Catalog::Create(dir->Str(), options));
+    ASSERT_OK(catalog->CreateDatabase("foo", {}, /*ignore_if_exists=*/false));
+    ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar"), &schema,
+                                   /*partition_keys=*/{}, 
/*primary_keys=*/{"f0"}, options,
+                                   /*ignore_if_exists=*/false));
+
+    std::string table_path = PathUtil::JoinPath(dir->Str(), "foo.db/bar");
+    WriteContextBuilder first_context_builder(table_path, "first-writer");
+    first_context_builder.SetOptions(options);
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> first_context,
+                         first_context_builder.Finish());
+    ASSERT_OK_AND_ASSIGN(auto first_write, 
FileStoreWrite::Create(std::move(first_context)));
+    ASSERT_OK(WriteSingleStringRow(first_write.get(), /*bucket=*/0, "alice"));
+    ASSERT_OK_AND_ASSIGN(auto commit_messages,
+                         first_write->PrepareCommit(/*wait_compaction=*/false, 
1));
+    ASSERT_OK(first_write->Close());
+    ASSERT_EQ(1, commit_messages.size());
+    auto commit_message = 
std::dynamic_pointer_cast<CommitMessageImpl>(commit_messages[0]);
+    ASSERT_NE(nullptr, commit_message);
+    ASSERT_EQ(1, commit_message->GetNewFilesIncrement().NewFiles().size());
+    
ASSERT_FALSE(commit_message->GetNewFilesIncrement().NewFiles()[0]->value_stats 
==
+                 SimpleStats::EmptyStats());
+    Commit(table_path, options, commit_messages);
+
+    WriteContextBuilder restored_context_builder(table_path, 
"restored-writer");
+    restored_context_builder.SetOptions(options);
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> restored_context,
+                         restored_context_builder.Finish());
+    ASSERT_OK_AND_ASSIGN(auto restored_write, 
FileStoreWrite::Create(std::move(restored_context)));
+    auto key_value_write = 
dynamic_cast<KeyValueFileStoreWrite*>(restored_write.get());
+    ASSERT_NE(nullptr, key_value_write);
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<RestoreFiles> restore_files,
+                         
key_value_write->ScanExistingFileMetas(BinaryRow::EmptyRow(),
+                                                                /*bucket=*/0));
+    ASSERT_EQ(1, restore_files->DataFiles().size());
+    ASSERT_FALSE(restore_files->DataFiles()[0]->value_stats == 
SimpleStats::EmptyStats());
+    ASSERT_OK(restored_write->Close());
+}
+
 TEST_F(KeyValueFileStoreWriteTest, 
TestSpillDiskQuotaExhaustedFallsBackToFlushDataFile) {
     auto fields = {arrow::field("f0", arrow::utf8(), /*nullable=*/false)};
     arrow::Schema typed_schema(fields);
diff --git a/test/inte/pk_compaction_inte_test.cpp 
b/test/inte/pk_compaction_inte_test.cpp
index 8706a49..9103fbc 100644
--- a/test/inte/pk_compaction_inte_test.cpp
+++ b/test/inte/pk_compaction_inte_test.cpp
@@ -384,6 +384,63 @@ class PkCompactionInteTest : public ::testing::Test,
     arrow::FieldVector fields_;
 };
 
+TEST_F(PkCompactionInteTest, TestMetadataOnlyLevelUpgradeKeepsValueStats) {
+    arrow::FieldVector fields = {arrow::field("id", arrow::int32()),
+                                 arrow::field("value", arrow::utf8())};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MANIFEST_DELETE_FILE_DROP_STATS, "true"}};
+    CreateTable(fields, /*partition_keys=*/{}, /*primary_keys=*/{"id"}, 
options);
+    const std::string table_path = TablePath();
+
+    auto array = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
+        [1, "Alice"],
+        [2, "Bob"]
+    ])")
+                     .ValueOrDie();
+    ASSERT_OK(WriteAndCommit(table_path, /*partition=*/{}, /*bucket=*/0, array,
+                             /*commit_identifier=*/0));
+
+    ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> 
compact_messages,
+                         CompactAndCommit(table_path, /*partition=*/{}, 
/*bucket=*/0,
+                                          /*full_compaction=*/true, 
/*commit_identifier=*/1));
+    ASSERT_EQ(1u, compact_messages.size());
+    auto compact_message = 
std::dynamic_pointer_cast<CommitMessageImpl>(compact_messages[0]);
+    ASSERT_NE(nullptr, compact_message);
+    const CompactIncrement& compact_increment = 
compact_message->GetCompactIncrement();
+    ASSERT_EQ(1u, compact_increment.CompactBefore().size());
+    ASSERT_EQ(1u, compact_increment.CompactAfter().size());
+
+    const std::shared_ptr<DataFileMeta>& before = 
compact_increment.CompactBefore()[0];
+    const std::shared_ptr<DataFileMeta>& after = 
compact_increment.CompactAfter()[0];
+    ASSERT_EQ(before->file_name, after->file_name)
+        << "Full compaction must use a metadata-only level upgrade";
+    ASSERT_LT(before->level, after->level);
+    ASSERT_FALSE(before->value_stats == SimpleStats::EmptyStats());
+    ASSERT_EQ(before->value_stats, after->value_stats);
+
+    ScanContextBuilder scan_context_builder(table_path);
+    scan_context_builder.WithStreamingMode(false)
+        .AddOption(Options::FILE_SYSTEM, "local")
+        .AddOption(Options::SCAN_MODE, StartupMode::LatestFull().ToString());
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> scan_context, 
scan_context_builder.Finish());
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableScan> table_scan,
+                         TableScan::Create(std::move(scan_context)));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, table_scan->CreatePlan());
+    ASSERT_EQ(1u, plan->Splits().size());
+    auto split = std::dynamic_pointer_cast<DataSplitImpl>(plan->Splits()[0]);
+    ASSERT_NE(nullptr, split);
+    ASSERT_EQ(1u, split->DataFiles().size());
+
+    const std::shared_ptr<DataFileMeta>& active_file = split->DataFiles()[0];
+    ASSERT_EQ(after->file_name, active_file->file_name);
+    ASSERT_EQ(after->level, active_file->level);
+    ASSERT_EQ(after->value_stats, active_file->value_stats);
+    ASSERT_FALSE(active_file->value_stats == SimpleStats::EmptyStats());
+}
+
 // Verify shared-shredding MAP can be read correctly after PK full compaction.
 TEST_P(PkCompactionInteTest, 
TestKeyValueTableFullCompactionWithMapSharedShredding) {
     auto file_format = GetParam();

Reply via email to