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 11db5d1de7 [core] Support manifest sorting for non-partitioned
bucketed tables (#9808)
11db5d1de7 is described below
commit 11db5d1de7b9e7183dea1456dad0e978678d7337
Author: Jingsong Lee <[email protected]>
AuthorDate: Mon Sep 14 16:44:25 2026 +0800
[core] Support manifest sorting for non-partitioned bucketed tables (#9808)
---
docs/generated/core_configuration.html | 4 +-
.../main/java/org/apache/paimon/CoreOptions.java | 11 +-
.../operation/ManifestAdjacentSortedRun.java | 8 +-
.../paimon/operation/ManifestCompactDryRun.java | 4 +-
.../paimon/operation/ManifestFileMerger.java | 8 +-
.../paimon/operation/ManifestFileSorter.java | 95 ++++++++-------
.../org/apache/paimon/schema/SchemaValidation.java | 6 +-
.../manifest/NoPartitionManifestFileMetaTest.java | 128 +++++++++++++++++++++
.../apache/paimon/schema/SchemaValidationTest.java | 29 ++++-
9 files changed, 236 insertions(+), 57 deletions(-)
diff --git a/docs/generated/core_configuration.html
b/docs/generated/core_configuration.html
index ac57300175..16d3339f41 100644
--- a/docs/generated/core_configuration.html
+++ b/docs/generated/core_configuration.html
@@ -1063,7 +1063,7 @@ Mainly to resolve data skew on primary keys. We recommend
starting with 64 mb wh
<td><h5>manifest-sort.enabled</h5></td>
<td style="word-wrap: break-word;">false</td>
<td>Boolean</td>
- <td>Whether to invoke manifest sort rewrite during commit.<br
/>Note: enabling this changes the semantics of 'manifest.merge-min-count'. In
the sort rewrite path, small manifest files within the rewrite budget are
sorted and merged directly, so the minimum-count gate no longer prevents
merging a small number of under-budget manifest files when full compaction is
not triggered.</td>
+ <td>Whether to invoke manifest sort rewrite during commit.
Non-partitioned tables can sort by bucket with fixed or postponed buckets, or
by RowID for data evolution tables when all input manifests contain RowID
ranges.<br />Note: enabling this changes the semantics of
'manifest.merge-min-count'. In the sort rewrite path, small manifest files
within the rewrite budget are sorted and merged directly, so the minimum-count
gate no longer prevents merging a small number of under-b [...]
</tr>
<tr>
<td><h5>manifest-sort.max-rewrite-size</h5></td>
@@ -1075,7 +1075,7 @@ Mainly to resolve data skew on primary keys. We recommend
starting with 64 mb wh
<td><h5>manifest-sort.partition-field</h5></td>
<td style="word-wrap: break-word;">(none)</td>
<td>String</td>
- <td>Partition field name to sort manifest entries by. Validated by
schema validation, if not configured, defaults to the first partition
field.</td>
+ <td>Partition field name to sort manifest entries by. Validated by
schema validation; must be unset for non-partitioned tables. If not configured,
defaults to the first partition field, or all partition fields for data
evolution RowID sorting.</td>
</tr>
<tr>
<td><h5>manifest.compression</h5></td>
diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
index 7b0665c502..68215e18e4 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -568,7 +568,12 @@ public class CoreOptions implements Serializable {
.defaultValue(false)
.withDescription(
Description.builder()
- .text("Whether to invoke manifest sort
rewrite during commit.")
+ .text(
+ "Whether to invoke manifest sort
rewrite during commit."
+ + " Non-partitioned tables
can sort by bucket"
+ + " with fixed or
postponed buckets, or by RowID"
+ + " for data evolution
tables when all input"
+ + " manifests contain
RowID ranges.")
.linebreak()
.text(
"Note: enabling this changes the
semantics of '"
@@ -587,7 +592,9 @@ public class CoreOptions implements Serializable {
.noDefaultValue()
.withDescription(
"Partition field name to sort manifest entries by.
Validated by"
- + " schema validation, if not configured,
defaults to the first partition field.");
+ + " schema validation; must be unset for
non-partitioned tables."
+ + " If not configured, defaults to the
first partition field,"
+ + " or all partition fields for data
evolution RowID sorting.");
public static final ConfigOption<MemorySize>
MANIFEST_SORT_MAX_REWRITE_SIZE =
key("manifest-sort.max-rewrite-size")
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestAdjacentSortedRun.java
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestAdjacentSortedRun.java
index b076387ab2..ceafa1ad5b 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestAdjacentSortedRun.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestAdjacentSortedRun.java
@@ -29,10 +29,10 @@ import java.util.stream.Collectors;
* A {@code ManifestAdjacentSortedRun} is a list of {@link ManifestFileMeta}s
sorted by manifest
* sort key. The sort-key intervals of these manifests do not overlap.
*
- * <p><b>Boundary Equality:</b> Partition-field sorting treats
boundary-touching intervals (min ==
- * previous.max) as non-overlapping, so they can be placed in the same
SortedRun. This reduces the
- * number of runs and improves compaction efficiency. RowID sorting treats
row-id ranges as
- * inclusive, so boundary-touching row-id ranges are considered overlapping.
+ * <p><b>Boundary Equality:</b> Bucket and partition-field sorting treat
boundary-touching intervals
+ * (min == previous.max) as non-overlapping, so they can be placed in the same
SortedRun. This
+ * reduces the number of runs and improves compaction efficiency. RowID
sorting treats row-id ranges
+ * as inclusive, so boundary-touching row-id ranges are considered overlapping.
*/
public class ManifestAdjacentSortedRun {
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 f80554ce6e..1b04912ad8 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
@@ -84,9 +84,7 @@ public class ManifestCompactDryRun {
}
RowType partitionType = table.schema().logicalPartitionType();
- if (partitionType.getFieldCount() == 0
- && !(options.dataEvolutionEnabled()
- && ManifestFileMeta.allContainsRowId(manifests))) {
+ if (!ManifestFileMerger.canUseManifestSort(manifests, partitionType,
options)) {
return summary + " Manifest sort level files: unavailable (no
sortable field).";
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
index e3f8c7af76..688d59f492 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
@@ -23,6 +23,7 @@ import org.apache.paimon.disk.IOManager;
import org.apache.paimon.manifest.ManifestEntry;
import org.apache.paimon.manifest.ManifestFile;
import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.table.BucketMode;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.ExceptionUtils;
@@ -61,9 +62,8 @@ public class ManifestFileMerger {
List<ManifestFileMeta> newFilesForAbort = new ArrayList<>();
try {
- // If manifest-sort.enabled is enabled and there are sortable
fields, use
- // trySortRewrite. Data evolution tables sort by RowID when all
manifest files contain
- // RowID ranges, so they do not require partition fields.
+ // Bucketed tables and data evolution tables with complete RowID
ranges do not require
+ // partition fields for manifest sort rewrite.
if (canUseManifestSort(input, partitionType, options)) {
return ManifestFileSorter.trySortCompaction(
input, newFilesForAbort, manifestFile, partitionType,
options, ioManager);
@@ -94,6 +94,8 @@ public class ManifestFileMerger {
List<ManifestFileMeta> input, RowType partitionType, CoreOptions
options) {
return options.manifestSortEnabled()
&& (partitionType.getFieldCount() > 0
+ || options.bucket() > 0
+ || options.bucket() == BucketMode.POSTPONE_BUCKET
|| (options.dataEvolutionEnabled() &&
allContainsRowId(input)));
}
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 87fd4611e7..94255953fc 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
@@ -62,7 +62,7 @@ import java.util.function.Function;
import static
org.apache.paimon.utils.ManifestReadThreadPool.sequentialBatchedExecute;
/**
- * Manifest file sorter that sorts and rewrites manifest files by a configured
partition field, or
+ * Manifest file sorter that sorts and rewrites manifest files by bucket
and/or partition fields, or
* by RowID for data evolution tables.
*/
public class ManifestFileSorter {
@@ -697,7 +697,7 @@ public class ManifestFileSorter {
} else if (sortKey.isAfterMax(file,
earliestRun.get(earliestRun.size() - 1))) {
// Current file's min is after the run's max, append to this
run
// Note: When min == max (boundary equality), files are
considered
- // non-overlapping for partition sort and can be placed in the
same SortedRun.
+ // non-overlapping for bucket/partition sort and can share the
same SortedRun.
// RowID sort uses inclusive ranges, so boundary equality is
treated as overlap.
//
// See ManifestAdjacentSortedRun class comment for the full
boundary equality
@@ -1207,23 +1207,22 @@ public class ManifestFileSorter {
return new RowIdSortKey(partitionComparator, partitionType,
partitionSortFields);
}
- if (partitionType.getFieldCount() == 0) {
- throw new IllegalArgumentException(
- "Cannot resolve sort key for manifest sort rewrite.");
- }
-
- String sortField = resolveSortField(sortPartitionField, partitionType);
- int sortFieldIndex = partitionType.getFieldNames().indexOf(sortField);
- if (sortFieldIndex < 0) {
- throw new IllegalArgumentException(
- String.format(
- "Cannot resolve sort field '%s' for manifest sort
rewrite.",
- sortField));
+ int sortFieldIndex = -1;
+ RecordComparator fieldComparator = null;
+ if (partitionType.getFieldCount() > 0) {
+ String sortField = resolveSortField(sortPartitionField,
partitionType);
+ sortFieldIndex = partitionType.getFieldNames().indexOf(sortField);
+ if (sortFieldIndex < 0) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Cannot resolve sort field '%s' for manifest
sort rewrite.",
+ sortField));
+ }
+ fieldComparator =
+ CodeGenUtils.newRecordComparator(
+ partitionType.getFieldTypes(), new int[]
{sortFieldIndex});
}
- RecordComparator fieldComparator =
- CodeGenUtils.newRecordComparator(
- partitionType.getFieldTypes(), new int[]
{sortFieldIndex});
if (bucketed) {
boolean compareManifestBuckets =
input.stream()
@@ -1231,6 +1230,10 @@ public class ManifestFileSorter {
return new BucketSortKey(
fieldComparator, partitionType, sortFieldIndex,
compareManifestBuckets);
}
+ if (fieldComparator == null) {
+ throw new IllegalArgumentException(
+ "Cannot resolve sort key for manifest sort rewrite.");
+ }
return new PartitionSortKey(fieldComparator, partitionType,
sortFieldIndex);
}
@@ -1355,31 +1358,38 @@ public class ManifestFileSorter {
private static class BucketSortKey implements ManifestSortKey {
- private final PartitionSortKey partitionSortKey;
- private final InternalRow.FieldGetter sortFieldGetter;
+ @Nullable private final PartitionSortKey partitionSortKey;
+ @Nullable 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,
+ @Nullable 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);
+ List<DataType> fieldTypes = new ArrayList<>();
+ fieldTypes.add(DataTypes.INT());
+ if (fieldComparator == null) {
+ this.partitionSortKey = null;
+ this.sortFieldGetter = null;
+ } else {
+ this.partitionSortKey =
+ new PartitionSortKey(fieldComparator, partitionType,
sortFieldIndex);
+ DataType sortFieldType =
partitionType.getTypeAt(sortFieldIndex);
+ this.sortFieldGetter =
InternalRow.createFieldGetter(sortFieldType, sortFieldIndex);
+ fieldTypes.add(sortFieldType);
+ }
+ Collections.addAll(
+ fieldTypes,
+ DataTypes.TINYINT(),
+ DataTypes.STRING(),
+ ManifestEntry.MANIFEST_ROW_TYPE);
+ this.sortFieldNum = fieldTypes.size() - 1;
+ this.externalSortRowType = DataTypes.ROW(fieldTypes.toArray(new
DataType[0]));
this.externalSortKeyFields = createSequentialFields(sortFieldNum);
}
@@ -1391,7 +1401,7 @@ public class ManifestFileSorter {
return bucketComparison;
}
}
- return partitionSortKey.compareMin(a, b);
+ return partitionSortKey == null ? 0 :
partitionSortKey.compareMin(a, b);
}
@Override
@@ -1402,7 +1412,7 @@ public class ManifestFileSorter {
return bucketComparison;
}
}
- return partitionSortKey.compareMax(a, b);
+ return partitionSortKey == null ? 0 :
partitionSortKey.compareMax(a, b);
}
@Override
@@ -1413,7 +1423,11 @@ public class ManifestFileSorter {
return bucketComparison > 0;
}
}
- return partitionSortKey.isAfterMax(file, maxFile);
+ // Without partition fields, equal bucket boundaries can share a
run. Missing bucket
+ // statistics must conservatively be treated as overlapping ranges.
+ return partitionSortKey == null
+ ? compareManifestBuckets
+ : partitionSortKey.isAfterMax(file, maxFile);
}
@Override
@@ -1430,14 +1444,17 @@ public class ManifestFileSorter {
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());
+ int pos = 1;
+ if (sortFieldGetter != null) {
+ row.setField(pos++,
sortFieldGetter.getFieldOrNull(entry.partition()));
+ }
+ row.setField(pos++, entry.kind().toByteValue());
row.setField(
- 3,
+ pos++,
entry instanceof ProjectedManifestEntry
? ((ProjectedManifestEntry)
entry).file().fileNameBinary()
:
BinaryString.fromString(entry.file().fileName()));
- row.setField(4, binaryManifestRow);
+ row.setField(pos, binaryManifestRow);
}
@Override
diff --git
a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
index 996fceaa97..3a31f7dbba 100644
--- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
+++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
@@ -2023,8 +2023,10 @@ public class SchemaValidation {
if (options.manifestSortEnabled()) {
if (!options.dataEvolutionEnabled()) {
checkArgument(
- !schema.partitionKeys().isEmpty(),
- "Cannot enable '%s' for non-partition table.",
+ !schema.partitionKeys().isEmpty()
+ || options.bucket() > 0
+ || options.bucket() ==
BucketMode.POSTPONE_BUCKET,
+ "Cannot enable '%s' for non-partition table without
fixed or postponed buckets or data evolution.",
CoreOptions.MANIFEST_SORT_ENABLED.key());
}
String sortPartitionField = options.manifestSortPartitionField();
diff --git
a/paimon-core/src/test/java/org/apache/paimon/manifest/NoPartitionManifestFileMetaTest.java
b/paimon-core/src/test/java/org/apache/paimon/manifest/NoPartitionManifestFileMetaTest.java
index 3170c08d02..af6aca173f 100644
---
a/paimon-core/src/test/java/org/apache/paimon/manifest/NoPartitionManifestFileMetaTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/manifest/NoPartitionManifestFileMetaTest.java
@@ -19,25 +19,34 @@
package org.apache.paimon.manifest;
import org.apache.paimon.CoreOptions;
+import org.apache.paimon.Snapshot;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.Timestamp;
import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.operation.ManifestCompactDryRun;
import org.apache.paimon.operation.ManifestFileMerger;
import org.apache.paimon.options.Options;
import org.apache.paimon.stats.StatsTestUtils;
+import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.types.RowType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/** Test {@link ManifestFile}. for table without partition */
public class NoPartitionManifestFileMetaTest extends ManifestFileMetaTestBase {
@@ -154,6 +163,125 @@ public class NoPartitionManifestFileMetaTest extends
ManifestFileMetaTestBase {
assertThat(outputFileNames).containsExactly("row0", "row10-seq3",
"row10-seq1", "row20");
}
+ @ParameterizedTest
+ @CsvSource({
+ "4, false, false", "4, true, false", "-2, false, false", "-2, true,
false",
+ "4, false, true", "4, true, true", "-2, false, true", "-2, true, true"
+ })
+ public void testManifestSortByBucket(
+ int bucket, boolean fullCompaction, boolean missingBucketStats) {
+ int firstBucket = bucket == -2 ? -2 : 0;
+ List<ManifestFileMeta> input =
+ Arrays.asList(
+ makeManifest(
+ makeBucketEntry(true, "a-high", 3),
+ makeBucketEntry(true, "same-name", 1),
+ makeBucketEntry(true, "z-low", firstBucket)),
+ makeManifest(
+ makeBucketEntry(true, "same-name", 2),
+ makeBucketEntry(false, "same-name", 1),
+ makeBucketEntry(true, "b-high", 3)));
+ if (missingBucketStats) {
+ input.set(0, withoutBucketStats(input.get(0)));
+ }
+
+ Options options = new Options();
+ options.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ options.set(CoreOptions.BUCKET, bucket);
+ options.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1G");
+ options.set(CoreOptions.MANIFEST_MERGE_MIN_COUNT, 100);
+ options.set(
+ CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(),
+ fullCompaction ? "1B" : Long.MAX_VALUE + "B");
+
+ List<ManifestFileMeta> merged =
+ ManifestFileMerger.merge(
+ input, manifestFile, getPartitionType(), new
CoreOptions(options));
+ List<ManifestEntry> entries =
+ merged.stream()
+ .flatMap(
+ meta ->
+ manifestFile.read(meta.fileName(),
meta.fileSize())
+ .stream())
+ .collect(Collectors.toList());
+
+ assertThat(entries)
+ .containsExactly(
+ makeBucketEntry(true, "z-low", firstBucket),
+ makeBucketEntry(true, "same-name", 2),
+ makeBucketEntry(true, "a-high", 3),
+ makeBucketEntry(true, "b-high", 3));
+ assertThat(merged).hasSize(1);
+ assertThat(merged.get(0).minBucket()).isEqualTo(firstBucket);
+ assertThat(merged.get(0).maxBucket()).isEqualTo(3);
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "4, 0, false", "4, 1, false", "4, 2, false",
+ "-2, 0, false", "-2, 1, false", "-2, 2, false",
+ "4, 0, true", "-2, 0, true"
+ })
+ public void testManifestSortDryRunByBucket(
+ int bucket, int lowMaxBucket, boolean missingBucketStats) {
+ int firstBucket = bucket == -2 ? -2 : 0;
+ List<ManifestFileMeta> input =
+ Arrays.asList(
+ makeManifest(
+ makeBucketEntry(true, "high-start", 1),
+ makeBucketEntry(true, "high-end", 3)),
+ makeManifest(
+ makeBucketEntry(true, "low-start",
firstBucket),
+ makeBucketEntry(true, "low-end",
lowMaxBucket)));
+ if (missingBucketStats) {
+ input.set(0, withoutBucketStats(input.get(0)));
+ }
+
+ Options options = new Options();
+ options.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ options.set(CoreOptions.BUCKET, bucket);
+ options.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1B");
+ options.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(),
Long.MAX_VALUE + "B");
+
+ FileStoreTable table = mock(FileStoreTable.class, RETURNS_DEEP_STUBS);
+ Snapshot snapshot = mock(Snapshot.class);
+ when(table.options()).thenReturn(options.toMap());
+
when(table.store().snapshotManager().latestSnapshot()).thenReturn(snapshot);
+
when(table.store().manifestListFactory().create().readDataManifests(snapshot))
+ .thenReturn(input);
+
when(table.store().manifestFileFactory().create()).thenReturn(manifestFile);
+
when(table.schema().logicalPartitionType()).thenReturn(getPartitionType());
+
+ assertThat(ManifestCompactDryRun.execute(table))
+ .endsWith(
+ missingBucketStats || lowMaxBucket > 1
+ ? "Manifest sort level files: L0=0, L1=0,
L2=0, L3=1, L4=1."
+ : "Manifest sort level files: L0=0, L1=0,
L2=0, L3=0, L4=2.");
+ }
+
+ private ManifestEntry makeBucketEntry(boolean isAdd, String fileName, int
bucket) {
+ ManifestEntry entry = makeEntry(isAdd, fileName, null);
+ return ManifestEntry.create(entry.kind(), entry.partition(), bucket,
4, entry.file());
+ }
+
+ private ManifestFileMeta withoutBucketStats(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
public ManifestFile getManifestFile() {
return manifestFile;
diff --git
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
index c9588f4552..238dbef999 100644
---
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
@@ -27,6 +27,8 @@ import org.apache.paimon.types.DataTypes;
import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -1928,6 +1930,29 @@ class SchemaValidationTest {
1, fields, 10, partitionKeys, primaryKeys, options,
"geospatial test");
}
+ @ParameterizedTest
+ @ValueSource(ints = {1, 4, -2})
+ void testManifestSortForNonPartitionBucketedTable(int bucket) {
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), "true");
+ options.put(BUCKET.key(), String.valueOf(bucket));
+ TableSchema schema =
+ new TableSchema(
+ 1,
+ singletonList(new DataField(0, "f0", DataTypes.INT())),
+ 10,
+ emptyList(),
+ singletonList("f0"),
+ options,
+ "");
+ assertThatNoException().isThrownBy(() -> validateTableSchema(schema));
+
+ options.put(CoreOptions.MANIFEST_SORT_PARTITION_FIELD.key(), "f0");
+ assertThatThrownBy(() -> validateTableSchema(schema.copy(options)))
+ .hasMessageContaining(
+ "'manifest-sort.partition-field' = 'f0' is not a
partition field");
+ }
+
@Test
void testManifestSortValidation() {
List<DataField> fields =
@@ -1935,7 +1960,7 @@ class SchemaValidationTest {
new DataField(0, "f0", DataTypes.INT()),
new DataField(1, "f1", DataTypes.INT()));
- // Test 1: manifest-sort.enabled on non-partition table should fail
+ // Test 1: non-partition tables without bucket or RowID sorting should
fail
Map<String, String> options1 = new HashMap<>();
options1.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), "true");
options1.put(BUCKET.key(), String.valueOf(-1));
@@ -1951,7 +1976,7 @@ class SchemaValidationTest {
options1,
"")))
.hasMessageContaining(
- "Cannot enable 'manifest-sort.enabled' for
non-partition table.");
+ "Cannot enable 'manifest-sort.enabled' for
non-partition table without fixed or postponed buckets or data evolution.");
// Test 2: manifest-sort-partition-field not in partition keys should
fail
Map<String, String> options2 = new HashMap<>();