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 6b80445762 [core] Support bucket-first sorting for manifest files 
(#9792)
6b80445762 is described below

commit 6b804457620f991ee9fec9cf8d151b77344eab56
Author: jianguotian <[email protected]>
AuthorDate: Mon Sep 14 09:51:21 2026 +0800

    [core] Support bucket-first sorting for manifest files (#9792)
---
 .../paimon/operation/ManifestCompactDryRun.java    |   3 +-
 .../paimon/operation/ManifestFileSorter.java       | 126 +++++++++++++++++++--
 .../paimon/manifest/ManifestFileMetaTest.java      | 101 ++++++++++++++---
 .../operation/ManifestEntryRunMergeTest.java       |  74 ++++++++++++
 4 files changed, 283 insertions(+), 21 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java
index 4ab06f52bf..6f1e5f8e40 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java
@@ -119,7 +119,8 @@ public class ManifestCompactDryRun {
                         options.dataEvolutionEnabled(),
                         manifests,
                         options.manifestSortPartitionField(),
-                        partitionType);
+                        partitionType,
+                        options.bucket() > 0);
         ManifestFileSorter.ClassifyResult classifyResult =
                 ManifestFileSorter.classifyManifests(
                         manifests,
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java
index 76c5b0ef5c..51f51dabfe 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java
@@ -155,6 +155,7 @@ public class ManifestFileSorter {
             @Nullable IOManager ioManager)
             throws Exception {
         String sortPartitionField = options.manifestSortPartitionField();
+        boolean bucketed = options.bucket() > 0;
         boolean runMergeOptimizeEnabled = 
options.manifestMergeOptimizeEnabled();
         long suggestedMetaSize = options.manifestTargetSize().getBytes();
         int suggestedMinMetaCount = options.manifestMergeMinCount();
@@ -173,6 +174,7 @@ public class ManifestFileSorter {
                         manifestFile,
                         partitionType,
                         sortPartitionField,
+                        bucketed,
                         options.dataEvolutionEnabled(),
                         runMergeOptimizeEnabled,
                         suggestedMetaSize,
@@ -192,6 +194,7 @@ public class ManifestFileSorter {
                 manifestFile,
                 partitionType,
                 sortPartitionField,
+                bucketed,
                 options.dataEvolutionEnabled(),
                 runMergeOptimizeEnabled,
                 suggestedMetaSize,
@@ -215,6 +218,7 @@ public class ManifestFileSorter {
             ManifestFile manifestFile,
             RowType partitionType,
             String sortPartitionField,
+            boolean bucketed,
             boolean dataEvolutionEnabled,
             boolean runMergeOptimizeEnabled,
             long suggestedMetaSize,
@@ -238,6 +242,7 @@ public class ManifestFileSorter {
                         manifestFile,
                         partitionType,
                         sortPartitionField,
+                        bucketed,
                         dataEvolutionEnabled,
                         runMergeOptimizeEnabled,
                         suggestedMetaSize,
@@ -321,6 +326,7 @@ public class ManifestFileSorter {
             ManifestFile manifestFile,
             RowType partitionType,
             String sortPartitionField,
+            boolean bucketed,
             boolean dataEvolutionEnabled,
             boolean runMergeOptimizeEnabled,
             long suggestedMetaSize,
@@ -339,6 +345,7 @@ public class ManifestFileSorter {
                         manifestFile,
                         partitionType,
                         sortPartitionField,
+                        bucketed,
                         dataEvolutionEnabled,
                         runMergeOptimizeEnabled,
                         suggestedMetaSize,
@@ -453,6 +460,7 @@ public class ManifestFileSorter {
             ManifestFile manifestFile,
             RowType partitionType,
             String sortPartitionField,
+            boolean bucketed,
             boolean dataEvolutionEnabled,
             boolean runMergeOptimizeEnabled,
             long suggestedMetaSize,
@@ -464,7 +472,9 @@ public class ManifestFileSorter {
         boolean useRunMergeOptimize = rowIdSort && runMergeOptimizeEnabled;
 
         // Step 1: Resolve sort key. Data evolution tables prefer RowID ranges 
when available.
-        ManifestSortKey sortKey = createSortKey(rowIdSort, sortPartitionField, 
partitionType);
+        ManifestSortKey sortKey =
+                createSortKey(
+                        dataEvolutionEnabled, input, sortPartitionField, 
partitionType, bucketed);
 
         // Step 2: Classify manifests into LSM files and collect delete 
entries.
         ClassifyResult classification =
@@ -1183,14 +1193,16 @@ public class ManifestFileSorter {
             List<ManifestFileMeta> input,
             String sortPartitionField,
             RowType partitionType) {
-        return createSortKey(
-                dataEvolutionEnabled && 
ManifestFileMeta.allContainsRowId(input),
-                sortPartitionField,
-                partitionType);
+        return createSortKey(dataEvolutionEnabled, input, sortPartitionField, 
partitionType, false);
     }
 
-    private static ManifestSortKey createSortKey(
-            boolean rowIdSort, String sortPartitionField, RowType 
partitionType) {
+    static ManifestSortKey createSortKey(
+            boolean dataEvolutionEnabled,
+            List<ManifestFileMeta> input,
+            String sortPartitionField,
+            RowType partitionType,
+            boolean bucketed) {
+        boolean rowIdSort = dataEvolutionEnabled && 
ManifestFileMeta.allContainsRowId(input);
         if (rowIdSort) {
             // RowID sorting uses the configured partition field as the 
primary key when specified,
             // otherwise it uses the full partition row to preserve partition 
locality. It then
@@ -1219,6 +1231,13 @@ public class ManifestFileSorter {
         RecordComparator fieldComparator =
                 CodeGenUtils.newRecordComparator(
                         partitionType.getFieldTypes(), new int[] 
{sortFieldIndex});
+        if (bucketed) {
+            boolean compareManifestBuckets =
+                    input.stream()
+                            .allMatch(meta -> meta.minBucket() != null && 
meta.maxBucket() != null);
+            return new BucketSortKey(
+                    fieldComparator, partitionType, sortFieldIndex, 
compareManifestBuckets);
+        }
         return new PartitionSortKey(fieldComparator, partitionType, 
sortFieldIndex);
     }
 
@@ -1346,6 +1365,99 @@ public class ManifestFileSorter {
         }
     }
 
+    private static class BucketSortKey implements ManifestSortKey {
+
+        private final PartitionSortKey partitionSortKey;
+        private final InternalRow.FieldGetter sortFieldGetter;
+        private final RowType externalSortRowType;
+        private final int[] externalSortKeyFields;
+        private final int sortFieldNum;
+        private final boolean compareManifestBuckets;
+
+        private BucketSortKey(
+                RecordComparator fieldComparator,
+                RowType partitionType,
+                int sortFieldIndex,
+                boolean compareManifestBuckets) {
+            this.partitionSortKey =
+                    new PartitionSortKey(fieldComparator, partitionType, 
sortFieldIndex);
+            this.compareManifestBuckets = compareManifestBuckets;
+            DataType sortFieldType = partitionType.getTypeAt(sortFieldIndex);
+            this.sortFieldGetter = 
InternalRow.createFieldGetter(sortFieldType, sortFieldIndex);
+            this.sortFieldNum = 4;
+            this.externalSortRowType =
+                    DataTypes.ROW(
+                            DataTypes.INT(),
+                            sortFieldType,
+                            DataTypes.TINYINT(),
+                            DataTypes.STRING(),
+                            ManifestEntry.MANIFEST_ROW_TYPE);
+            this.externalSortKeyFields = createSequentialFields(sortFieldNum);
+        }
+
+        @Override
+        public int compareMin(ManifestFileMeta a, ManifestFileMeta b) {
+            if (compareManifestBuckets) {
+                int bucketComparison = Integer.compare(a.minBucket(), 
b.minBucket());
+                if (bucketComparison != 0) {
+                    return bucketComparison;
+                }
+            }
+            return partitionSortKey.compareMin(a, b);
+        }
+
+        @Override
+        public int compareMax(ManifestFileMeta a, ManifestFileMeta b) {
+            if (compareManifestBuckets) {
+                int bucketComparison = Integer.compare(a.maxBucket(), 
b.maxBucket());
+                if (bucketComparison != 0) {
+                    return bucketComparison;
+                }
+            }
+            return partitionSortKey.compareMax(a, b);
+        }
+
+        @Override
+        public boolean isAfterMax(ManifestFileMeta file, ManifestFileMeta 
maxFile) {
+            if (compareManifestBuckets) {
+                int bucketComparison = Integer.compare(file.minBucket(), 
maxFile.maxBucket());
+                if (bucketComparison != 0) {
+                    return bucketComparison > 0;
+                }
+            }
+            return partitionSortKey.isAfterMax(file, maxFile);
+        }
+
+        @Override
+        public RowType externalSortRowType() {
+            return externalSortRowType;
+        }
+
+        @Override
+        public int[] externalSortKeyFields() {
+            return externalSortKeyFields;
+        }
+
+        @Override
+        public void replaceExternalSortRow(
+                GenericRow row, ManifestEntry entry, InternalRow 
binaryManifestRow) {
+            row.setField(0, entry.bucket());
+            row.setField(1, sortFieldGetter.getFieldOrNull(entry.partition()));
+            row.setField(2, entry.kind().toByteValue());
+            row.setField(
+                    3,
+                    entry instanceof ProjectedManifestEntry
+                            ? ((ProjectedManifestEntry) 
entry).file().fileNameBinary()
+                            : 
BinaryString.fromString(entry.file().fileName()));
+            row.setField(4, binaryManifestRow);
+        }
+
+        @Override
+        public InternalRow binaryManifestRow(BinaryRow row) {
+            return row.getRow(sortFieldNum, 
ManifestEntry.MANIFEST_ROW_TYPE.getFieldCount());
+        }
+    }
+
     private static class RowIdSortKey implements RowIdEntrySortKey {
 
         @Nullable private final RecordComparator partitionComparator;
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
index c27d8c012f..898f238231 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
@@ -1286,6 +1286,60 @@ public class ManifestFileMetaTest extends 
ManifestFileMetaTestBase {
         }
     }
 
+    @Test
+    public void testManifestSortPreservesExistingOrderForUnawareBucketTable() {
+        List<ManifestFileMeta> input =
+                Arrays.asList(
+                        makeManifest(makeBucketEntry("a-3", 0, 3), 
makeBucketEntry("a-1", 0, 1)),
+                        makeManifest(makeBucketEntry("b-2", 0, 2), 
makeBucketEntry("b-0", 0, 0)));
+
+        Options testOptions = new Options();
+        testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+        testOptions.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1G");
+        testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), 
"1B");
+        List<ManifestFileMeta> merged =
+                ManifestFileMerger.merge(
+                        input,
+                        manifestFile,
+                        getPartitionType(),
+                        CoreOptions.fromMap(testOptions.toMap()));
+
+        assertEquivalentEntries(input, merged);
+        assertThat(readEntries(merged))
+                .extracting(ManifestEntry::bucket)
+                .containsExactly(1, 3, 0, 2);
+    }
+
+    @Test
+    public void testManifestSortUsesBucketAsPrimaryKeyForBucketedTable() {
+        List<ManifestFileMeta> input =
+                Arrays.asList(
+                        makeManifest(
+                                makeBucketEntry("a-b1-p1", 1, 1), 
makeBucketEntry("a-b0-p0", 0, 0)),
+                        makeManifest(
+                                makeBucketEntry("b-b1-p0", 0, 1),
+                                makeBucketEntry("b-b0-p1", 1, 0)));
+
+        Options testOptions = new Options();
+        testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+        testOptions.set(CoreOptions.BUCKET, 4);
+        testOptions.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1G");
+        testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), 
"1B");
+        List<ManifestFileMeta> merged =
+                ManifestFileMerger.merge(
+                        input,
+                        manifestFile,
+                        getPartitionType(),
+                        CoreOptions.fromMap(testOptions.toMap()));
+
+        assertEquivalentEntries(input, merged);
+        List<ManifestEntry> entries = readEntries(merged);
+        
assertThat(entries).extracting(ManifestEntry::bucket).containsExactly(0, 0, 1, 
1);
+        assertThat(entries)
+                .extracting(entry -> entry.partition().getInt(0))
+                .containsExactly(0, 1, 0, 1);
+    }
+
     @Test
     public void testManifestSortMinorCompactionRespectsMergeMinCount() {
         List<ManifestFileMeta> input = new ArrayList<>();
@@ -1392,26 +1446,31 @@ public class ManifestFileMetaTest extends 
ManifestFileMetaTestBase {
                 .isTrue();
     }
 
-    @Test
-    public void testManifestSortWithSpillableExternalSortBuffer() {
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testManifestSortWithSpillableExternalSortBuffer(boolean 
bucketed) {
         List<ManifestFileMeta> input = new ArrayList<>();
         for (int manifest = 0; manifest < 4; manifest++) {
             List<ManifestEntry> entries = new ArrayList<>();
             for (int i = 0; i < 80; i++) {
                 int partition = manifest % 2 == 0 ? 79 - i : i;
+                int bucket = Math.floorMod(manifest * 31 + i * 17, 4);
                 entries.add(
-                        makeEntry(
-                                true,
+                        makeBucketEntry(
                                 String.format(
                                         
"spill-manifest-%02d-entry-%03d-payload-padding-%040d",
                                         manifest, i, i),
-                                partition));
+                                partition,
+                                bucket));
             }
             input.add(makeManifest(entries.toArray(new ManifestEntry[0])));
         }
 
         Options testOptions = new Options();
         testOptions.set("manifest-sort.enabled", "true");
+        if (bucketed) {
+            testOptions.set(CoreOptions.BUCKET, 4);
+        }
         testOptions.set("manifest.full-compaction-threshold-size", "1B");
         testOptions.set("page-size", "1kb");
         testOptions.set("sort-spill-buffer-size", "4kb");
@@ -1425,15 +1484,25 @@ public class ManifestFileMetaTest extends 
ManifestFileMetaTestBase {
                         CoreOptions.fromMap(testOptions.toMap()));
 
         assertEquivalentEntries(input, merged);
-        for (ManifestFileMeta meta : merged) {
-            List<ManifestEntry> entries = manifestFile.read(meta.fileName(), 
meta.fileSize());
-            for (int i = 1; i < entries.size(); i++) {
-                int prevPartition = entries.get(i - 1).partition().getInt(0);
-                int currPartition = entries.get(i).partition().getInt(0);
-                assertThat(currPartition)
-                        .as("Entries within a manifest should be sorted after 
spill")
-                        .isGreaterThanOrEqualTo(prevPartition);
+        List<ManifestEntry> entries = readEntries(merged);
+        for (int i = 1; i < entries.size(); i++) {
+            ManifestEntry previous = entries.get(i - 1);
+            ManifestEntry current = entries.get(i);
+            int comparison = 0;
+            if (bucketed) {
+                comparison = Integer.compare(previous.bucket(), 
current.bucket());
+            }
+            if (comparison == 0) {
+                comparison =
+                        Integer.compare(
+                                previous.partition().getInt(0), 
current.partition().getInt(0));
             }
+            if (comparison == 0) {
+                comparison = 
previous.file().fileName().compareTo(current.file().fileName());
+            }
+            assertThat(comparison)
+                    .as("Entries should use the table's sort order after 
spill")
+                    .isLessThanOrEqualTo(0);
         }
     }
 
@@ -2708,6 +2777,12 @@ public class ManifestFileMetaTest extends 
ManifestFileMetaTestBase {
         }
     }
 
+    /** Create a ManifestEntry with an explicit bucket. */
+    private ManifestEntry makeBucketEntry(String fileName, int partition, int 
bucket) {
+        ManifestEntry entry = makeEntry(true, fileName, partition);
+        return ManifestEntry.create(entry.kind(), entry.partition(), bucket, 
240, entry.file());
+    }
+
     /** Create a ManifestEntry with a 3-field partition row (region, dt, 
hour). */
     private ManifestEntry makeMultiPartEntry(
             boolean isAdd, String fileName, int region, int dt, int hour) {
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java
index 2d08a158d5..1142f2fa16 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java
@@ -38,6 +38,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -111,6 +112,26 @@ class ManifestEntryRunMergeTest extends 
ManifestFileMetaTestBase {
                                 .collect(Collectors.toList()));
     }
 
+    @Test
+    void testBucketedManifestComparisonFallsBackForLegacyMetadata() {
+        ManifestFileMeta first = makeManifest(bucketEntry("first", 2, 0));
+        ManifestFileMeta legacy = 
copyWithoutBucketStats(makeManifest(bucketEntry("legacy", 1, 2)));
+        ManifestFileMeta last = makeManifest(bucketEntry("last", 0, 1));
+
+        ManifestFileSorter.ManifestSortKey sortKey =
+                ManifestFileSorter.createSortKey(
+                        false, Arrays.asList(first, legacy, last), null, 
partitionType, true);
+
+        assertThat(sortKey.compareMin(first, legacy)).isPositive();
+        assertThat(sortKey.compareMin(legacy, last)).isPositive();
+        assertThat(sortKey.compareMin(first, last)).isPositive();
+
+        ManifestFileSorter.ManifestSortKey bucketSortKey =
+                ManifestFileSorter.createSortKey(
+                        false, Arrays.asList(first, last), null, 
partitionType, true);
+        assertThat(bucketSortKey.compareMin(first, last)).isNegative();
+    }
+
     private ManifestEntry rowIdEntry(String fileName, long firstRowId) {
         return ManifestEntry.create(
                 FileKind.ADD,
@@ -141,6 +162,59 @@ class ManifestEntryRunMergeTest extends 
ManifestFileMetaTestBase {
                         null));
     }
 
+    private ManifestEntry bucketEntry(String fileName, int partitionValue, int 
bucket) {
+        BinaryRow entryPartition = new BinaryRow(1);
+        BinaryRowWriter writer = new BinaryRowWriter(entryPartition);
+        writer.writeInt(0, partitionValue);
+        writer.complete();
+
+        return ManifestEntry.create(
+                FileKind.ADD,
+                entryPartition,
+                bucket,
+                240,
+                DataFileMeta.create(
+                        fileName,
+                        0,
+                        1,
+                        entryPartition,
+                        entryPartition,
+                        StatsTestUtils.newEmptySimpleStats(),
+                        StatsTestUtils.newEmptySimpleStats(),
+                        0,
+                        0,
+                        0,
+                        0,
+                        Collections.emptyList(),
+                        Timestamp.fromEpochMillis(200000),
+                        0L,
+                        null,
+                        FileSource.APPEND,
+                        null,
+                        null,
+                        null,
+                        Collections.singletonList("f0"),
+                        null));
+    }
+
+    private ManifestFileMeta copyWithoutBucketStats(ManifestFileMeta meta) {
+        return new ManifestFileMeta(
+                meta.fileName(),
+                meta.fileSize(),
+                meta.numAddedFiles(),
+                meta.numDeletedFiles(),
+                meta.partitionStats(),
+                meta.schemaId(),
+                null,
+                null,
+                meta.minLevel(),
+                meta.maxLevel(),
+                meta.minRowId(),
+                meta.maxRowId(),
+                meta.totalBuckets(),
+                meta.extraFiles());
+    }
+
     @Override
     protected ManifestFile getManifestFile() {
         return manifestFile;

Reply via email to