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

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


The following commit(s) were added to refs/heads/master by this push:
     new 12635dffc8 [python] Preserve manifest order in Data Evolution splits 
(#9242)
12635dffc8 is described below

commit 12635dffc8d75a8c2698345ea943234650bbe965
Author: XiaoHongbo <[email protected]>
AuthorDate: Sun Aug 16 16:10:29 2026 +0800

    [python] Preserve manifest order in Data Evolution splits (#9242)
---
 .../read/scanner/data_evolution_split_generator.py |  32 +++---
 .../tests/data_evolution_split_generator_test.py   | 112 +++++++++++++++++++++
 2 files changed, 125 insertions(+), 19 deletions(-)

diff --git 
a/paimon-python/pypaimon/read/scanner/data_evolution_split_generator.py 
b/paimon-python/pypaimon/read/scanner/data_evolution_split_generator.py
index fea6ab8687..5536026126 100644
--- a/paimon-python/pypaimon/read/scanner/data_evolution_split_generator.py
+++ b/paimon-python/pypaimon/read/scanner/data_evolution_split_generator.py
@@ -51,20 +51,8 @@ class DataEvolutionSplitGenerator(AbstractSplitGenerator):
         """
         Create splits for data evolution tables.
         """
-        def sort_key(manifest_entry: ManifestEntry) -> tuple:
-            first_row_id = (
-                manifest_entry.file.first_row_id
-                if manifest_entry.file.first_row_id is not None
-                else float('-inf')
-            )
-            is_blob = 1 if 
DataFileMeta.is_blob_file(manifest_entry.file.file_name) else 0
-            max_seq = manifest_entry.file.max_sequence_number
-            return first_row_id, is_blob, -max_seq
-
-        sorted_entries = sorted(file_entries, key=sort_key)
-
         partitioned_files = defaultdict(list)
-        for entry in sorted_entries:
+        for entry in file_entries:
             partitioned_files[(tuple(entry.partition.values), 
entry.bucket)].append(entry)
 
         slice_row_ranges = None  # Row ID ranges for slice-based filtering
@@ -79,11 +67,11 @@ class DataEvolutionSplitGenerator(AbstractSplitGenerator):
             return max(sum(f.file_size for f in file_list), 
self.open_file_cost)
 
         splits = []
-        for key, sorted_entries_list in partitioned_files.items():
-            if not sorted_entries_list:
+        for key, entries_list in partitioned_files.items():
+            if not entries_list:
                 continue
 
-            data_files: List[DataFileMeta] = [e.file for e in 
sorted_entries_list]
+            data_files: List[DataFileMeta] = [e.file for e in entries_list]
 
             # Split files by firstRowId for data evolution
             split_by_row_id = self._split_by_row_id(data_files)
@@ -109,7 +97,7 @@ class DataEvolutionSplitGenerator(AbstractSplitGenerator):
             ]
 
             splits += self._build_split_from_pack_for_data_evolution(
-                flatten_packed_files, packed_files, sorted_entries_list
+                flatten_packed_files, packed_files, entries_list
             )
 
         # merge slice_row_ranges and self.row_ranges
@@ -298,8 +286,14 @@ class DataEvolutionSplitGenerator(AbstractSplitGenerator):
                         filtered_blob_entries.append(entry)
                         break
             
-            # Combine filtered non-blob and blob files
-            filtered_entries = filtered_non_blob_entries + 
filtered_blob_entries
+            kept_entries = {
+                id(entry)
+                for entry in filtered_non_blob_entries + filtered_blob_entries
+            }
+            filtered_entries = [
+                entry for entry in file_entries
+                if id(entry) in kept_entries
+            ]
             
             if filtered_entries:
                 filtered_partitioned_files[key] = filtered_entries
diff --git 
a/paimon-python/pypaimon/tests/data_evolution_split_generator_test.py 
b/paimon-python/pypaimon/tests/data_evolution_split_generator_test.py
index dbb0a756cc..e36f6c8bcb 100644
--- a/paimon-python/pypaimon/tests/data_evolution_split_generator_test.py
+++ b/paimon-python/pypaimon/tests/data_evolution_split_generator_test.py
@@ -18,7 +18,11 @@
 import random
 import unittest
 
+from pypaimon.manifest.schema.data_file_meta import DataFileMeta
+from pypaimon.manifest.schema.manifest_entry import ManifestEntry
+from pypaimon.manifest.schema.simple_stats import SimpleStats
 from pypaimon.read.scanner.data_evolution_split_generator import 
DataEvolutionSplitGenerator
+from pypaimon.table.row.generic_row import GenericRow
 from pypaimon.utils.range import Range
 
 
@@ -112,5 +116,113 @@ class SplitByRowIdEquivalenceTest(unittest.TestCase):
                 _grouping(_reference_split(files)))
 
 
