From 68c7397b24b56a9f524203210b8a3de5e4af4e90 Mon Sep 17 00:00:00 2001
From: jianguotian <18464293+jianguotian@users.noreply.github.com>
Date: Mon, 14 Sep 2026 10:50:38 +0800
Subject: [PATCH 1/6] [core] Support one-shot forced manifest rewrite
---
docs/docs/flink/procedures/compaction.md | 22 +-
docs/docs/spark/procedures/maintenance.md | 23 ++
docs/generated/core_configuration.html | 6 +
.../java/org/apache/paimon/CoreOptions.java | 66 ++++
.../operation/ManifestCompactDryRun.java | 1 +
.../paimon/operation/ManifestFileSorter.java | 82 ++++-
.../paimon/manifest/ManifestFileMetaTest.java | 294 ++++++++++++++++++
.../flink/action/CompactManifestAction.java | 3 +-
.../procedure/CompactManifestProcedure.java | 22 +-
.../CompactManifestProcedureITCase.java | 38 +++
.../procedure/CompactManifestProcedure.java | 19 +-
.../CompactManifestProcedureTest.scala | 3 +-
12 files changed, 567 insertions(+), 12 deletions(-)
diff --git a/docs/docs/flink/procedures/compaction.md b/docs/docs/flink/procedures/compaction.md
index 71c2fd531d7b..ae8727594eec 100644
--- a/docs/docs/flink/procedures/compaction.md
+++ b/docs/docs/flink/procedures/compaction.md
@@ -250,6 +250,12 @@ To compact_manifest the manifests. Arguments:
- manifest_sort_max_rewrite_size (String, optional): maximum manifest size rewritten by one sort pass.
+- manifest_sort_order (String, optional): target layout for a one-shot rewrite. Supported values are `bucket-first` and `partition-first`. Setting it enables manifest sort and forces existing manifests to be rewritten. `bucket-first` requires a bucketed table, and explicit sort orders are not supported for data evolution tables.
+
+When `manifest_sort_order` is omitted, the existing layout selection remains unchanged: bucketed tables use bucket-first, non-bucket tables use partition-first, and data evolution tables use RowID sorting when RowID metadata is available.
+
+Set `manifest-sort.force-rewrite=true` in `options` together with `manifest_sort_enabled=true` to rewrite already compacted manifest runs using the current sort order. Use it only as a one-shot dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget semantics still apply; raise it to migrate more manifests in one invocation.
+
**Syntax**
```sql
@@ -263,7 +269,8 @@ CALL [catalog.]sys.compact_manifest(
`table` => 'identifier',
`manifest_sort_enabled` => true,
`manifest_sort_partition_field` => 'dt',
- `manifest_sort_max_rewrite_size` => '1 gb'
+ `manifest_sort_max_rewrite_size` => '1 gb',
+ `manifest_sort_order` => 'partition-first'
);
```
@@ -280,6 +287,19 @@ CALL sys.compact_manifest(
`manifest_sort_partition_field` => 'dt',
`manifest_sort_max_rewrite_size` => '1 gb'
);
+
+CALL sys.compact_manifest(
+ `table` => 'default.T',
+ `manifest_sort_order` => 'partition-first',
+ `manifest_sort_max_rewrite_size` => '1 gb'
+);
+
+-- Switch the same bucketed table back to bucket-first layout.
+CALL sys.compact_manifest(
+ `table` => 'default.T',
+ `manifest_sort_order` => 'bucket-first',
+ `manifest_sort_max_rewrite_size` => '1 gb'
+);
```
## rescale
diff --git a/docs/docs/spark/procedures/maintenance.md b/docs/docs/spark/procedures/maintenance.md
index 1244848028fc..e636a9f03569 100644
--- a/docs/docs/spark/procedures/maintenance.md
+++ b/docs/docs/spark/procedures/maintenance.md
@@ -126,6 +126,16 @@ Compact manifest files.
- `manifest_sort_enabled` (`BOOLEAN`, optional): whether to use manifest sort rewrite for this invocation.
- `manifest_sort_partition_field` (`STRING`, optional): partition field used to sort manifest entries. Defaults to the first partition field.
- `manifest_sort_max_rewrite_size` (`STRING`, optional): maximum manifest size rewritten by one sort pass.
+- `manifest_sort_order` (`STRING`, optional): target layout for a one-shot rewrite. Supported values are `bucket-first` and `partition-first`. Setting it enables manifest sort and forces existing manifests to be rewritten. `bucket-first` requires a bucketed table, and explicit sort orders are not supported for data evolution tables.
+
+When `manifest_sort_order` is omitted, the existing layout selection remains unchanged: bucketed
+tables use bucket-first, non-bucket tables use partition-first, and data evolution tables use RowID
+sorting when RowID metadata is available.
+
+Set `manifest-sort.force-rewrite=true` in `options` together with `manifest_sort_enabled=true` to
+rewrite already compacted manifest runs using the current sort order. Use it only as a one-shot
+dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget semantics still apply;
+raise it to migrate more manifests in one invocation.
```sql
CALL sys.compact_manifest(`table` => 'default.T');
@@ -138,6 +148,19 @@ CALL sys.compact_manifest(
manifest_sort_partition_field => 'dt',
manifest_sort_max_rewrite_size => '1 gb'
);
+
+CALL sys.compact_manifest(
+ `table` => 'default.T',
+ manifest_sort_order => 'partition-first',
+ manifest_sort_max_rewrite_size => '1 gb'
+);
+
+-- Switch the same bucketed table back to bucket-first layout.
+CALL sys.compact_manifest(
+ `table` => 'default.T',
+ manifest_sort_order => 'bucket-first',
+ manifest_sort_max_rewrite_size => '1 gb'
+);
```
## materialize_deletion_vectors
diff --git a/docs/generated/core_configuration.html b/docs/generated/core_configuration.html
index ac5730017523..2ee3a3321c16 100644
--- a/docs/generated/core_configuration.html
+++ b/docs/generated/core_configuration.html
@@ -1065,6 +1065,12 @@
Boolean |
Whether to invoke manifest sort rewrite during commit. 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. |
+
+ manifest-sort.force-rewrite |
+ false |
+ Boolean |
+ When 'manifest-sort.enabled' is true, force an explicit manifest compaction to rewrite already compacted manifest runs using the configured manifest sort order. The existing 'manifest-sort.max-rewrite-size' rewrite budget semantics still apply. This should be supplied as a one-shot dynamic option for maintenance, not persisted for routine writes. |
+
manifest-sort.max-rewrite-size |
256 mb |
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 7b0665c50296..1a237eb4d63a 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -599,6 +599,27 @@ public InlineElement getDescription() {
+ " skipped. Set to a larger value to allow more aggressive"
+ " sort rewriting. The cap only limits the sorted rewrite portion and full/minor cleanup may still happen beyond it.");
+ public static final ConfigOption MANIFEST_SORT_FORCE_REWRITE =
+ key("manifest-sort.force-rewrite")
+ .booleanType()
+ .defaultValue(false)
+ .withDescription(
+ "When 'manifest-sort.enabled' is true, force an explicit manifest"
+ + " compaction to rewrite already compacted manifest runs using"
+ + " the configured manifest sort order."
+ + " The existing 'manifest-sort.max-rewrite-size' rewrite budget"
+ + " semantics still apply."
+ + " This should be supplied as a one-shot dynamic option for"
+ + " maintenance, not persisted for routine writes.");
+
+ @ExcludeFromDocumentation("Only used by compact_manifest maintenance procedure")
+ public static final ConfigOption MANIFEST_SORT_ORDER =
+ key("manifest-sort.order")
+ .enumType(ManifestSortOrder.class)
+ .noDefaultValue()
+ .withDescription(
+ "Target manifest layout for a one-shot manifest sort rewrite.");
+
public static final ConfigOption MANIFEST_MERGE_OPTIMIZE_ENABLED =
key("manifest.merge-optimize.enabled")
.booleanType()
@@ -3227,6 +3248,15 @@ public long manifestSortMaxRewriteSize() {
return options.get(MANIFEST_SORT_MAX_REWRITE_SIZE).getBytes();
}
+ public boolean manifestSortForceRewrite() {
+ return options.get(MANIFEST_SORT_FORCE_REWRITE);
+ }
+
+ @Nullable
+ public ManifestSortOrder manifestSortOrder() {
+ return options.getOptional(MANIFEST_SORT_ORDER).orElse(null);
+ }
+
public boolean manifestMergeOptimizeEnabled() {
return options.get(MANIFEST_MERGE_OPTIMIZE_ENABLED);
}
@@ -5431,6 +5461,42 @@ public static List> getOptions() {
})
.collect(Collectors.toSet());
+ /** Target layout for an explicit manifest sort rewrite. */
+ public enum ManifestSortOrder implements DescribedEnum {
+ BUCKET_FIRST("bucket-first", "Sort manifest entries by bucket before partition."),
+ PARTITION_FIRST("partition-first", "Sort manifest entries by partition.");
+
+ private final String value;
+ private final String description;
+
+ ManifestSortOrder(String value, String description) {
+ this.value = value;
+ this.description = description;
+ }
+
+ public static ManifestSortOrder fromString(String value) {
+ for (ManifestSortOrder order : values()) {
+ if (order.value.equalsIgnoreCase(value.trim())) {
+ return order;
+ }
+ }
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported manifest sort order '%s'. Supported values are 'bucket-first' and 'partition-first'.",
+ value));
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ @Override
+ public InlineElement getDescription() {
+ return text(description);
+ }
+ }
+
/** Specifies the sort engine for table with primary key. */
public enum SortEngine implements DescribedEnum {
MIN_HEAP("min-heap", "Use min-heap for multiway sorting."),
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 f80554ce6ea4..aebcc1622d00 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
@@ -121,6 +121,7 @@ private static List buildLevelSortedRunsForDryRun(
manifests,
options.manifestSortPartitionField(),
partitionType,
+ options.manifestSortOrder(),
options.bucket() > 0 || options.bucket() == BucketMode.POSTPONE_BUCKET);
ManifestFileSorter.ClassifyResult classifyResult =
ManifestFileSorter.classifyManifests(
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 87fd4611e72b..0f1250696829 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
@@ -71,6 +71,7 @@ public class ManifestFileSorter {
/** Context object that carries shared state across compaction methods. */
static class CompactionContext {
final boolean fullCompaction;
+ final boolean forceRewrite;
final boolean runMergeOptimizeEnabled;
final ManifestSortKey sortKey;
final RowType partitionType;
@@ -91,6 +92,7 @@ static class CompactionContext {
CompactionContext(
boolean fullCompaction,
+ boolean forceRewrite,
boolean runMergeOptimizeEnabled,
ManifestSortKey sortKey,
RowType partitionType,
@@ -100,6 +102,7 @@ static class CompactionContext {
List levelRuns,
List pickedRuns) {
this.fullCompaction = fullCompaction;
+ this.forceRewrite = forceRewrite;
this.runMergeOptimizeEnabled = runMergeOptimizeEnabled;
this.sortKey = sortKey;
this.partitionType = partitionType;
@@ -156,11 +159,13 @@ static List trySortCompaction(
@Nullable IOManager ioManager)
throws Exception {
String sortPartitionField = options.manifestSortPartitionField();
+ CoreOptions.ManifestSortOrder sortOrder = options.manifestSortOrder();
boolean bucketed = options.bucket() > 0 || options.bucket() == BucketMode.POSTPONE_BUCKET;
boolean runMergeOptimizeEnabled = options.manifestMergeOptimizeEnabled();
long suggestedMetaSize = options.manifestTargetSize().getBytes();
int suggestedMinMetaCount = options.manifestMergeMinCount();
long fullCompactionThreshold = options.manifestFullCompactionThresholdSize().getBytes();
+ boolean forceRewrite = options.manifestSortForceRewrite();
long maxRewriteSize = options.manifestSortMaxRewriteSize();
int maxSizeAmplificationPercent = options.maxSizeAmplificationPercent();
int sortedRunSizeRatio = options.sortedRunSizeRatio();
@@ -175,12 +180,14 @@ static List trySortCompaction(
manifestFile,
partitionType,
sortPartitionField,
+ sortOrder,
bucketed,
options.dataEvolutionEnabled(),
runMergeOptimizeEnabled,
suggestedMetaSize,
suggestedMinMetaCount,
fullCompactionThreshold,
+ forceRewrite,
maxRewriteSize,
maxSizeAmplificationPercent,
sortedRunSizeRatio,
@@ -195,6 +202,7 @@ static List trySortCompaction(
manifestFile,
partitionType,
sortPartitionField,
+ sortOrder,
bucketed,
options.dataEvolutionEnabled(),
runMergeOptimizeEnabled,
@@ -219,12 +227,14 @@ private static Optional> tryFullCompaction(
ManifestFile manifestFile,
RowType partitionType,
String sortPartitionField,
+ @Nullable CoreOptions.ManifestSortOrder sortOrder,
boolean bucketed,
boolean dataEvolutionEnabled,
boolean runMergeOptimizeEnabled,
long suggestedMetaSize,
int suggestedMinMetaCount,
long fullCompactionThreshold,
+ boolean forceRewrite,
long maxRewriteSize,
int maxSizeAmplificationPercent,
int sortedRunSizeRatio,
@@ -232,7 +242,9 @@ private static Optional> tryFullCompaction(
@Nullable Integer manifestReadParallelism)
throws Exception {
// Step 1: Check if full compaction threshold is met
- if (!reachesFullCompactionThreshold(input, suggestedMetaSize, fullCompactionThreshold)) {
+ if (!forceRewrite
+ && !reachesFullCompactionThreshold(
+ input, suggestedMetaSize, fullCompactionThreshold)) {
return Optional.empty();
}
// Step 2: Prepare compaction context
@@ -240,9 +252,11 @@ private static Optional> tryFullCompaction(
prepareCompaction(
input,
true,
+ forceRewrite,
manifestFile,
partitionType,
sortPartitionField,
+ sortOrder,
bucketed,
dataEvolutionEnabled,
runMergeOptimizeEnabled,
@@ -254,6 +268,9 @@ private static Optional> tryFullCompaction(
try {
List levelRuns = ctx.levelRuns;
List pickedRuns = ctx.pickedRuns;
+ if (forceRewrite) {
+ pickedRuns = new ArrayList<>(levelRuns);
+ }
if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) {
LOG.debug(
@@ -283,9 +300,23 @@ private static Optional> tryFullCompaction(
}
pickedFiles.addAll(ctx.defaultCompactFiles.keySet());
- // Step 4: Split into sections and merge small adjacent sections
- List sections = splitIntoSections(pickedFiles, ctx);
- sections = mergeSmallAdjacentSections(sections, suggestedMetaSize);
+ // Step 4: Split into sections and merge small adjacent sections. A forced rewrite
+ // intentionally uses one global section so entries from different already-compacted
+ // manifests can be clustered using the current sort order.
+ List sections;
+ if (forceRewrite) {
+ long totalSize = 0L;
+ boolean hasDefaultCompactFile = false;
+ for (ManifestFileMeta file : pickedFiles) {
+ totalSize += file.fileSize();
+ hasDefaultCompactFile |= ctx.isMarkedForDefaultCompaction(file);
+ }
+ sections = new ArrayList<>();
+ sections.add(new Section(pickedFiles, totalSize, hasDefaultCompactFile));
+ } else {
+ sections = splitIntoSections(pickedFiles, ctx);
+ sections = mergeSmallAdjacentSections(sections, suggestedMetaSize);
+ }
LOG.info(
"Manifest sort full compact: pickedFiles={}, sections={}.",
@@ -327,6 +358,7 @@ private static List tryMinorCompaction(
ManifestFile manifestFile,
RowType partitionType,
String sortPartitionField,
+ @Nullable CoreOptions.ManifestSortOrder sortOrder,
boolean bucketed,
boolean dataEvolutionEnabled,
boolean runMergeOptimizeEnabled,
@@ -343,9 +375,11 @@ private static List tryMinorCompaction(
prepareCompaction(
input,
false,
+ false,
manifestFile,
partitionType,
sortPartitionField,
+ sortOrder,
bucketed,
dataEvolutionEnabled,
runMergeOptimizeEnabled,
@@ -458,9 +492,11 @@ private static List tryMinorCompaction(
private static CompactionContext prepareCompaction(
List input,
boolean fullCompaction,
+ boolean forceRewrite,
ManifestFile manifestFile,
RowType partitionType,
String sortPartitionField,
+ @Nullable CoreOptions.ManifestSortOrder sortOrder,
boolean bucketed,
boolean dataEvolutionEnabled,
boolean runMergeOptimizeEnabled,
@@ -475,7 +511,12 @@ private static CompactionContext prepareCompaction(
// Step 1: Resolve sort key. Data evolution tables prefer RowID ranges when available.
ManifestSortKey sortKey =
createSortKey(
- dataEvolutionEnabled, input, sortPartitionField, partitionType, bucketed);
+ dataEvolutionEnabled,
+ input,
+ sortPartitionField,
+ partitionType,
+ sortOrder,
+ bucketed);
// Step 2: Classify manifests into LSM files and collect delete entries.
ClassifyResult classification =
@@ -500,6 +541,7 @@ private static CompactionContext prepareCompaction(
return new CompactionContext(
fullCompaction,
+ forceRewrite,
useRunMergeOptimize,
sortKey,
partitionType,
@@ -1080,7 +1122,9 @@ private static void rewriteSection(
@Nullable Integer manifestReadParallelism)
throws Exception {
// Skip rewrite for single file not in delete-range.
- if (section.size() == 1 && !ctx.defaultCompactFiles.getOrDefault(section.get(0), false)) {
+ if (section.size() == 1
+ && !ctx.forceRewrite
+ && !ctx.defaultCompactFiles.getOrDefault(section.get(0), false)) {
output.addUnchanged(section.get(0));
return;
}
@@ -1195,6 +1239,27 @@ static ManifestSortKey createSortKey(
String sortPartitionField,
RowType partitionType,
boolean bucketed) {
+ return createSortKey(
+ dataEvolutionEnabled, input, sortPartitionField, partitionType, null, bucketed);
+ }
+
+ static ManifestSortKey createSortKey(
+ boolean dataEvolutionEnabled,
+ List input,
+ String sortPartitionField,
+ RowType partitionType,
+ @Nullable CoreOptions.ManifestSortOrder sortOrder,
+ boolean bucketed) {
+ if (sortOrder != null && dataEvolutionEnabled) {
+ throw new IllegalArgumentException(
+ "Explicit manifest sort order is not supported for data evolution tables.");
+ }
+
+ if (sortOrder == CoreOptions.ManifestSortOrder.BUCKET_FIRST && !bucketed) {
+ throw new IllegalArgumentException(
+ "Manifest sort order 'bucket-first' requires a bucketed table.");
+ }
+
boolean rowIdSort = dataEvolutionEnabled && ManifestFileMeta.allContainsRowId(input);
if (rowIdSort) {
// RowID sorting uses the configured partition field as the primary key when specified,
@@ -1224,7 +1289,10 @@ static ManifestSortKey createSortKey(
RecordComparator fieldComparator =
CodeGenUtils.newRecordComparator(
partitionType.getFieldTypes(), new int[] {sortFieldIndex});
- if (bucketed) {
+ boolean useBucketSort =
+ sortOrder == CoreOptions.ManifestSortOrder.BUCKET_FIRST
+ || (sortOrder == null && bucketed);
+ if (useBucketSort) {
boolean compareManifestBuckets =
input.stream()
.allMatch(meta -> meta.minBucket() != null && meta.maxBucket() != null);
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 6d67eddfbdcd..7d0abc7c7d73 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
@@ -1352,6 +1352,264 @@ public void testManifestSortUsesBucketAsPrimaryKeyForBucketedTable(int bucket) {
.containsExactly(0, 1, 0, 1);
}
+ @Test
+ public void testManifestSortForceRewriteAlreadyCompactedRuns() {
+ List physical =
+ Arrays.asList(
+ makeManifest(makeBucketEntry("a-3", 0, 3), makeBucketEntry("a-1", 0, 1)),
+ makeManifest(makeBucketEntry("b-2", 1, 2), makeBucketEntry("b-0", 1, 0)));
+ long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
+ List input =
+ physical.stream()
+ .map(meta -> copyAsLegacyWithFileSize(meta, targetSize))
+ .collect(Collectors.toList());
+ assertThat(input)
+ .allMatch(
+ meta ->
+ meta.minBucket() == null
+ && meta.maxBucket() == null
+ && meta.totalBuckets() == null);
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1G");
+ testOptions.set(CoreOptions.BUCKET, 4);
+
+ List unchanged =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+ assertThat(unchanged).containsExactlyInAnyOrderElementsOf(input);
+
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ List rewritten =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+
+ assertEquivalentEntries(input, rewritten);
+ assertThat(rewritten)
+ .extracting(ManifestFileMeta::fileName)
+ .doesNotContainAnyElementsOf(
+ input.stream()
+ .map(ManifestFileMeta::fileName)
+ .collect(Collectors.toList()));
+ assertThat(readEntries(rewritten))
+ .extracting(ManifestEntry::bucket)
+ .containsExactly(0, 1, 2, 3);
+ assertThat(rewritten).hasSize(1);
+ assertThat(rewritten.get(0).minBucket()).isZero();
+ assertThat(rewritten.get(0).maxBucket()).isEqualTo(3);
+ assertThat(rewritten.get(0).totalBuckets()).isEqualTo(240);
+
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, false);
+ List afterMigration =
+ ManifestFileMerger.merge(
+ rewritten,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+ assertThat(afterMigration).containsExactlyElementsOf(rewritten);
+ }
+
+ @Test
+ public void testManifestSortForceRewriteSwitchesLayout() {
+ List input =
+ Arrays.asList(
+ makeManifest(
+ makeBucketEntry("a-p0-b3", 0, 3), makeBucketEntry("a-p1-b1", 1, 1)),
+ makeManifest(
+ makeBucketEntry("b-p0-b2", 0, 2),
+ makeBucketEntry("b-p1-b0", 1, 0)));
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1G");
+ testOptions.set(CoreOptions.BUCKET, 4);
+ testOptions.set(
+ CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.PARTITION_FIRST);
+
+ List partitionFirst =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+ assertEquivalentEntries(input, partitionFirst);
+ assertThat(readEntries(partitionFirst))
+ .extracting(entry -> entry.partition().getInt(0))
+ .containsExactly(0, 0, 1, 1);
+
+ testOptions.set(
+ CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.BUCKET_FIRST);
+ List bucketFirst =
+ ManifestFileMerger.merge(
+ partitionFirst,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+ assertEquivalentEntries(partitionFirst, bucketFirst);
+ assertThat(readEntries(bucketFirst))
+ .extracting(ManifestEntry::bucket)
+ .containsExactly(0, 1, 2, 3);
+ }
+
+ @Test
+ public void testManifestSortExplicitOrderValidation() {
+ List input =
+ Collections.singletonList(makeManifest(makeBucketEntry("file", 0, 0)));
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ testOptions.set(
+ CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.BUCKET_FIRST);
+ assertThat(
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()))))
+ .hasMessage("Manifest sort order 'bucket-first' requires a bucketed table.");
+
+ testOptions.set(
+ CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.PARTITION_FIRST);
+ testOptions.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
+ assertThat(
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()))))
+ .hasMessage(
+ "Explicit manifest sort order is not supported for data evolution tables.");
+ }
+
+ @Test
+ public void testManifestSortForceRewriteAllLevelRuns() {
+ List physical =
+ Arrays.asList(
+ makeManifest(makeBucketEntry("a-3", 0, 3), makeBucketEntry("a-1", 2, 1)),
+ makeManifest(makeBucketEntry("b-2", 1, 2), makeBucketEntry("b-0", 3, 0)));
+ long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
+ List input =
+ physical.stream()
+ .map(meta -> copyAsLegacyWithFileSize(meta, targetSize))
+ .collect(Collectors.toList());
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1G");
+ testOptions.set(CoreOptions.BUCKET, 4);
+
+ // Without bucket metadata, manifest sort falls back to the overlapping partition ranges
+ // [0, 2] and [1, 3]. The dry run therefore shows that these files form two level runs.
+ FileStoreTable table = mock(FileStoreTable.class, RETURNS_DEEP_STUBS);
+ Snapshot snapshot = mock(Snapshot.class);
+ when(table.options()).thenReturn(testOptions.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("Manifest sort level files: L0=0, L1=0, L2=0, L3=1, L4=1.");
+
+ List unchanged =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+ assertThat(unchanged).containsExactlyInAnyOrderElementsOf(input);
+
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ List rewritten =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+
+ assertEquivalentEntries(input, rewritten);
+ assertThat(rewritten)
+ .extracting(ManifestFileMeta::fileName)
+ .doesNotContainAnyElementsOf(
+ input.stream()
+ .map(ManifestFileMeta::fileName)
+ .collect(Collectors.toList()));
+ assertThat(readEntries(rewritten))
+ .extracting(ManifestEntry::bucket)
+ .containsExactly(0, 1, 2, 3);
+ }
+
+ @Test
+ public void testManifestSortForceRewriteSingleManifest() {
+ ManifestFileMeta physical =
+ makeManifest(makeBucketEntry("file-3", 0, 3), makeBucketEntry("file-0", 0, 0));
+ ManifestFileMeta input =
+ copyWithFileSize(
+ physical, CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes());
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ testOptions.set(CoreOptions.BUCKET, 4);
+ List rewritten =
+ ManifestFileMerger.merge(
+ Collections.singletonList(input),
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+
+ assertEquivalentEntries(Collections.singletonList(input), rewritten);
+ assertThat(rewritten)
+ .extracting(ManifestFileMeta::fileName)
+ .doesNotContain(input.fileName());
+ assertThat(readEntries(rewritten)).extracting(ManifestEntry::bucket).containsExactly(0, 3);
+ }
+
+ @Test
+ public void testManifestSortForceRewriteRespectsRewriteBudget() {
+ long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
+ List input = new ArrayList<>();
+ for (int i = 0; i < 4; i++) {
+ input.add(
+ copyWithFileSize(makeManifest(makeBucketEntry("file-" + i, 0, i)), targetSize));
+ }
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1B");
+ testOptions.set(CoreOptions.BUCKET, 4);
+ List rewritten =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+
+ Set inputNames =
+ input.stream().map(ManifestFileMeta::fileName).collect(Collectors.toSet());
+ assertThat(rewritten)
+ .extracting(ManifestFileMeta::fileName)
+ .filteredOn(inputNames::contains)
+ .hasSize(2);
+ assertEquivalentEntries(input, rewritten);
+ }
+
@ParameterizedTest
@ValueSource(ints = {-1, 4, -2})
public void testManifestSortDryRunUsesBucketRangesForBucketedTable(int bucket) {
@@ -2830,6 +3088,42 @@ private ManifestEntry makeBucketEntry(String fileName, int partition, int bucket
return ManifestEntry.create(entry.kind(), entry.partition(), bucket, 240, entry.file());
}
+ private ManifestFileMeta copyWithFileSize(ManifestFileMeta meta, long fileSize) {
+ return new ManifestFileMeta(
+ meta.fileName(),
+ fileSize,
+ meta.numAddedFiles(),
+ meta.numDeletedFiles(),
+ meta.partitionStats(),
+ meta.schemaId(),
+ meta.minBucket(),
+ meta.maxBucket(),
+ meta.minLevel(),
+ meta.maxLevel(),
+ meta.minRowId(),
+ meta.maxRowId(),
+ meta.totalBuckets(),
+ meta.extraFiles());
+ }
+
+ private ManifestFileMeta copyAsLegacyWithFileSize(ManifestFileMeta meta, long fileSize) {
+ return new ManifestFileMeta(
+ meta.fileName(),
+ fileSize,
+ meta.numAddedFiles(),
+ meta.numDeletedFiles(),
+ meta.partitionStats(),
+ meta.schemaId(),
+ null,
+ null,
+ meta.minLevel(),
+ meta.maxLevel(),
+ meta.minRowId(),
+ meta.maxRowId(),
+ null,
+ meta.extraFiles());
+ }
+
/** 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-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java
index 4b70de86dbf3..7c0c123339eb 100644
--- a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java
+++ b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java
@@ -69,7 +69,8 @@ public void executeLocally() throws Exception {
dryRun,
manifestSortEnabled,
manifestSortPartitionField,
- manifestSortMaxRewriteSize);
+ manifestSortMaxRewriteSize,
+ null);
for (String result : results) {
LOG.info(result);
}
diff --git a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
index 1bae106b9f6b..a12c9abcdb8b 100644
--- a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
+++ b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
@@ -59,6 +59,10 @@ public String identifier() {
@ArgumentHint(
name = "manifest_sort_max_rewrite_size",
type = @DataTypeHint("STRING"),
+ isOptional = true),
+ @ArgumentHint(
+ name = "manifest_sort_order",
+ type = @DataTypeHint("STRING"),
isOptional = true)
})
public String[] call(
@@ -68,7 +72,8 @@ public String[] call(
@Nullable Boolean dryRun,
@Nullable Boolean manifestSortEnabled,
@Nullable String manifestSortPartitionField,
- @Nullable String manifestSortMaxRewriteSize)
+ @Nullable String manifestSortMaxRewriteSize,
+ @Nullable String manifestSortOrder)
throws Exception {
FileStoreTable table = (FileStoreTable) table(tableId);
@@ -88,6 +93,21 @@ public String[] call(
dynamicOptions.put(
CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), manifestSortMaxRewriteSize);
}
+ if (manifestSortOrder != null) {
+ if (Boolean.FALSE.equals(manifestSortEnabled)
+ || "false"
+ .equalsIgnoreCase(
+ dynamicOptions.get(CoreOptions.MANIFEST_SORT_ENABLED.key()))) {
+ throw new IllegalArgumentException(
+ "'manifest_sort_order' cannot be used with 'manifest_sort_enabled=false'.");
+ }
+ CoreOptions.ManifestSortOrder order =
+ CoreOptions.ManifestSortOrder.fromString(manifestSortOrder);
+ dynamicOptions.put(CoreOptions.MANIFEST_SORT_ORDER.key(), order.toString());
+ dynamicOptions.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), Boolean.TRUE.toString());
+ dynamicOptions.put(
+ CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(), Boolean.TRUE.toString());
+ }
table = table.copy(dynamicOptions);
diff --git a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
index a40fa8f905f6..cef9509018f1 100644
--- a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
+++ b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
@@ -18,6 +18,7 @@
package org.apache.paimon.flink.procedure;
+import org.apache.paimon.CoreOptions;
import org.apache.paimon.flink.CatalogITCaseBase;
import org.apache.paimon.flink.action.ActionFactory;
import org.apache.paimon.flink.action.CompactManifestAction;
@@ -123,6 +124,24 @@ public void testManifestSortParameters() throws Exception {
sql(procedure);
Assertions.assertThat(table.snapshotManager().latestSnapshot().id())
.isEqualTo(compactSnapshotId);
+
+ String forceRewriteProcedure =
+ "CALL sys.compact_manifest("
+ + "`table` => 'default.T_SORT', "
+ + "`manifest_sort_partition_field` => 'dt', "
+ + "`manifest_sort_max_rewrite_size` => '1 gb', "
+ + "`manifest_sort_order` => 'partition-first')";
+ sql(forceRewriteProcedure);
+ long forceRewriteSnapshotId = table.snapshotManager().latestSnapshot().id();
+ Assertions.assertThat(forceRewriteSnapshotId).isEqualTo(compactSnapshotId + 1);
+ Assertions.assertThat(paimonTable("T_SORT").options())
+ .doesNotContainKeys(
+ CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(),
+ CoreOptions.MANIFEST_SORT_ORDER.key());
+
+ sql(procedure);
+ Assertions.assertThat(table.snapshotManager().latestSnapshot().id())
+ .isEqualTo(forceRewriteSnapshotId);
}
@Test
@@ -141,6 +160,25 @@ public void testManifestSortParametersValidation() {
+ "`manifest_sort_partition_field` => 'missing')"))
.hasStackTraceContaining(
"'manifest-sort.partition-field' = 'missing' is not a partition field");
+
+ Assertions.assertThatThrownBy(
+ () ->
+ sql(
+ "CALL sys.compact_manifest("
+ + "`table` => 'default.T_INVALID', "
+ + "`manifest_sort_order` => 'unknown')"))
+ .hasStackTraceContaining(
+ "Unsupported manifest sort order 'unknown'. Supported values are 'bucket-first' and 'partition-first'.");
+
+ Assertions.assertThatThrownBy(
+ () ->
+ sql(
+ "CALL sys.compact_manifest("
+ + "`table` => 'default.T_INVALID', "
+ + "`manifest_sort_enabled` => false, "
+ + "`manifest_sort_order` => 'partition-first')"))
+ .hasStackTraceContaining(
+ "'manifest_sort_order' cannot be used with 'manifest_sort_enabled=false'.");
}
@Test
diff --git a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
index 25b417e5b0a7..89c15761d3b8 100644
--- a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
+++ b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
@@ -59,7 +59,8 @@ public class CompactManifestProcedure extends BaseProcedure {
ProcedureParameter.optional("dry_run", BooleanType),
ProcedureParameter.optional("manifest_sort_enabled", BooleanType),
ProcedureParameter.optional("manifest_sort_partition_field", StringType),
- ProcedureParameter.optional("manifest_sort_max_rewrite_size", StringType)
+ ProcedureParameter.optional("manifest_sort_max_rewrite_size", StringType),
+ ProcedureParameter.optional("manifest_sort_order", StringType)
};
private static final StructType OUTPUT_TYPE =
@@ -91,6 +92,7 @@ public InternalRow[] call(InternalRow args) {
Boolean manifestSortEnabled = args.isNullAt(3) ? null : args.getBoolean(3);
String manifestSortPartitionField = args.isNullAt(4) ? null : args.getString(4);
String manifestSortMaxRewriteSize = args.isNullAt(5) ? null : args.getString(5);
+ String manifestSortOrder = args.isNullAt(6) ? null : args.getString(6);
Table table = loadSparkTable(tableIdent).getTable();
HashMap dynamicOptions = new HashMap<>();
@@ -107,6 +109,21 @@ public InternalRow[] call(InternalRow args) {
dynamicOptions.put(
CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), manifestSortMaxRewriteSize);
}
+ if (manifestSortOrder != null) {
+ if (Boolean.FALSE.equals(manifestSortEnabled)
+ || "false"
+ .equalsIgnoreCase(
+ dynamicOptions.get(CoreOptions.MANIFEST_SORT_ENABLED.key()))) {
+ throw new IllegalArgumentException(
+ "'manifest_sort_order' cannot be used with 'manifest_sort_enabled=false'.");
+ }
+ CoreOptions.ManifestSortOrder order =
+ CoreOptions.ManifestSortOrder.fromString(manifestSortOrder);
+ dynamicOptions.put(CoreOptions.MANIFEST_SORT_ORDER.key(), order.toString());
+ dynamicOptions.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), Boolean.TRUE.toString());
+ dynamicOptions.put(
+ CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(), Boolean.TRUE.toString());
+ }
table = table.copy(dynamicOptions);
if (dryRun) {
diff --git a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
index 76368e6f240c..4dab5d46484d 100644
--- a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
+++ b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
@@ -69,7 +69,8 @@ class CompactManifestProcedureTest extends PaimonSparkTestBase with StreamTest {
"dry_run => true, " +
"manifest_sort_enabled => true, " +
"manifest_sort_partition_field => 'dt', " +
- "manifest_sort_max_rewrite_size => '1gb')")
+ "manifest_sort_max_rewrite_size => '1gb', " +
+ "manifest_sort_order => 'partition-first')")
.collectAsList()
Assertions.assertThat(dryRunRows.get(0).getBoolean(0)).isTrue
From 1592745594ecb04fff7d10524384d422904a580a Mon Sep 17 00:00:00 2001
From: mingfeng
Date: Mon, 14 Sep 2026 13:23:39 +0800
Subject: [PATCH 2/6] [core] Restrict forced manifest rewrite to maintenance
---
docs/generated/core_configuration.html | 6 ---
.../java/org/apache/paimon/CoreOptions.java | 1 +
.../operation/ManifestCompactDryRun.java | 13 +++---
.../paimon/schema/SchemaValidation.java | 15 ++++++-
.../paimon/manifest/ManifestFileMetaTest.java | 27 +++++++++++++
.../paimon/schema/SchemaValidationTest.java | 40 +++++++++++++++++++
6 files changed, 89 insertions(+), 13 deletions(-)
diff --git a/docs/generated/core_configuration.html b/docs/generated/core_configuration.html
index 2ee3a3321c16..ac5730017523 100644
--- a/docs/generated/core_configuration.html
+++ b/docs/generated/core_configuration.html
@@ -1065,12 +1065,6 @@
| Boolean |
Whether to invoke manifest sort rewrite during commit. 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. |
-
- manifest-sort.force-rewrite |
- false |
- Boolean |
- When 'manifest-sort.enabled' is true, force an explicit manifest compaction to rewrite already compacted manifest runs using the configured manifest sort order. The existing 'manifest-sort.max-rewrite-size' rewrite budget semantics still apply. This should be supplied as a one-shot dynamic option for maintenance, not persisted for routine writes. |
-
manifest-sort.max-rewrite-size |
256 mb |
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 1a237eb4d63a..8e345dd998f3 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -599,6 +599,7 @@ public InlineElement getDescription() {
+ " skipped. Set to a larger value to allow more aggressive"
+ " sort rewriting. The cap only limits the sorted rewrite portion and full/minor cleanup may still happen beyond it.");
+ @ExcludeFromDocumentation("Only used by compact_manifest maintenance procedure")
public static final ConfigOption MANIFEST_SORT_FORCE_REWRITE =
key("manifest-sort.force-rewrite")
.booleanType()
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 aebcc1622d00..3c2abfbdb451 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
@@ -110,11 +110,13 @@ private static List buildLevelSortedRunsForDryRun(
RowType partitionType,
CoreOptions options) {
long suggestedMetaSize = options.manifestTargetSize().getBytes();
+ boolean forceRewrite = options.manifestSortForceRewrite();
boolean fullCompaction =
- ManifestFileSorter.reachesFullCompactionThreshold(
- manifests,
- suggestedMetaSize,
- options.manifestFullCompactionThresholdSize().getBytes());
+ forceRewrite
+ || ManifestFileSorter.reachesFullCompactionThreshold(
+ manifests,
+ suggestedMetaSize,
+ options.manifestFullCompactionThresholdSize().getBytes());
ManifestFileSorter.ManifestSortKey sortKey =
ManifestFileSorter.createSortKey(
options.dataEvolutionEnabled(),
@@ -135,7 +137,8 @@ private static List buildLevelSortedRunsForDryRun(
// A full compaction with no work falls through to the minor path. Mirror that fallback so
// the reported levels describe the path which a real compaction would use.
- if (fullCompaction
+ if (!forceRewrite
+ && fullCompaction
&& classifyResult.compactWithoutSort.isEmpty()
&& new ManifestPickStrategy(
options.maxSizeAmplificationPercent(), options.sortedRunSizeRatio())
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 996fceaa97bf..25c7e7b3a2a4 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
@@ -417,7 +417,7 @@ public static void validateTableSchema(TableSchema schema, Set dynamicOp
validatePkClusteringOverride(options);
- validateManifestSort(schema, options);
+ validateManifestSort(schema, options, dynamicOptionKeys);
}
/**
@@ -2019,7 +2019,18 @@ public static void validatePkClusteringOverride(CoreOptions options) {
}
}
- private static void validateManifestSort(TableSchema schema, CoreOptions options) {
+ private static void validateManifestSort(
+ TableSchema schema, CoreOptions options, Set dynamicOptionKeys) {
+ for (ConfigOption> option :
+ Arrays.asList(
+ CoreOptions.MANIFEST_SORT_FORCE_REWRITE, CoreOptions.MANIFEST_SORT_ORDER)) {
+ checkArgument(
+ !schema.options().containsKey(option.key())
+ || dynamicOptionKeys.contains(option.key()),
+ "'%s' is only supported as a dynamic option for explicit manifest compaction.",
+ option.key());
+ }
+
if (options.manifestSortEnabled()) {
if (!options.dataEvolutionEnabled()) {
checkArgument(
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 7d0abc7c7d73..452f96d59d0d 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
@@ -1554,6 +1554,33 @@ public void testManifestSortForceRewriteAllLevelRuns() {
.containsExactly(0, 1, 2, 3);
}
+ @Test
+ public void testManifestSortForceRewriteDryRunUsesFullCompaction() {
+ List input =
+ Arrays.asList(
+ makeManifest(makeEntry(true, "base", 0)),
+ makeManifest(
+ makeEntry(false, "base", 0), makeEntry(true, "replacement", 0)));
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ testOptions.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1B");
+ testOptions.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(testOptions.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("Manifest sort level files: L0=0, L1=0, L2=0, L3=0, L4=0.");
+ }
+
@Test
public void testManifestSortForceRewriteSingleManifest() {
ManifestFileMeta physical =
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 c9588f4552fa..a891d8c1068d 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
@@ -36,6 +36,7 @@
import java.util.Map;
import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.apache.paimon.CoreOptions.BUCKET;
import static org.apache.paimon.CoreOptions.DATA_EVOLUTION_ENABLED;
@@ -2049,6 +2050,45 @@ void testManifestSortValidation() {
.hasMessageContaining("is not a partition field");
}
+ @Test
+ void testManifestSortMaintenanceOptionsAreDynamicOnly() {
+ List fields =
+ Arrays.asList(
+ new DataField(0, "f0", DataTypes.INT()),
+ new DataField(1, "f1", DataTypes.INT()));
+ Map baseOptions = new HashMap<>();
+ baseOptions.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), "true");
+ baseOptions.put(BUCKET.key(), "-1");
+
+ Map forceOptions = new HashMap<>(baseOptions);
+ forceOptions.put(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(), "true");
+ TableSchema forceSchema =
+ new TableSchema(1, fields, 10, singletonList("f0"), emptyList(), forceOptions, "");
+ assertThatThrownBy(() -> validateTableSchema(forceSchema))
+ .hasMessage(
+ "'manifest-sort.force-rewrite' is only supported as a dynamic option for explicit manifest compaction.");
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ validateTableSchema(
+ forceSchema,
+ singleton(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key())));
+
+ Map orderOptions = new HashMap<>(baseOptions);
+ orderOptions.put(CoreOptions.MANIFEST_SORT_ORDER.key(), "partition-first");
+ TableSchema orderSchema =
+ new TableSchema(1, fields, 10, singletonList("f0"), emptyList(), orderOptions, "");
+ assertThatThrownBy(() -> validateTableSchema(orderSchema))
+ .hasMessage(
+ "'manifest-sort.order' is only supported as a dynamic option for explicit manifest compaction.");
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ validateTableSchema(
+ orderSchema,
+ singleton(CoreOptions.MANIFEST_SORT_ORDER.key())));
+ }
+
@Test
public void testMergeOnReadCoexistsWithVisibilityCallback() {
Map options = new HashMap<>();
From dfcb41002e37e9e2d2904c34f7ff8fd0047f8a09 Mon Sep 17 00:00:00 2001
From: mingfeng
Date: Mon, 14 Sep 2026 13:55:33 +0800
Subject: [PATCH 3/6] [core] Validate explicit manifest sort order eagerly
---
.../paimon/schema/SchemaValidation.java | 10 +++++
.../paimon/schema/SchemaValidationTest.java | 42 +++++++++++++++++++
.../CompactManifestProcedureITCase.java | 9 ++++
3 files changed, 61 insertions(+)
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 25c7e7b3a2a4..72cb80f526b1 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
@@ -2031,6 +2031,16 @@ private static void validateManifestSort(
option.key());
}
+ CoreOptions.ManifestSortOrder sortOrder = options.manifestSortOrder();
+ checkArgument(
+ sortOrder == null || !options.dataEvolutionEnabled(),
+ "Explicit manifest sort order is not supported for data evolution tables.");
+ checkArgument(
+ sortOrder != CoreOptions.ManifestSortOrder.BUCKET_FIRST
+ || options.bucket() > 0
+ || options.bucket() == BucketMode.POSTPONE_BUCKET,
+ "Manifest sort order 'bucket-first' requires a bucketed table.");
+
if (options.manifestSortEnabled()) {
if (!options.dataEvolutionEnabled()) {
checkArgument(
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 a891d8c1068d..a1c31c24bbfb 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
@@ -2089,6 +2089,48 @@ void testManifestSortMaintenanceOptionsAreDynamicOnly() {
singleton(CoreOptions.MANIFEST_SORT_ORDER.key())));
}
+ @Test
+ void testManifestSortMaintenanceOrderValidation() {
+ List fields =
+ Arrays.asList(
+ new DataField(0, "f0", DataTypes.INT()),
+ new DataField(1, "f1", DataTypes.INT()));
+ Map options = new HashMap<>();
+ options.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), "true");
+ options.put(CoreOptions.MANIFEST_SORT_ORDER.key(), "bucket-first");
+ options.put(BUCKET.key(), "-1");
+
+ TableSchema schema =
+ new TableSchema(1, fields, 10, singletonList("f0"), emptyList(), options, "");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ schema, singleton(CoreOptions.MANIFEST_SORT_ORDER.key())))
+ .hasMessage("Manifest sort order 'bucket-first' requires a bucketed table.");
+
+ options.put(BUCKET.key(), "4");
+ options.put(CoreOptions.BUCKET_KEY.key(), "f1");
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ validateTableSchema(
+ schema.copy(options),
+ singleton(CoreOptions.MANIFEST_SORT_ORDER.key())));
+
+ options.put(CoreOptions.MANIFEST_SORT_ORDER.key(), "partition-first");
+ options.put(CoreOptions.DATA_EVOLUTION_ENABLED.key(), "true");
+ options.put(CoreOptions.ROW_TRACKING_ENABLED.key(), "true");
+ options.put(BUCKET.key(), "-1");
+ options.remove(CoreOptions.BUCKET_KEY.key());
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ schema.copy(options),
+ singleton(CoreOptions.MANIFEST_SORT_ORDER.key())))
+ .hasMessage(
+ "Explicit manifest sort order is not supported for data evolution tables.");
+ }
+
@Test
public void testMergeOnReadCoexistsWithVisibilityCallback() {
Map options = new HashMap<>();
diff --git a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
index cef9509018f1..4e892aab6ae9 100644
--- a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
+++ b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
@@ -179,6 +179,15 @@ public void testManifestSortParametersValidation() {
+ "`manifest_sort_order` => 'partition-first')"))
.hasStackTraceContaining(
"'manifest_sort_order' cannot be used with 'manifest_sort_enabled=false'.");
+
+ Assertions.assertThatThrownBy(
+ () ->
+ sql(
+ "CALL sys.compact_manifest("
+ + "`table` => 'default.T_INVALID', "
+ + "`manifest_sort_order` => 'bucket-first')"))
+ .hasStackTraceContaining(
+ "Manifest sort order 'bucket-first' requires a bucketed table.");
}
@Test
From 0c23e13f85b86ba9a6ffe3fb6b685a99c0c6b4eb Mon Sep 17 00:00:00 2001
From: mingfeng
Date: Mon, 14 Sep 2026 15:21:00 +0800
Subject: [PATCH 4/6] [core] Respect table options for forced manifest rewrite
---
docs/docs/flink/procedures/compaction.md | 19 +--
docs/docs/spark/procedures/maintenance.md | 22 +--
.../java/org/apache/paimon/CoreOptions.java | 51 +------
.../operation/ManifestCompactDryRun.java | 1 -
.../paimon/operation/ManifestFileSorter.java | 72 ++-------
.../paimon/schema/SchemaValidation.java | 23 +--
.../paimon/manifest/ManifestFileMetaTest.java | 143 ++++++++----------
.../paimon/schema/SchemaValidationTest.java | 58 +------
.../flink/action/CompactManifestAction.java | 3 +-
.../procedure/CompactManifestProcedure.java | 22 +--
.../CompactManifestProcedureITCase.java | 37 +----
.../procedure/CompactManifestProcedure.java | 19 +--
.../CompactManifestProcedureTest.scala | 3 +-
13 files changed, 101 insertions(+), 372 deletions(-)
diff --git a/docs/docs/flink/procedures/compaction.md b/docs/docs/flink/procedures/compaction.md
index ae8727594eec..22090db69850 100644
--- a/docs/docs/flink/procedures/compaction.md
+++ b/docs/docs/flink/procedures/compaction.md
@@ -250,11 +250,7 @@ To compact_manifest the manifests. Arguments:
- manifest_sort_max_rewrite_size (String, optional): maximum manifest size rewritten by one sort pass.
-- manifest_sort_order (String, optional): target layout for a one-shot rewrite. Supported values are `bucket-first` and `partition-first`. Setting it enables manifest sort and forces existing manifests to be rewritten. `bucket-first` requires a bucketed table, and explicit sort orders are not supported for data evolution tables.
-
-When `manifest_sort_order` is omitted, the existing layout selection remains unchanged: bucketed tables use bucket-first, non-bucket tables use partition-first, and data evolution tables use RowID sorting when RowID metadata is available.
-
-Set `manifest-sort.force-rewrite=true` in `options` together with `manifest_sort_enabled=true` to rewrite already compacted manifest runs using the current sort order. Use it only as a one-shot dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget semantics still apply; raise it to migrate more manifests in one invocation.
+Set `manifest-sort.force-rewrite=true` in `options` together with `manifest_sort_enabled=true` to rewrite already compacted manifest runs using the layout selected from the table options. Use it only as a one-shot dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget semantics still apply; raise it to migrate more manifests in one invocation.
**Syntax**
@@ -269,8 +265,7 @@ CALL [catalog.]sys.compact_manifest(
`table` => 'identifier',
`manifest_sort_enabled` => true,
`manifest_sort_partition_field` => 'dt',
- `manifest_sort_max_rewrite_size` => '1 gb',
- `manifest_sort_order` => 'partition-first'
+ `manifest_sort_max_rewrite_size` => '1 gb'
);
```
@@ -290,14 +285,8 @@ CALL sys.compact_manifest(
CALL sys.compact_manifest(
`table` => 'default.T',
- `manifest_sort_order` => 'partition-first',
- `manifest_sort_max_rewrite_size` => '1 gb'
-);
-
--- Switch the same bucketed table back to bucket-first layout.
-CALL sys.compact_manifest(
- `table` => 'default.T',
- `manifest_sort_order` => 'bucket-first',
+ `options` => 'manifest-sort.force-rewrite=true',
+ `manifest_sort_enabled` => true,
`manifest_sort_max_rewrite_size` => '1 gb'
);
```
diff --git a/docs/docs/spark/procedures/maintenance.md b/docs/docs/spark/procedures/maintenance.md
index e636a9f03569..9cabe8571323 100644
--- a/docs/docs/spark/procedures/maintenance.md
+++ b/docs/docs/spark/procedures/maintenance.md
@@ -126,16 +126,10 @@ Compact manifest files.
- `manifest_sort_enabled` (`BOOLEAN`, optional): whether to use manifest sort rewrite for this invocation.
- `manifest_sort_partition_field` (`STRING`, optional): partition field used to sort manifest entries. Defaults to the first partition field.
- `manifest_sort_max_rewrite_size` (`STRING`, optional): maximum manifest size rewritten by one sort pass.
-- `manifest_sort_order` (`STRING`, optional): target layout for a one-shot rewrite. Supported values are `bucket-first` and `partition-first`. Setting it enables manifest sort and forces existing manifests to be rewritten. `bucket-first` requires a bucketed table, and explicit sort orders are not supported for data evolution tables.
-
-When `manifest_sort_order` is omitted, the existing layout selection remains unchanged: bucketed
-tables use bucket-first, non-bucket tables use partition-first, and data evolution tables use RowID
-sorting when RowID metadata is available.
-
Set `manifest-sort.force-rewrite=true` in `options` together with `manifest_sort_enabled=true` to
-rewrite already compacted manifest runs using the current sort order. Use it only as a one-shot
-dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget semantics still apply;
-raise it to migrate more manifests in one invocation.
+rewrite already compacted manifest runs using the layout selected from the table options. Use it
+only as a one-shot dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget
+semantics still apply; raise it to migrate more manifests in one invocation.
```sql
CALL sys.compact_manifest(`table` => 'default.T');
@@ -151,14 +145,8 @@ CALL sys.compact_manifest(
CALL sys.compact_manifest(
`table` => 'default.T',
- manifest_sort_order => 'partition-first',
- manifest_sort_max_rewrite_size => '1 gb'
-);
-
--- Switch the same bucketed table back to bucket-first layout.
-CALL sys.compact_manifest(
- `table` => 'default.T',
- manifest_sort_order => 'bucket-first',
+ options => 'manifest-sort.force-rewrite=true',
+ manifest_sort_enabled => true,
manifest_sort_max_rewrite_size => '1 gb'
);
```
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 8e345dd998f3..f6f9892670f4 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -607,20 +607,12 @@ public InlineElement getDescription() {
.withDescription(
"When 'manifest-sort.enabled' is true, force an explicit manifest"
+ " compaction to rewrite already compacted manifest runs using"
- + " the configured manifest sort order."
+ + " the layout selected from the table options."
+ " The existing 'manifest-sort.max-rewrite-size' rewrite budget"
+ " semantics still apply."
+ " This should be supplied as a one-shot dynamic option for"
+ " maintenance, not persisted for routine writes.");
- @ExcludeFromDocumentation("Only used by compact_manifest maintenance procedure")
- public static final ConfigOption MANIFEST_SORT_ORDER =
- key("manifest-sort.order")
- .enumType(ManifestSortOrder.class)
- .noDefaultValue()
- .withDescription(
- "Target manifest layout for a one-shot manifest sort rewrite.");
-
public static final ConfigOption MANIFEST_MERGE_OPTIMIZE_ENABLED =
key("manifest.merge-optimize.enabled")
.booleanType()
@@ -3253,11 +3245,6 @@ public boolean manifestSortForceRewrite() {
return options.get(MANIFEST_SORT_FORCE_REWRITE);
}
- @Nullable
- public ManifestSortOrder manifestSortOrder() {
- return options.getOptional(MANIFEST_SORT_ORDER).orElse(null);
- }
-
public boolean manifestMergeOptimizeEnabled() {
return options.get(MANIFEST_MERGE_OPTIMIZE_ENABLED);
}
@@ -5462,42 +5449,6 @@ public static List> getOptions() {
})
.collect(Collectors.toSet());
- /** Target layout for an explicit manifest sort rewrite. */
- public enum ManifestSortOrder implements DescribedEnum {
- BUCKET_FIRST("bucket-first", "Sort manifest entries by bucket before partition."),
- PARTITION_FIRST("partition-first", "Sort manifest entries by partition.");
-
- private final String value;
- private final String description;
-
- ManifestSortOrder(String value, String description) {
- this.value = value;
- this.description = description;
- }
-
- public static ManifestSortOrder fromString(String value) {
- for (ManifestSortOrder order : values()) {
- if (order.value.equalsIgnoreCase(value.trim())) {
- return order;
- }
- }
- throw new IllegalArgumentException(
- String.format(
- "Unsupported manifest sort order '%s'. Supported values are 'bucket-first' and 'partition-first'.",
- value));
- }
-
- @Override
- public String toString() {
- return value;
- }
-
- @Override
- public InlineElement getDescription() {
- return text(description);
- }
- }
-
/** Specifies the sort engine for table with primary key. */
public enum SortEngine implements DescribedEnum {
MIN_HEAP("min-heap", "Use min-heap for multiway sorting."),
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 3c2abfbdb451..480871a4a6da 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
@@ -123,7 +123,6 @@ private static List buildLevelSortedRunsForDryRun(
manifests,
options.manifestSortPartitionField(),
partitionType,
- options.manifestSortOrder(),
options.bucket() > 0 || options.bucket() == BucketMode.POSTPONE_BUCKET);
ManifestFileSorter.ClassifyResult classifyResult =
ManifestFileSorter.classifyManifests(
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 0f1250696829..f6981de56577 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
@@ -159,7 +159,6 @@ static List trySortCompaction(
@Nullable IOManager ioManager)
throws Exception {
String sortPartitionField = options.manifestSortPartitionField();
- CoreOptions.ManifestSortOrder sortOrder = options.manifestSortOrder();
boolean bucketed = options.bucket() > 0 || options.bucket() == BucketMode.POSTPONE_BUCKET;
boolean runMergeOptimizeEnabled = options.manifestMergeOptimizeEnabled();
long suggestedMetaSize = options.manifestTargetSize().getBytes();
@@ -180,7 +179,6 @@ static List trySortCompaction(
manifestFile,
partitionType,
sortPartitionField,
- sortOrder,
bucketed,
options.dataEvolutionEnabled(),
runMergeOptimizeEnabled,
@@ -202,7 +200,6 @@ static List trySortCompaction(
manifestFile,
partitionType,
sortPartitionField,
- sortOrder,
bucketed,
options.dataEvolutionEnabled(),
runMergeOptimizeEnabled,
@@ -227,7 +224,6 @@ private static Optional> tryFullCompaction(
ManifestFile manifestFile,
RowType partitionType,
String sortPartitionField,
- @Nullable CoreOptions.ManifestSortOrder sortOrder,
boolean bucketed,
boolean dataEvolutionEnabled,
boolean runMergeOptimizeEnabled,
@@ -256,7 +252,6 @@ private static Optional> tryFullCompaction(
manifestFile,
partitionType,
sortPartitionField,
- sortOrder,
bucketed,
dataEvolutionEnabled,
runMergeOptimizeEnabled,
@@ -302,7 +297,7 @@ private static Optional> tryFullCompaction(
// Step 4: Split into sections and merge small adjacent sections. A forced rewrite
// intentionally uses one global section so entries from different already-compacted
- // manifests can be clustered using the current sort order.
+ // manifests can be clustered using the layout selected from the table options.
List sections;
if (forceRewrite) {
long totalSize = 0L;
@@ -358,7 +353,6 @@ private static List tryMinorCompaction(
ManifestFile manifestFile,
RowType partitionType,
String sortPartitionField,
- @Nullable CoreOptions.ManifestSortOrder sortOrder,
boolean bucketed,
boolean dataEvolutionEnabled,
boolean runMergeOptimizeEnabled,
@@ -379,7 +373,6 @@ private static List tryMinorCompaction(
manifestFile,
partitionType,
sortPartitionField,
- sortOrder,
bucketed,
dataEvolutionEnabled,
runMergeOptimizeEnabled,
@@ -496,7 +489,6 @@ private static CompactionContext prepareCompaction(
ManifestFile manifestFile,
RowType partitionType,
String sortPartitionField,
- @Nullable CoreOptions.ManifestSortOrder sortOrder,
boolean bucketed,
boolean dataEvolutionEnabled,
boolean runMergeOptimizeEnabled,
@@ -511,12 +503,7 @@ private static CompactionContext prepareCompaction(
// Step 1: Resolve sort key. Data evolution tables prefer RowID ranges when available.
ManifestSortKey sortKey =
createSortKey(
- dataEvolutionEnabled,
- input,
- sortPartitionField,
- partitionType,
- sortOrder,
- bucketed);
+ dataEvolutionEnabled, input, sortPartitionField, partitionType, bucketed);
// Step 2: Classify manifests into LSM files and collect delete entries.
ClassifyResult classification =
@@ -904,18 +891,6 @@ private static void rewriteSections(
for (int i = 0; i < sections.size(); i++) {
Section section = sections.get(i);
- // A single-file section is always handled directly, regardless of the budget.
- if (section.files.size() == 1) {
- rewriteSection(
- section.files,
- output,
- sortNewFiles,
- ctx,
- manifestFile,
- manifestReadParallelism);
- continue;
- }
-
// Phase 1: budget not yet exhausted -- perform aggressive sort rewrite.
if (!budgetExhausted) {
// Phase 1a: section fits within the remaining budget -- sort and rewrite it
@@ -928,7 +903,8 @@ private static void rewriteSections(
sortNewFiles,
ctx,
manifestFile,
- manifestReadParallelism);
+ manifestReadParallelism,
+ true);
} else {
// Phase 1b: first overflow -- split the section at the budget boundary,
// rewrite the affordable head, and append the remaining tail back for later
@@ -1008,7 +984,8 @@ private static Section splitSectionAndRewriteHead(
}
}
- rewriteSection(headFiles, output, sortNewFiles, ctx, manifestFile, manifestReadParallelism);
+ rewriteSection(
+ headFiles, output, sortNewFiles, ctx, manifestFile, manifestReadParallelism, true);
if (tailFiles.isEmpty()) {
return null;
@@ -1086,7 +1063,8 @@ private static void unsortedCompactSection(
sortNewFiles,
ctx,
manifestFile,
- manifestReadParallelism);
+ manifestReadParallelism,
+ false);
candidates.clear();
candidatesSize = 0;
}
@@ -1100,7 +1078,8 @@ private static void unsortedCompactSection(
sortNewFiles,
ctx,
manifestFile,
- manifestReadParallelism);
+ manifestReadParallelism,
+ false);
} else {
output.addAllUnchanged(candidates);
}
@@ -1119,11 +1098,12 @@ private static void rewriteSection(
List sortNewFiles,
CompactionContext ctx,
ManifestFile manifestFile,
- @Nullable Integer manifestReadParallelism)
+ @Nullable Integer manifestReadParallelism,
+ boolean allowForceRewrite)
throws Exception {
// Skip rewrite for single file not in delete-range.
if (section.size() == 1
- && !ctx.forceRewrite
+ && !(allowForceRewrite && ctx.forceRewrite)
&& !ctx.defaultCompactFiles.getOrDefault(section.get(0), false)) {
output.addUnchanged(section.get(0));
return;
@@ -1239,27 +1219,6 @@ static ManifestSortKey createSortKey(
String sortPartitionField,
RowType partitionType,
boolean bucketed) {
- return createSortKey(
- dataEvolutionEnabled, input, sortPartitionField, partitionType, null, bucketed);
- }
-
- static ManifestSortKey createSortKey(
- boolean dataEvolutionEnabled,
- List input,
- String sortPartitionField,
- RowType partitionType,
- @Nullable CoreOptions.ManifestSortOrder sortOrder,
- boolean bucketed) {
- if (sortOrder != null && dataEvolutionEnabled) {
- throw new IllegalArgumentException(
- "Explicit manifest sort order is not supported for data evolution tables.");
- }
-
- if (sortOrder == CoreOptions.ManifestSortOrder.BUCKET_FIRST && !bucketed) {
- throw new IllegalArgumentException(
- "Manifest sort order 'bucket-first' requires a bucketed table.");
- }
-
boolean rowIdSort = dataEvolutionEnabled && ManifestFileMeta.allContainsRowId(input);
if (rowIdSort) {
// RowID sorting uses the configured partition field as the primary key when specified,
@@ -1289,10 +1248,7 @@ static ManifestSortKey createSortKey(
RecordComparator fieldComparator =
CodeGenUtils.newRecordComparator(
partitionType.getFieldTypes(), new int[] {sortFieldIndex});
- boolean useBucketSort =
- sortOrder == CoreOptions.ManifestSortOrder.BUCKET_FIRST
- || (sortOrder == null && bucketed);
- if (useBucketSort) {
+ if (bucketed) {
boolean compareManifestBuckets =
input.stream()
.allMatch(meta -> meta.minBucket() != null && meta.maxBucket() != null);
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 72cb80f526b1..8144112db6c5 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
@@ -2021,25 +2021,12 @@ public static void validatePkClusteringOverride(CoreOptions options) {
private static void validateManifestSort(
TableSchema schema, CoreOptions options, Set dynamicOptionKeys) {
- for (ConfigOption> option :
- Arrays.asList(
- CoreOptions.MANIFEST_SORT_FORCE_REWRITE, CoreOptions.MANIFEST_SORT_ORDER)) {
- checkArgument(
- !schema.options().containsKey(option.key())
- || dynamicOptionKeys.contains(option.key()),
- "'%s' is only supported as a dynamic option for explicit manifest compaction.",
- option.key());
- }
-
- CoreOptions.ManifestSortOrder sortOrder = options.manifestSortOrder();
- checkArgument(
- sortOrder == null || !options.dataEvolutionEnabled(),
- "Explicit manifest sort order is not supported for data evolution tables.");
checkArgument(
- sortOrder != CoreOptions.ManifestSortOrder.BUCKET_FIRST
- || options.bucket() > 0
- || options.bucket() == BucketMode.POSTPONE_BUCKET,
- "Manifest sort order 'bucket-first' requires a bucketed table.");
+ !schema.options().containsKey(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key())
+ || dynamicOptionKeys.contains(
+ CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key()),
+ "'%s' is only supported as a dynamic option for explicit manifest compaction.",
+ CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key());
if (options.manifestSortEnabled()) {
if (!options.dataEvolutionEnabled()) {
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 452f96d59d0d..0b6a34ba57a4 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
@@ -1416,86 +1416,6 @@ public void testManifestSortForceRewriteAlreadyCompactedRuns() {
assertThat(afterMigration).containsExactlyElementsOf(rewritten);
}
- @Test
- public void testManifestSortForceRewriteSwitchesLayout() {
- List input =
- Arrays.asList(
- makeManifest(
- makeBucketEntry("a-p0-b3", 0, 3), makeBucketEntry("a-p1-b1", 1, 1)),
- makeManifest(
- makeBucketEntry("b-p0-b2", 0, 2),
- makeBucketEntry("b-p1-b0", 1, 0)));
-
- Options testOptions = new Options();
- testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1G");
- testOptions.set(CoreOptions.BUCKET, 4);
- testOptions.set(
- CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.PARTITION_FIRST);
-
- List partitionFirst =
- ManifestFileMerger.merge(
- input,
- manifestFile,
- getPartitionType(),
- CoreOptions.fromMap(testOptions.toMap()));
- assertEquivalentEntries(input, partitionFirst);
- assertThat(readEntries(partitionFirst))
- .extracting(entry -> entry.partition().getInt(0))
- .containsExactly(0, 0, 1, 1);
-
- testOptions.set(
- CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.BUCKET_FIRST);
- List bucketFirst =
- ManifestFileMerger.merge(
- partitionFirst,
- manifestFile,
- getPartitionType(),
- CoreOptions.fromMap(testOptions.toMap()));
- assertEquivalentEntries(partitionFirst, bucketFirst);
- assertThat(readEntries(bucketFirst))
- .extracting(ManifestEntry::bucket)
- .containsExactly(0, 1, 2, 3);
- }
-
- @Test
- public void testManifestSortExplicitOrderValidation() {
- List input =
- Collections.singletonList(makeManifest(makeBucketEntry("file", 0, 0)));
-
- Options testOptions = new Options();
- testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
- testOptions.set(
- CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.BUCKET_FIRST);
- assertThat(
- assertThrows(
- IllegalArgumentException.class,
- () ->
- ManifestFileMerger.merge(
- input,
- manifestFile,
- getPartitionType(),
- CoreOptions.fromMap(testOptions.toMap()))))
- .hasMessage("Manifest sort order 'bucket-first' requires a bucketed table.");
-
- testOptions.set(
- CoreOptions.MANIFEST_SORT_ORDER, CoreOptions.ManifestSortOrder.PARTITION_FIRST);
- testOptions.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
- assertThat(
- assertThrows(
- IllegalArgumentException.class,
- () ->
- ManifestFileMerger.merge(
- input,
- manifestFile,
- getPartitionType(),
- CoreOptions.fromMap(testOptions.toMap()))))
- .hasMessage(
- "Explicit manifest sort order is not supported for data evolution tables.");
- }
-
@Test
public void testManifestSortForceRewriteAllLevelRuns() {
List physical =
@@ -1637,6 +1557,69 @@ public void testManifestSortForceRewriteRespectsRewriteBudget() {
assertEquivalentEntries(input, rewritten);
}
+ @Test
+ public void testManifestSortForceRewriteDoesNotExceedBudgetForSingletonTail() {
+ long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
+ List input = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ input.add(
+ copyWithFileSize(makeManifest(makeBucketEntry("file-" + i, 0, i)), targetSize));
+ }
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1B");
+ testOptions.set(CoreOptions.BUCKET, 4);
+ List rewritten =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+
+ Set inputNames =
+ input.stream().map(ManifestFileMeta::fileName).collect(Collectors.toSet());
+ assertThat(rewritten)
+ .extracting(ManifestFileMeta::fileName)
+ .filteredOn(inputNames::contains)
+ .hasSize(1);
+ assertEquivalentEntries(input, rewritten);
+ }
+
+ @Test
+ public void testManifestSortForceRewriteDoesNotRewriteTailBeyondBudget() {
+ long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
+ List input = new ArrayList<>();
+ for (int i = 0; i < 4; i++) {
+ input.add(
+ copyWithFileSize(makeManifest(makeBucketEntry("file-" + i, 0, i)), targetSize));
+ }
+ input.add(
+ copyWithFileSize(
+ makeManifest(makeBucketEntry("small-file", 0, 4)), targetSize - 1));
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1B");
+ testOptions.set(CoreOptions.BUCKET, 8);
+ List rewritten =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+
+ Set inputNames =
+ input.stream().map(ManifestFileMeta::fileName).collect(Collectors.toSet());
+ assertThat(rewritten)
+ .extracting(ManifestFileMeta::fileName)
+ .filteredOn(inputNames::contains)
+ .hasSize(3);
+ assertEquivalentEntries(input, rewritten);
+ }
+
@ParameterizedTest
@ValueSource(ints = {-1, 4, -2})
public void testManifestSortDryRunUsesBucketRangesForBucketedTable(int bucket) {
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 a1c31c24bbfb..f66d623a0e23 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
@@ -2051,7 +2051,7 @@ void testManifestSortValidation() {
}
@Test
- void testManifestSortMaintenanceOptionsAreDynamicOnly() {
+ void testManifestSortForceRewriteIsDynamicOnly() {
List fields =
Arrays.asList(
new DataField(0, "f0", DataTypes.INT()),
@@ -2073,62 +2073,6 @@ void testManifestSortMaintenanceOptionsAreDynamicOnly() {
validateTableSchema(
forceSchema,
singleton(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key())));
-
- Map orderOptions = new HashMap<>(baseOptions);
- orderOptions.put(CoreOptions.MANIFEST_SORT_ORDER.key(), "partition-first");
- TableSchema orderSchema =
- new TableSchema(1, fields, 10, singletonList("f0"), emptyList(), orderOptions, "");
- assertThatThrownBy(() -> validateTableSchema(orderSchema))
- .hasMessage(
- "'manifest-sort.order' is only supported as a dynamic option for explicit manifest compaction.");
- assertThatNoException()
- .isThrownBy(
- () ->
- validateTableSchema(
- orderSchema,
- singleton(CoreOptions.MANIFEST_SORT_ORDER.key())));
- }
-
- @Test
- void testManifestSortMaintenanceOrderValidation() {
- List fields =
- Arrays.asList(
- new DataField(0, "f0", DataTypes.INT()),
- new DataField(1, "f1", DataTypes.INT()));
- Map options = new HashMap<>();
- options.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), "true");
- options.put(CoreOptions.MANIFEST_SORT_ORDER.key(), "bucket-first");
- options.put(BUCKET.key(), "-1");
-
- TableSchema schema =
- new TableSchema(1, fields, 10, singletonList("f0"), emptyList(), options, "");
- assertThatThrownBy(
- () ->
- validateTableSchema(
- schema, singleton(CoreOptions.MANIFEST_SORT_ORDER.key())))
- .hasMessage("Manifest sort order 'bucket-first' requires a bucketed table.");
-
- options.put(BUCKET.key(), "4");
- options.put(CoreOptions.BUCKET_KEY.key(), "f1");
- assertThatNoException()
- .isThrownBy(
- () ->
- validateTableSchema(
- schema.copy(options),
- singleton(CoreOptions.MANIFEST_SORT_ORDER.key())));
-
- options.put(CoreOptions.MANIFEST_SORT_ORDER.key(), "partition-first");
- options.put(CoreOptions.DATA_EVOLUTION_ENABLED.key(), "true");
- options.put(CoreOptions.ROW_TRACKING_ENABLED.key(), "true");
- options.put(BUCKET.key(), "-1");
- options.remove(CoreOptions.BUCKET_KEY.key());
- assertThatThrownBy(
- () ->
- validateTableSchema(
- schema.copy(options),
- singleton(CoreOptions.MANIFEST_SORT_ORDER.key())))
- .hasMessage(
- "Explicit manifest sort order is not supported for data evolution tables.");
}
@Test
diff --git a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java
index 7c0c123339eb..4b70de86dbf3 100644
--- a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java
+++ b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/CompactManifestAction.java
@@ -69,8 +69,7 @@ public void executeLocally() throws Exception {
dryRun,
manifestSortEnabled,
manifestSortPartitionField,
- manifestSortMaxRewriteSize,
- null);
+ manifestSortMaxRewriteSize);
for (String result : results) {
LOG.info(result);
}
diff --git a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
index a12c9abcdb8b..1bae106b9f6b 100644
--- a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
+++ b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/CompactManifestProcedure.java
@@ -59,10 +59,6 @@ public String identifier() {
@ArgumentHint(
name = "manifest_sort_max_rewrite_size",
type = @DataTypeHint("STRING"),
- isOptional = true),
- @ArgumentHint(
- name = "manifest_sort_order",
- type = @DataTypeHint("STRING"),
isOptional = true)
})
public String[] call(
@@ -72,8 +68,7 @@ public String[] call(
@Nullable Boolean dryRun,
@Nullable Boolean manifestSortEnabled,
@Nullable String manifestSortPartitionField,
- @Nullable String manifestSortMaxRewriteSize,
- @Nullable String manifestSortOrder)
+ @Nullable String manifestSortMaxRewriteSize)
throws Exception {
FileStoreTable table = (FileStoreTable) table(tableId);
@@ -93,21 +88,6 @@ public String[] call(
dynamicOptions.put(
CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), manifestSortMaxRewriteSize);
}
- if (manifestSortOrder != null) {
- if (Boolean.FALSE.equals(manifestSortEnabled)
- || "false"
- .equalsIgnoreCase(
- dynamicOptions.get(CoreOptions.MANIFEST_SORT_ENABLED.key()))) {
- throw new IllegalArgumentException(
- "'manifest_sort_order' cannot be used with 'manifest_sort_enabled=false'.");
- }
- CoreOptions.ManifestSortOrder order =
- CoreOptions.ManifestSortOrder.fromString(manifestSortOrder);
- dynamicOptions.put(CoreOptions.MANIFEST_SORT_ORDER.key(), order.toString());
- dynamicOptions.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), Boolean.TRUE.toString());
- dynamicOptions.put(
- CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(), Boolean.TRUE.toString());
- }
table = table.copy(dynamicOptions);
diff --git a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
index 4e892aab6ae9..213e6b73afb4 100644
--- a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
+++ b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
@@ -128,16 +128,15 @@ public void testManifestSortParameters() throws Exception {
String forceRewriteProcedure =
"CALL sys.compact_manifest("
+ "`table` => 'default.T_SORT', "
+ + "`options` => 'manifest-sort.force-rewrite=true', "
+ + "`manifest_sort_enabled` => true, "
+ "`manifest_sort_partition_field` => 'dt', "
- + "`manifest_sort_max_rewrite_size` => '1 gb', "
- + "`manifest_sort_order` => 'partition-first')";
+ + "`manifest_sort_max_rewrite_size` => '1 gb')";
sql(forceRewriteProcedure);
long forceRewriteSnapshotId = table.snapshotManager().latestSnapshot().id();
Assertions.assertThat(forceRewriteSnapshotId).isEqualTo(compactSnapshotId + 1);
Assertions.assertThat(paimonTable("T_SORT").options())
- .doesNotContainKeys(
- CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(),
- CoreOptions.MANIFEST_SORT_ORDER.key());
+ .doesNotContainKey(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key());
sql(procedure);
Assertions.assertThat(table.snapshotManager().latestSnapshot().id())
@@ -160,34 +159,6 @@ public void testManifestSortParametersValidation() {
+ "`manifest_sort_partition_field` => 'missing')"))
.hasStackTraceContaining(
"'manifest-sort.partition-field' = 'missing' is not a partition field");
-
- Assertions.assertThatThrownBy(
- () ->
- sql(
- "CALL sys.compact_manifest("
- + "`table` => 'default.T_INVALID', "
- + "`manifest_sort_order` => 'unknown')"))
- .hasStackTraceContaining(
- "Unsupported manifest sort order 'unknown'. Supported values are 'bucket-first' and 'partition-first'.");
-
- Assertions.assertThatThrownBy(
- () ->
- sql(
- "CALL sys.compact_manifest("
- + "`table` => 'default.T_INVALID', "
- + "`manifest_sort_enabled` => false, "
- + "`manifest_sort_order` => 'partition-first')"))
- .hasStackTraceContaining(
- "'manifest_sort_order' cannot be used with 'manifest_sort_enabled=false'.");
-
- Assertions.assertThatThrownBy(
- () ->
- sql(
- "CALL sys.compact_manifest("
- + "`table` => 'default.T_INVALID', "
- + "`manifest_sort_order` => 'bucket-first')"))
- .hasStackTraceContaining(
- "Manifest sort order 'bucket-first' requires a bucketed table.");
}
@Test
diff --git a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
index 89c15761d3b8..25b417e5b0a7 100644
--- a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
+++ b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactManifestProcedure.java
@@ -59,8 +59,7 @@ public class CompactManifestProcedure extends BaseProcedure {
ProcedureParameter.optional("dry_run", BooleanType),
ProcedureParameter.optional("manifest_sort_enabled", BooleanType),
ProcedureParameter.optional("manifest_sort_partition_field", StringType),
- ProcedureParameter.optional("manifest_sort_max_rewrite_size", StringType),
- ProcedureParameter.optional("manifest_sort_order", StringType)
+ ProcedureParameter.optional("manifest_sort_max_rewrite_size", StringType)
};
private static final StructType OUTPUT_TYPE =
@@ -92,7 +91,6 @@ public InternalRow[] call(InternalRow args) {
Boolean manifestSortEnabled = args.isNullAt(3) ? null : args.getBoolean(3);
String manifestSortPartitionField = args.isNullAt(4) ? null : args.getString(4);
String manifestSortMaxRewriteSize = args.isNullAt(5) ? null : args.getString(5);
- String manifestSortOrder = args.isNullAt(6) ? null : args.getString(6);
Table table = loadSparkTable(tableIdent).getTable();
HashMap dynamicOptions = new HashMap<>();
@@ -109,21 +107,6 @@ public InternalRow[] call(InternalRow args) {
dynamicOptions.put(
CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), manifestSortMaxRewriteSize);
}
- if (manifestSortOrder != null) {
- if (Boolean.FALSE.equals(manifestSortEnabled)
- || "false"
- .equalsIgnoreCase(
- dynamicOptions.get(CoreOptions.MANIFEST_SORT_ENABLED.key()))) {
- throw new IllegalArgumentException(
- "'manifest_sort_order' cannot be used with 'manifest_sort_enabled=false'.");
- }
- CoreOptions.ManifestSortOrder order =
- CoreOptions.ManifestSortOrder.fromString(manifestSortOrder);
- dynamicOptions.put(CoreOptions.MANIFEST_SORT_ORDER.key(), order.toString());
- dynamicOptions.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), Boolean.TRUE.toString());
- dynamicOptions.put(
- CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(), Boolean.TRUE.toString());
- }
table = table.copy(dynamicOptions);
if (dryRun) {
diff --git a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
index 4dab5d46484d..76368e6f240c 100644
--- a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
+++ b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactManifestProcedureTest.scala
@@ -69,8 +69,7 @@ class CompactManifestProcedureTest extends PaimonSparkTestBase with StreamTest {
"dry_run => true, " +
"manifest_sort_enabled => true, " +
"manifest_sort_partition_field => 'dt', " +
- "manifest_sort_max_rewrite_size => '1gb', " +
- "manifest_sort_order => 'partition-first')")
+ "manifest_sort_max_rewrite_size => '1gb')")
.collectAsList()
Assertions.assertThat(dryRunRows.get(0).getBoolean(0)).isTrue
From 2b4a1a18f162802cfd4747af82de772f7eb676bf Mon Sep 17 00:00:00 2001
From: mingfeng
Date: Mon, 14 Sep 2026 16:22:53 +0800
Subject: [PATCH 5/6] [core] Make compact_manifest perform full manifest sort
---
docs/docs/flink/procedures/compaction.md | 11 +---
docs/docs/spark/procedures/maintenance.md | 14 +----
.../java/org/apache/paimon/CoreOptions.java | 18 ------
.../paimon/operation/FileStoreCommitImpl.java | 20 +++---
.../operation/ManifestCompactDryRun.java | 13 ++--
.../paimon/operation/ManifestFileMerger.java | 18 +++++-
.../paimon/operation/ManifestFileSorter.java | 39 ++++++------
.../paimon/schema/SchemaValidation.java | 12 +---
.../paimon/manifest/ManifestFileMetaTest.java | 62 +++++--------------
.../paimon/operation/FileStoreCommitTest.java | 12 ++--
.../operation/ManifestFileMergerTest.java | 4 +-
.../ManifestFileMergerTestUtils.java | 40 ++++++++++++
.../paimon/schema/SchemaValidationTest.java | 26 --------
.../CompactManifestProcedureITCase.java | 20 +-----
14 files changed, 118 insertions(+), 191 deletions(-)
create mode 100644 paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTestUtils.java
diff --git a/docs/docs/flink/procedures/compaction.md b/docs/docs/flink/procedures/compaction.md
index 22090db69850..5b26f1dfdab4 100644
--- a/docs/docs/flink/procedures/compaction.md
+++ b/docs/docs/flink/procedures/compaction.md
@@ -250,7 +250,9 @@ To compact_manifest the manifests. Arguments:
- manifest_sort_max_rewrite_size (String, optional): maximum manifest size rewritten by one sort pass.
-Set `manifest-sort.force-rewrite=true` in `options` together with `manifest_sort_enabled=true` to rewrite already compacted manifest runs using the layout selected from the table options. Use it only as a one-shot dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget semantics still apply; raise it to migrate more manifests in one invocation.
+When manifest sort is enabled, `compact_manifest` performs a full sort using the layout selected
+from the table options. The existing `manifest_sort_max_rewrite_size` limit still controls the
+amount of manifest data rewritten in one invocation.
**Syntax**
@@ -282,13 +284,6 @@ CALL sys.compact_manifest(
`manifest_sort_partition_field` => 'dt',
`manifest_sort_max_rewrite_size` => '1 gb'
);
-
-CALL sys.compact_manifest(
- `table` => 'default.T',
- `options` => 'manifest-sort.force-rewrite=true',
- `manifest_sort_enabled` => true,
- `manifest_sort_max_rewrite_size` => '1 gb'
-);
```
## rescale
diff --git a/docs/docs/spark/procedures/maintenance.md b/docs/docs/spark/procedures/maintenance.md
index 9cabe8571323..9952a70cc552 100644
--- a/docs/docs/spark/procedures/maintenance.md
+++ b/docs/docs/spark/procedures/maintenance.md
@@ -126,10 +126,9 @@ Compact manifest files.
- `manifest_sort_enabled` (`BOOLEAN`, optional): whether to use manifest sort rewrite for this invocation.
- `manifest_sort_partition_field` (`STRING`, optional): partition field used to sort manifest entries. Defaults to the first partition field.
- `manifest_sort_max_rewrite_size` (`STRING`, optional): maximum manifest size rewritten by one sort pass.
-Set `manifest-sort.force-rewrite=true` in `options` together with `manifest_sort_enabled=true` to
-rewrite already compacted manifest runs using the layout selected from the table options. Use it
-only as a one-shot dynamic option. The existing `manifest_sort_max_rewrite_size` rewrite budget
-semantics still apply; raise it to migrate more manifests in one invocation.
+When manifest sort is enabled, `compact_manifest` performs a full sort using the layout selected
+from the table options. The existing `manifest_sort_max_rewrite_size` limit still controls the
+amount of manifest data rewritten in one invocation.
```sql
CALL sys.compact_manifest(`table` => 'default.T');
@@ -142,13 +141,6 @@ CALL sys.compact_manifest(
manifest_sort_partition_field => 'dt',
manifest_sort_max_rewrite_size => '1 gb'
);
-
-CALL sys.compact_manifest(
- `table` => 'default.T',
- options => 'manifest-sort.force-rewrite=true',
- manifest_sort_enabled => true,
- manifest_sort_max_rewrite_size => '1 gb'
-);
```
## materialize_deletion_vectors
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 f6f9892670f4..7b0665c50296 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -599,20 +599,6 @@ public InlineElement getDescription() {
+ " skipped. Set to a larger value to allow more aggressive"
+ " sort rewriting. The cap only limits the sorted rewrite portion and full/minor cleanup may still happen beyond it.");
- @ExcludeFromDocumentation("Only used by compact_manifest maintenance procedure")
- public static final ConfigOption MANIFEST_SORT_FORCE_REWRITE =
- key("manifest-sort.force-rewrite")
- .booleanType()
- .defaultValue(false)
- .withDescription(
- "When 'manifest-sort.enabled' is true, force an explicit manifest"
- + " compaction to rewrite already compacted manifest runs using"
- + " the layout selected from the table options."
- + " The existing 'manifest-sort.max-rewrite-size' rewrite budget"
- + " semantics still apply."
- + " This should be supplied as a one-shot dynamic option for"
- + " maintenance, not persisted for routine writes.");
-
public static final ConfigOption MANIFEST_MERGE_OPTIMIZE_ENABLED =
key("manifest.merge-optimize.enabled")
.booleanType()
@@ -3241,10 +3227,6 @@ public long manifestSortMaxRewriteSize() {
return options.get(MANIFEST_SORT_MAX_REWRITE_SIZE).getBytes();
}
- public boolean manifestSortForceRewrite() {
- return options.get(MANIFEST_SORT_FORCE_REWRITE);
- }
-
public boolean manifestMergeOptimizeEnabled() {
return options.get(MANIFEST_MERGE_OPTIMIZE_ENABLED);
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
index 2325d85ff7ed..0f60d6a3cb7c 100644
--- a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
+++ b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
@@ -1615,8 +1615,9 @@ private boolean compactManifestOnce() {
mergeBeforeManifests,
manifestFile,
partitionType,
- manifestCompactionOptions(options, mergeBeforeManifests, partitionType),
- ioManager);
+ manifestCompactionOptions(options),
+ ioManager,
+ true);
if (new HashSet<>(mergeBeforeManifests).equals(new HashSet<>(mergeAfterManifests))) {
// no need to commit this snapshot, because no compact were happened
@@ -1655,17 +1656,12 @@ private boolean compactManifestOnce() {
return commitSnapshotImpl(latestSnapshot, newSnapshot, emptyList());
}
- static CoreOptions manifestCompactionOptions(
- CoreOptions options, List manifests, RowType partitionType) {
- // Use a copied options with forced full compaction settings for the legacy merge path.
- // Manifest sort has its own full/minor picking strategy and should respect its configured
- // thresholds.
+ static CoreOptions manifestCompactionOptions(CoreOptions options) {
+ // Use copied options so explicit manifest compaction always takes the full-compaction path
+ // without changing the table options used by regular commits.
Options compactOptions = Options.fromMap(options.toMap());
- if (!ManifestFileMerger.canUseManifestSort(manifests, partitionType, options)) {
- compactOptions.set(CoreOptions.MANIFEST_MERGE_MIN_COUNT, 1);
- compactOptions.set(
- CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE, MemorySize.ofBytes(1));
- }
+ compactOptions.set(CoreOptions.MANIFEST_MERGE_MIN_COUNT, 1);
+ compactOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE, MemorySize.ofBytes(1));
return new CoreOptions(compactOptions);
}
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 480871a4a6da..f80554ce6ea4 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
@@ -110,13 +110,11 @@ private static List buildLevelSortedRunsForDryRun(
RowType partitionType,
CoreOptions options) {
long suggestedMetaSize = options.manifestTargetSize().getBytes();
- boolean forceRewrite = options.manifestSortForceRewrite();
boolean fullCompaction =
- forceRewrite
- || ManifestFileSorter.reachesFullCompactionThreshold(
- manifests,
- suggestedMetaSize,
- options.manifestFullCompactionThresholdSize().getBytes());
+ ManifestFileSorter.reachesFullCompactionThreshold(
+ manifests,
+ suggestedMetaSize,
+ options.manifestFullCompactionThresholdSize().getBytes());
ManifestFileSorter.ManifestSortKey sortKey =
ManifestFileSorter.createSortKey(
options.dataEvolutionEnabled(),
@@ -136,8 +134,7 @@ private static List buildLevelSortedRunsForDryRun(
// A full compaction with no work falls through to the minor path. Mirror that fallback so
// the reported levels describe the path which a real compaction would use.
- if (!forceRewrite
- && fullCompaction
+ if (fullCompaction
&& classifyResult.compactWithoutSort.isEmpty()
&& new ManifestPickStrategy(
options.maxSizeAmplificationPercent(), options.sortedRunSizeRatio())
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 e3f8c7af7671..1dd630d077b3 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
@@ -57,6 +57,16 @@ public static List merge(
RowType partitionType,
CoreOptions options,
@Nullable IOManager ioManager) {
+ return merge(input, manifestFile, partitionType, options, ioManager, false);
+ }
+
+ static List merge(
+ List input,
+ ManifestFile manifestFile,
+ RowType partitionType,
+ CoreOptions options,
+ @Nullable IOManager ioManager,
+ boolean fullCompaction) {
// these are the newly created manifest files, clean them up if exception occurs
List newFilesForAbort = new ArrayList<>();
@@ -66,7 +76,13 @@ public static List merge(
// RowID ranges, so they do not require partition fields.
if (canUseManifestSort(input, partitionType, options)) {
return ManifestFileSorter.trySortCompaction(
- input, newFilesForAbort, manifestFile, partitionType, options, ioManager);
+ input,
+ newFilesForAbort,
+ manifestFile,
+ partitionType,
+ options,
+ ioManager,
+ fullCompaction);
}
if (options.manifestMergeOptimizeEnabled()) {
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 f6981de56577..e581f2f301c2 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
@@ -71,7 +71,7 @@ public class ManifestFileSorter {
/** Context object that carries shared state across compaction methods. */
static class CompactionContext {
final boolean fullCompaction;
- final boolean forceRewrite;
+ final boolean fullSort;
final boolean runMergeOptimizeEnabled;
final ManifestSortKey sortKey;
final RowType partitionType;
@@ -92,7 +92,7 @@ static class CompactionContext {
CompactionContext(
boolean fullCompaction,
- boolean forceRewrite,
+ boolean fullSort,
boolean runMergeOptimizeEnabled,
ManifestSortKey sortKey,
RowType partitionType,
@@ -102,7 +102,7 @@ static class CompactionContext {
List levelRuns,
List pickedRuns) {
this.fullCompaction = fullCompaction;
- this.forceRewrite = forceRewrite;
+ this.fullSort = fullSort;
this.runMergeOptimizeEnabled = runMergeOptimizeEnabled;
this.sortKey = sortKey;
this.partitionType = partitionType;
@@ -156,7 +156,8 @@ static List trySortCompaction(
ManifestFile manifestFile,
RowType partitionType,
CoreOptions options,
- @Nullable IOManager ioManager)
+ @Nullable IOManager ioManager,
+ boolean fullSort)
throws Exception {
String sortPartitionField = options.manifestSortPartitionField();
boolean bucketed = options.bucket() > 0 || options.bucket() == BucketMode.POSTPONE_BUCKET;
@@ -164,7 +165,6 @@ static List trySortCompaction(
long suggestedMetaSize = options.manifestTargetSize().getBytes();
int suggestedMinMetaCount = options.manifestMergeMinCount();
long fullCompactionThreshold = options.manifestFullCompactionThresholdSize().getBytes();
- boolean forceRewrite = options.manifestSortForceRewrite();
long maxRewriteSize = options.manifestSortMaxRewriteSize();
int maxSizeAmplificationPercent = options.maxSizeAmplificationPercent();
int sortedRunSizeRatio = options.sortedRunSizeRatio();
@@ -185,7 +185,7 @@ static List trySortCompaction(
suggestedMetaSize,
suggestedMinMetaCount,
fullCompactionThreshold,
- forceRewrite,
+ fullSort,
maxRewriteSize,
maxSizeAmplificationPercent,
sortedRunSizeRatio,
@@ -230,7 +230,7 @@ private static Optional> tryFullCompaction(
long suggestedMetaSize,
int suggestedMinMetaCount,
long fullCompactionThreshold,
- boolean forceRewrite,
+ boolean fullSort,
long maxRewriteSize,
int maxSizeAmplificationPercent,
int sortedRunSizeRatio,
@@ -238,7 +238,7 @@ private static Optional> tryFullCompaction(
@Nullable Integer manifestReadParallelism)
throws Exception {
// Step 1: Check if full compaction threshold is met
- if (!forceRewrite
+ if (!fullSort
&& !reachesFullCompactionThreshold(
input, suggestedMetaSize, fullCompactionThreshold)) {
return Optional.empty();
@@ -248,7 +248,7 @@ private static Optional> tryFullCompaction(
prepareCompaction(
input,
true,
- forceRewrite,
+ fullSort,
manifestFile,
partitionType,
sortPartitionField,
@@ -262,10 +262,8 @@ private static Optional> tryFullCompaction(
manifestReadParallelism);
try {
List levelRuns = ctx.levelRuns;
- List pickedRuns = ctx.pickedRuns;
- if (forceRewrite) {
- pickedRuns = new ArrayList<>(levelRuns);
- }
+ List pickedRuns =
+ fullSort ? new ArrayList<>(levelRuns) : ctx.pickedRuns;
if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) {
LOG.debug(
@@ -295,11 +293,10 @@ private static Optional> tryFullCompaction(
}
pickedFiles.addAll(ctx.defaultCompactFiles.keySet());
- // Step 4: Split into sections and merge small adjacent sections. A forced rewrite
- // intentionally uses one global section so entries from different already-compacted
- // manifests can be clustered using the layout selected from the table options.
+ // Step 4: A full sort uses one global section so entries from all existing runs can be
+ // clustered using the layout selected from the table options.
List sections;
- if (forceRewrite) {
+ if (fullSort) {
long totalSize = 0L;
boolean hasDefaultCompactFile = false;
for (ManifestFileMeta file : pickedFiles) {
@@ -485,7 +482,7 @@ private static List tryMinorCompaction(
private static CompactionContext prepareCompaction(
List input,
boolean fullCompaction,
- boolean forceRewrite,
+ boolean fullSort,
ManifestFile manifestFile,
RowType partitionType,
String sortPartitionField,
@@ -528,7 +525,7 @@ private static CompactionContext prepareCompaction(
return new CompactionContext(
fullCompaction,
- forceRewrite,
+ fullSort,
useRunMergeOptimize,
sortKey,
partitionType,
@@ -1099,11 +1096,11 @@ private static void rewriteSection(
CompactionContext ctx,
ManifestFile manifestFile,
@Nullable Integer manifestReadParallelism,
- boolean allowForceRewrite)
+ boolean allowFullRewrite)
throws Exception {
// Skip rewrite for single file not in delete-range.
if (section.size() == 1
- && !(allowForceRewrite && ctx.forceRewrite)
+ && !(allowFullRewrite && ctx.fullSort)
&& !ctx.defaultCompactFiles.getOrDefault(section.get(0), false)) {
output.addUnchanged(section.get(0));
return;
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 8144112db6c5..996fceaa97bf 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
@@ -417,7 +417,7 @@ public static void validateTableSchema(TableSchema schema, Set dynamicOp
validatePkClusteringOverride(options);
- validateManifestSort(schema, options, dynamicOptionKeys);
+ validateManifestSort(schema, options);
}
/**
@@ -2019,15 +2019,7 @@ public static void validatePkClusteringOverride(CoreOptions options) {
}
}
- private static void validateManifestSort(
- TableSchema schema, CoreOptions options, Set dynamicOptionKeys) {
- checkArgument(
- !schema.options().containsKey(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key())
- || dynamicOptionKeys.contains(
- CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key()),
- "'%s' is only supported as a dynamic option for explicit manifest compaction.",
- CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key());
-
+ private static void validateManifestSort(TableSchema schema, CoreOptions options) {
if (options.manifestSortEnabled()) {
if (!options.dataEvolutionEnabled()) {
checkArgument(
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 0b6a34ba57a4..557f2131345d 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
@@ -33,6 +33,7 @@
import org.apache.paimon.io.DataFileMeta;
import org.apache.paimon.operation.ManifestCompactDryRun;
import org.apache.paimon.operation.ManifestFileMerger;
+import org.apache.paimon.operation.ManifestFileMergerTestUtils;
import org.apache.paimon.options.Options;
import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.schema.FileSystemSchemaManager;
@@ -1353,7 +1354,7 @@ public void testManifestSortUsesBucketAsPrimaryKeyForBucketedTable(int bucket) {
}
@Test
- public void testManifestSortForceRewriteAlreadyCompactedRuns() {
+ public void testManifestSortFullCompactionAlreadyCompactedRuns() {
List physical =
Arrays.asList(
makeManifest(makeBucketEntry("a-3", 0, 3), makeBucketEntry("a-1", 0, 1)),
@@ -1373,6 +1374,7 @@ public void testManifestSortForceRewriteAlreadyCompactedRuns() {
Options testOptions = new Options();
testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1G");
+ testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), Long.MAX_VALUE + "B");
testOptions.set(CoreOptions.BUCKET, 4);
List unchanged =
@@ -1383,9 +1385,8 @@ public void testManifestSortForceRewriteAlreadyCompactedRuns() {
CoreOptions.fromMap(testOptions.toMap()));
assertThat(unchanged).containsExactlyInAnyOrderElementsOf(input);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
List rewritten =
- ManifestFileMerger.merge(
+ ManifestFileMergerTestUtils.fullMerge(
input,
manifestFile,
getPartitionType(),
@@ -1406,7 +1407,7 @@ public void testManifestSortForceRewriteAlreadyCompactedRuns() {
assertThat(rewritten.get(0).maxBucket()).isEqualTo(3);
assertThat(rewritten.get(0).totalBuckets()).isEqualTo(240);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, false);
+ testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), Long.MAX_VALUE + "B");
List afterMigration =
ManifestFileMerger.merge(
rewritten,
@@ -1417,7 +1418,7 @@ public void testManifestSortForceRewriteAlreadyCompactedRuns() {
}
@Test
- public void testManifestSortForceRewriteAllLevelRuns() {
+ public void testManifestSortFullCompactionAllLevelRuns() {
List physical =
Arrays.asList(
makeManifest(makeBucketEntry("a-3", 0, 3), makeBucketEntry("a-1", 2, 1)),
@@ -1431,6 +1432,7 @@ public void testManifestSortForceRewriteAllLevelRuns() {
Options testOptions = new Options();
testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1G");
+ testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), Long.MAX_VALUE + "B");
testOptions.set(CoreOptions.BUCKET, 4);
// Without bucket metadata, manifest sort falls back to the overlapping partition ranges
@@ -1454,9 +1456,8 @@ public void testManifestSortForceRewriteAllLevelRuns() {
CoreOptions.fromMap(testOptions.toMap()));
assertThat(unchanged).containsExactlyInAnyOrderElementsOf(input);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
List rewritten =
- ManifestFileMerger.merge(
+ ManifestFileMergerTestUtils.fullMerge(
input,
manifestFile,
getPartitionType(),
@@ -1475,34 +1476,7 @@ public void testManifestSortForceRewriteAllLevelRuns() {
}
@Test
- public void testManifestSortForceRewriteDryRunUsesFullCompaction() {
- List input =
- Arrays.asList(
- makeManifest(makeEntry(true, "base", 0)),
- makeManifest(
- makeEntry(false, "base", 0), makeEntry(true, "replacement", 0)));
-
- Options testOptions = new Options();
- testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
- testOptions.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1B");
- testOptions.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(testOptions.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("Manifest sort level files: L0=0, L1=0, L2=0, L3=0, L4=0.");
- }
-
- @Test
- public void testManifestSortForceRewriteSingleManifest() {
+ public void testManifestSortFullCompactionSingleManifest() {
ManifestFileMeta physical =
makeManifest(makeBucketEntry("file-3", 0, 3), makeBucketEntry("file-0", 0, 0));
ManifestFileMeta input =
@@ -1511,10 +1485,9 @@ public void testManifestSortForceRewriteSingleManifest() {
Options testOptions = new Options();
testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
testOptions.set(CoreOptions.BUCKET, 4);
List rewritten =
- ManifestFileMerger.merge(
+ ManifestFileMergerTestUtils.fullMerge(
Collections.singletonList(input),
manifestFile,
getPartitionType(),
@@ -1528,7 +1501,7 @@ public void testManifestSortForceRewriteSingleManifest() {
}
@Test
- public void testManifestSortForceRewriteRespectsRewriteBudget() {
+ public void testManifestSortFullCompactionRespectsRewriteLimit() {
long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
List input = new ArrayList<>();
for (int i = 0; i < 4; i++) {
@@ -1538,11 +1511,10 @@ public void testManifestSortForceRewriteRespectsRewriteBudget() {
Options testOptions = new Options();
testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1B");
testOptions.set(CoreOptions.BUCKET, 4);
List rewritten =
- ManifestFileMerger.merge(
+ ManifestFileMergerTestUtils.fullMerge(
input,
manifestFile,
getPartitionType(),
@@ -1558,7 +1530,7 @@ public void testManifestSortForceRewriteRespectsRewriteBudget() {
}
@Test
- public void testManifestSortForceRewriteDoesNotExceedBudgetForSingletonTail() {
+ public void testManifestSortFullCompactionDoesNotExceedLimitForSingletonTail() {
long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
List input = new ArrayList<>();
for (int i = 0; i < 3; i++) {
@@ -1568,11 +1540,10 @@ public void testManifestSortForceRewriteDoesNotExceedBudgetForSingletonTail() {
Options testOptions = new Options();
testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1B");
testOptions.set(CoreOptions.BUCKET, 4);
List rewritten =
- ManifestFileMerger.merge(
+ ManifestFileMergerTestUtils.fullMerge(
input,
manifestFile,
getPartitionType(),
@@ -1588,7 +1559,7 @@ public void testManifestSortForceRewriteDoesNotExceedBudgetForSingletonTail() {
}
@Test
- public void testManifestSortForceRewriteDoesNotRewriteTailBeyondBudget() {
+ public void testManifestSortFullCompactionDoesNotRewriteTailBeyondLimit() {
long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
List input = new ArrayList<>();
for (int i = 0; i < 4; i++) {
@@ -1601,11 +1572,10 @@ public void testManifestSortForceRewriteDoesNotRewriteTailBeyondBudget() {
Options testOptions = new Options();
testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
- testOptions.set(CoreOptions.MANIFEST_SORT_FORCE_REWRITE, true);
testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "1B");
testOptions.set(CoreOptions.BUCKET, 8);
List rewritten =
- ManifestFileMerger.merge(
+ ManifestFileMergerTestUtils.fullMerge(
input,
manifestFile,
getPartitionType(),
diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
index c96b7b711195..437944fae5d9 100644
--- a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
@@ -1430,21 +1430,17 @@ public void testManifestCompact(boolean skipOnWriteOnly, boolean writeOnly) thro
}
@Test
- public void testManifestSortCompactManifestRespectsCompactionThresholds() {
+ public void testManifestSortCompactManifestUsesFullCompactionThresholds() {
Options options = new Options();
options.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
options.set(CoreOptions.MANIFEST_MERGE_MIN_COUNT, 100);
options.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), Long.MAX_VALUE + "B");
CoreOptions compactOptions =
- FileStoreCommitImpl.manifestCompactionOptions(
- new CoreOptions(options),
- Collections.emptyList(),
- TestKeyValueGenerator.DEFAULT_PART_TYPE);
+ FileStoreCommitImpl.manifestCompactionOptions(new CoreOptions(options));
- assertThat(compactOptions.manifestMergeMinCount()).isEqualTo(100);
- assertThat(compactOptions.manifestFullCompactionThresholdSize().getBytes())
- .isEqualTo(Long.MAX_VALUE);
+ assertThat(compactOptions.manifestMergeMinCount()).isEqualTo(1);
+ assertThat(compactOptions.manifestFullCompactionThresholdSize().getBytes()).isEqualTo(1);
}
@Test
diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java
index cfd1cb8fc1bd..57b4dadec48f 100644
--- a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTest.java
@@ -81,9 +81,7 @@ public void testManifestSortFallsBackToForcedLegacyMergeWithoutRowId() {
assertThat(ManifestFileMerger.canUseManifestSort(input, NO_PARTITION_TYPE, tableOptions))
.isFalse();
- CoreOptions compactOptions =
- FileStoreCommitImpl.manifestCompactionOptions(
- tableOptions, input, NO_PARTITION_TYPE);
+ CoreOptions compactOptions = FileStoreCommitImpl.manifestCompactionOptions(tableOptions);
assertThat(compactOptions.manifestMergeMinCount()).isEqualTo(1);
assertThat(compactOptions.manifestFullCompactionThresholdSize().getBytes()).isEqualTo(1);
diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTestUtils.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTestUtils.java
new file mode 100644
index 000000000000..87d93d128530
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestFileMergerTestUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.operation;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.manifest.ManifestFile;
+import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.types.RowType;
+
+import java.util.List;
+
+/** Test access to explicit full manifest compaction. */
+public class ManifestFileMergerTestUtils {
+
+ private ManifestFileMergerTestUtils() {}
+
+ public static List fullMerge(
+ List input,
+ ManifestFile manifestFile,
+ RowType partitionType,
+ CoreOptions options) {
+ return ManifestFileMerger.merge(input, manifestFile, partitionType, options, null, true);
+ }
+}
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 f66d623a0e23..c9588f4552fa 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
@@ -36,7 +36,6 @@
import java.util.Map;
import static java.util.Collections.emptyList;
-import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.apache.paimon.CoreOptions.BUCKET;
import static org.apache.paimon.CoreOptions.DATA_EVOLUTION_ENABLED;
@@ -2050,31 +2049,6 @@ void testManifestSortValidation() {
.hasMessageContaining("is not a partition field");
}
- @Test
- void testManifestSortForceRewriteIsDynamicOnly() {
- List fields =
- Arrays.asList(
- new DataField(0, "f0", DataTypes.INT()),
- new DataField(1, "f1", DataTypes.INT()));
- Map baseOptions = new HashMap<>();
- baseOptions.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), "true");
- baseOptions.put(BUCKET.key(), "-1");
-
- Map forceOptions = new HashMap<>(baseOptions);
- forceOptions.put(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key(), "true");
- TableSchema forceSchema =
- new TableSchema(1, fields, 10, singletonList("f0"), emptyList(), forceOptions, "");
- assertThatThrownBy(() -> validateTableSchema(forceSchema))
- .hasMessage(
- "'manifest-sort.force-rewrite' is only supported as a dynamic option for explicit manifest compaction.");
- assertThatNoException()
- .isThrownBy(
- () ->
- validateTableSchema(
- forceSchema,
- singleton(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key())));
- }
-
@Test
public void testMergeOnReadCoexistsWithVisibilityCallback() {
Map options = new HashMap<>();
diff --git a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
index 213e6b73afb4..a2e0f85fcee3 100644
--- a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
+++ b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/CompactManifestProcedureITCase.java
@@ -18,7 +18,6 @@
package org.apache.paimon.flink.procedure;
-import org.apache.paimon.CoreOptions;
import org.apache.paimon.flink.CatalogITCaseBase;
import org.apache.paimon.flink.action.ActionFactory;
import org.apache.paimon.flink.action.CompactManifestAction;
@@ -123,24 +122,7 @@ public void testManifestSortParameters() throws Exception {
long compactSnapshotId = table.snapshotManager().latestSnapshot().id();
sql(procedure);
Assertions.assertThat(table.snapshotManager().latestSnapshot().id())
- .isEqualTo(compactSnapshotId);
-
- String forceRewriteProcedure =
- "CALL sys.compact_manifest("
- + "`table` => 'default.T_SORT', "
- + "`options` => 'manifest-sort.force-rewrite=true', "
- + "`manifest_sort_enabled` => true, "
- + "`manifest_sort_partition_field` => 'dt', "
- + "`manifest_sort_max_rewrite_size` => '1 gb')";
- sql(forceRewriteProcedure);
- long forceRewriteSnapshotId = table.snapshotManager().latestSnapshot().id();
- Assertions.assertThat(forceRewriteSnapshotId).isEqualTo(compactSnapshotId + 1);
- Assertions.assertThat(paimonTable("T_SORT").options())
- .doesNotContainKey(CoreOptions.MANIFEST_SORT_FORCE_REWRITE.key());
-
- sql(procedure);
- Assertions.assertThat(table.snapshotManager().latestSnapshot().id())
- .isEqualTo(forceRewriteSnapshotId);
+ .isEqualTo(compactSnapshotId + 1);
}
@Test
From 13725eb0dd64d4492a286548fc7dd6ed372b2f0c Mon Sep 17 00:00:00 2001
From: mingfeng
Date: Mon, 14 Sep 2026 17:27:26 +0800
Subject: [PATCH 6/6] [core] Preserve singleton handling in manifest sort
---
.../paimon/operation/ManifestFileSorter.java | 14 ++++++++
.../paimon/manifest/ManifestFileMetaTest.java | 35 +++++++++++++++++++
2 files changed, 49 insertions(+)
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 e581f2f301c2..a44b94a310bd 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
@@ -888,6 +888,20 @@ private static void rewriteSections(
for (int i = 0; i < sections.size(); i++) {
Section section = sections.get(i);
+ // Preserve the ordinary-compaction shortcut: an unchanged singleton must not consume
+ // the sort rewrite limit. Explicit full sort intentionally rewrites the singleton.
+ if (!ctx.fullSort && section.files.size() == 1) {
+ rewriteSection(
+ section.files,
+ output,
+ sortNewFiles,
+ ctx,
+ manifestFile,
+ manifestReadParallelism,
+ false);
+ continue;
+ }
+
// Phase 1: budget not yet exhausted -- perform aggressive sort rewrite.
if (!budgetExhausted) {
// Phase 1a: section fits within the remaining budget -- sort and rewrite it
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 557f2131345d..ad3641b121b4 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
@@ -1590,6 +1590,41 @@ public void testManifestSortFullCompactionDoesNotRewriteTailBeyondLimit() {
assertEquivalentEntries(input, rewritten);
}
+ @Test
+ public void testManifestSortUnchangedSingletonDoesNotConsumeRewriteLimit() {
+ long targetSize = CoreOptions.MANIFEST_TARGET_FILE_SIZE.defaultValue().getBytes();
+ List input = new ArrayList<>();
+ input.add(copyWithFileSize(makeManifest(makeEntry(true, "singleton", 0)), targetSize));
+ for (int i = 0; i < 5; i++) {
+ input.add(
+ copyWithFileSize(
+ makeManifest(
+ makeEntry(true, "range-" + i + "-1", 1),
+ makeEntry(true, "range-" + i + "-2", 2)),
+ targetSize));
+ }
+
+ Options testOptions = new Options();
+ testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ testOptions.set(CoreOptions.MANIFEST_SORT_MAX_REWRITE_SIZE.key(), "4M");
+ testOptions.set(CoreOptions.BUCKET, -1);
+ List merged =
+ ManifestFileMerger.merge(
+ input,
+ manifestFile,
+ getPartitionType(),
+ CoreOptions.fromMap(testOptions.toMap()));
+
+ Set inputNames =
+ input.stream().map(ManifestFileMeta::fileName).collect(Collectors.toSet());
+ assertThat(merged).extracting(ManifestFileMeta::fileName).contains(input.get(0).fileName());
+ assertThat(merged)
+ .extracting(ManifestFileMeta::fileName)
+ .filteredOn(inputNames::contains)
+ .hasSize(4);
+ assertEquivalentEntries(input, merged);
+ }
+
@ParameterizedTest
@ValueSource(ints = {-1, 4, -2})
public void testManifestSortDryRunUsesBucketRangesForBucketedTable(int bucket) {