+class SplitOrderTest(unittest.TestCase):
+    class _Options:
+        options = {}
+
+    class _Table:
+        table_path = '/table'
+        options = None
+
+    _Table.options = _Options()
+
+    @staticmethod
+    def _entry(name, sequence, first_row_id=0, external_path=None):
+        empty_row = GenericRow([], [])
+        empty_stats = SimpleStats(empty_row, empty_row, [])
+        file = DataFileMeta.create(
+            file_name=name,
+            file_size=1,
+            row_count=10,
+            min_key=empty_row,
+            max_key=empty_row,
+            key_stats=empty_stats,
+            value_stats=empty_stats,
+            min_sequence_number=sequence,
+            max_sequence_number=sequence,
+            schema_id=0,
+            level=0,
+            extra_files=[],
+            external_path=external_path,
+            first_row_id=first_row_id,
+        )
+        return ManifestEntry(
+            kind=0,
+            partition=empty_row,
+            bucket=0,
+            total_buckets=1,
+            file=file,
+        )
+
+    def test_preserves_manifest_order_within_row_id_group(self):
+        entries = [
+            self._entry('a.parquet', 1),
+            self._entry('b.parquet', 3),
+            self._entry('c.parquet', 2),
+        ]
+        splits = DataEvolutionSplitGenerator(
+            self._Table(), target_split_size=1024, open_file_cost=0
+        ).create_splits(entries)
+
+        self.assertEqual(
+            ['a.parquet', 'b.parquet', 'c.parquet'],
+            [file.file_name for file in splits[0].files],
+        )
+
+    def test_slice_and_shard_preserve_blob_manifest_order(self):
+        entries = [
+            self._entry('a.blob', 1),
+            self._entry('b.parquet', 2),
+            self._entry('c.blob', 3),
+        ]
+        expected = ['a.blob', 'b.parquet', 'c.blob']
+
+        generators = [
+            DataEvolutionSplitGenerator(
+                self._Table(), target_split_size=1024, open_file_cost=0
+            ).with_slice(0, 5),
+            DataEvolutionSplitGenerator(
+                self._Table(), target_split_size=1024, open_file_cost=0
+            ).with_shard(0, 2),
+        ]
+        for generator in generators:
+            with self.subTest(generator=type(generator).__name__):
+                splits = generator.create_splits(entries)
+                self.assertEqual(
+                    expected,
+                    [file.file_name for file in splits[0].files],
+                )
+
+    def test_slice_and_shard_distinguish_same_external_file_name(self):
+        entries = [
+            self._entry(
+                'same.parquet', 1, first_row_id=0,
+                external_path='s3://bucket-a/data/same.parquet',
+            ),
+            self._entry(
+                'same.parquet', 2, first_row_id=10,
+                external_path='s3://bucket-b/data/same.parquet',
+            ),
+        ]
+        expected = ['s3://bucket-a/data/same.parquet']
+
+        generators = [
+            DataEvolutionSplitGenerator(
+                self._Table(), target_split_size=1024, open_file_cost=0
+            ).with_slice(0, 10),
+            DataEvolutionSplitGenerator(
+                self._Table(), target_split_size=1024, open_file_cost=0
+            ).with_shard(0, 2),
+        ]
+        for generator in generators:
+            with self.subTest(generator=type(generator).__name__):
+                splits = generator.create_splits(entries)
+                self.assertEqual(
+                    expected,
+                    [file.external_path for split in splits
+                     for file in split.files],
+                )
+
+
 if __name__ == "__main__":
     unittest.main()

Reply via email to