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

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 0237d79b73 [#13726] feat(policy): Support Iceberg rewrite strategy and 
sort order (#13727)
0237d79b73 is described below

commit 0237d79b737a688659481505c32b7dee5f49411b
Author: MaSai <[email protected]>
AuthorDate: Sat Oct 10 16:16:27 2026 +0800

    [#13726] feat(policy): Support Iceberg rewrite strategy and sort order 
(#13727)
    
    ### What changes were proposed in this pull request?
    
    - Add typed policy fields `rewriteStrategy` / `sortOrder` on
    `system_iceberg_compaction`
    - Emit them as rules `job.strategy` / `job.sort-order` (not under
    `job.options.*`)
    - Propagate through `CompactionJobContext` →
    `GravitinoCompactionJobAdapter` job template args `strategy` /
    `sort_order`
    - Keep `rewriteOptions` as the Iceberg `options` map only
    - Update docs and unit tests
    
    ### Why are the changes needed?
    
    The optimizer previously hardcoded `strategy=binpack` and empty
    `sort_order`. Sort/z-order compaction cannot be configured via
    `rewriteOptions` because Iceberg treats those as top-level procedure
    parameters, separate from the `options` map.
    
    Fix: #13726
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes:
    - New optional content fields: `rewriteStrategy` (default `binpack`),
    `sortOrder` (default `""`)
    - Validation: `rewriteStrategy` ∈ `{binpack, sort}`; `sortOrder`
    required when strategy is `sort`
    
    ### How was this patch tested?
    
    ```
    ./gradlew :api:test --tests TestPolicyContents \
      :common:test --tests TestPolicyDTO \
      :maintenance:optimizer:test \
        --tests TestGravitinoCompactionJobAdapter \
        --tests TestGravitinoPolicyCompactionStrategy \
      -PskipITs -PskipDockerTests=true
    ```
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../policy/IcebergDataCompactionContent.java       | 85 +++++++++++++++++++++-
 .../apache/gravitino/policy/PolicyContents.java    | 41 +++++++++++
 .../gravitino/policy/TestPolicyContents.java       | 53 ++++++++++++++
 .../gravitino/dto/policy/PolicyContentDTO.java     | 28 +++++++
 .../apache/gravitino/dto/util/DTOConverters.java   |  4 +
 .../apache/gravitino/dto/policy/TestPolicyDTO.java |  6 ++
 docs/iceberg-compaction-policy.md                  | 34 +++++++--
 .../handler/compaction/CompactionJobContext.java   |  8 ++
 .../compaction/CompactionStrategyHandler.java      | 22 ++++++
 .../job/GravitinoCompactionJobAdapter.java         | 13 +++-
 .../compaction/TestCompactionStrategyHandler.java  |  2 +
 .../TestGravitinoPolicyCompactionStrategy.java     | 55 ++++++++++++++
 .../job/TestBuiltinIcebergRewriteDataFiles.java    |  2 +
 .../job/TestGravitinoCompactionJobAdapter.java     | 19 ++++-
 14 files changed, 359 insertions(+), 13 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/policy/IcebergDataCompactionContent.java
 
b/api/src/main/java/org/apache/gravitino/policy/IcebergDataCompactionContent.java
index 50f76b538f..08c8478d66 100644
--- 
a/api/src/main/java/org/apache/gravitino/policy/IcebergDataCompactionContent.java
+++ 
b/api/src/main/java/org/apache/gravitino/policy/IcebergDataCompactionContent.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -42,6 +43,20 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
   public static final String JOB_TEMPLATE_NAME_VALUE = 
"builtin-iceberg-rewrite-data-files";
   /** Prefix for rewrite options propagated to job options. */
   public static final String JOB_OPTIONS_PREFIX = "job.options.";
+  /**
+   * Rule key for Iceberg {@code rewrite_data_files} top-level {@code 
strategy} argument.
+   *
+   * <p>This is separate from {@link #JOB_OPTIONS_PREFIX} because Iceberg 
treats {@code strategy} as
+   * a procedure parameter, not an entry in the {@code options} map.
+   */
+  public static final String REWRITE_STRATEGY_KEY = "rewriteStrategy";
+  /**
+   * Rule key for Iceberg {@code rewrite_data_files} top-level {@code 
sort_order} argument.
+   *
+   * <p>This is separate from {@link #JOB_OPTIONS_PREFIX} because Iceberg 
treats {@code sort_order}
+   * as a procedure parameter, not an entry in the {@code options} map.
+   */
+  public static final String SORT_ORDER_KEY = "sortOrder";
   /** Rule key for trigger expression. */
   public static final String TRIGGER_EXPR_KEY = "trigger-expr";
   /** Rule key for score expression. */
@@ -72,7 +87,17 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
   public static final long DEFAULT_MAX_PARTITION_NUM = 50L;
   /** Default rewrite options for Iceberg rewrite data files. */
   public static final Map<String, String> DEFAULT_REWRITE_OPTIONS = 
ImmutableMap.of();
+  /** Iceberg rewrite strategy value for binpack. */
+  public static final String REWRITE_STRATEGY_BINPACK = "binpack";
+  /** Iceberg rewrite strategy value for sort. */
+  public static final String REWRITE_STRATEGY_SORT = "sort";
+  /** Default Iceberg rewrite strategy. */
+  public static final String DEFAULT_REWRITE_STRATEGY = 
REWRITE_STRATEGY_BINPACK;
+  /** Default Iceberg sort order (empty means unset). */
+  public static final String DEFAULT_SORT_ORDER = "";
 
+  private static final Set<String> SUPPORTED_REWRITE_STRATEGIES =
+      ImmutableSet.of(REWRITE_STRATEGY_BINPACK, REWRITE_STRATEGY_SORT);
   private static final Pattern OPTION_KEY_PATTERN = 
Pattern.compile("[A-Za-z0-9._-]+");
   private static final Set<MetadataObject.Type> SUPPORTED_OBJECT_TYPES =
       ImmutableSet.of(
@@ -99,11 +124,13 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
   private final Long dataFileMseWeight;
   private final Long deleteFileNumberWeight;
   private final Long maxPartitionNum;
+  private final String rewriteStrategy;
+  private final String sortOrder;
   private final Map<String, String> rewriteOptions;
 
   /** Default constructor for Jackson deserialization only. */
   private IcebergDataCompactionContent() {
-    this(null, null, null, null, null, null);
+    this(null, null, null, null, null, null, null, null);
   }
 
   IcebergDataCompactionContent(
@@ -112,6 +139,8 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
       Long dataFileMseWeight,
       Long deleteFileNumberWeight,
       Long maxPartitionNum,
+      String rewriteStrategy,
+      String sortOrder,
       Map<String, String> rewriteOptions) {
     // Nullable inputs are treated as "use default" to simplify policy 
creation.
     this.minDataFileMse = minDataFileMse == null ? DEFAULT_MIN_DATA_FILE_MSE : 
minDataFileMse;
@@ -122,6 +151,13 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
     this.deleteFileNumberWeight =
         deleteFileNumberWeight == null ? DEFAULT_DELETE_FILE_NUMBER_WEIGHT : 
deleteFileNumberWeight;
     this.maxPartitionNum = maxPartitionNum == null ? DEFAULT_MAX_PARTITION_NUM 
: maxPartitionNum;
+    this.rewriteStrategy =
+        rewriteStrategy == null
+            ? DEFAULT_REWRITE_STRATEGY
+            : rewriteStrategy.trim().toLowerCase(Locale.ROOT);
+    // Null → "" for binpack (Iceberg leaves sort_order unset). When 
rewriteStrategy is
+    // "sort", validate() requires a non-blank sortOrder — do not treat "" as 
valid for sort.
+    this.sortOrder = sortOrder == null ? DEFAULT_SORT_ORDER : sortOrder.trim();
     this.rewriteOptions =
         rewriteOptions == null
             ? DEFAULT_REWRITE_OPTIONS
@@ -173,6 +209,25 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
     return maxPartitionNum;
   }
 
+  /**
+   * Returns the Iceberg {@code rewrite_data_files} top-level {@code strategy} 
argument.
+   *
+   * @return rewrite strategy, one of {@value REWRITE_STRATEGY_BINPACK} or 
{@value
+   *     REWRITE_STRATEGY_SORT}
+   */
+  public String rewriteStrategy() {
+    return rewriteStrategy;
+  }
+
+  /**
+   * Returns the Iceberg {@code rewrite_data_files} top-level {@code 
sort_order} argument.
+   *
+   * @return sort order expression, or empty string when unset
+   */
+  public String sortOrder() {
+    return sortOrder;
+  }
+
   /**
    * Returns rewrite options that are expanded to {@code job.options.*} rule 
entries.
    *
@@ -204,6 +259,8 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
     rules.put(MAX_PARTITION_NUM_KEY, maxPartitionNum);
     rules.put(TRIGGER_EXPR_KEY, TRIGGER_EXPR);
     rules.put(SCORE_EXPR_KEY, SCORE_EXPR);
+    rules.put(REWRITE_STRATEGY_KEY, rewriteStrategy);
+    rules.put(SORT_ORDER_KEY, sortOrder);
     rewriteOptions.forEach((key, value) -> rules.put(JOB_OPTIONS_PREFIX + key, 
value));
     return Collections.unmodifiableMap(rules);
   }
@@ -217,6 +274,22 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
     Preconditions.checkArgument(dataFileMseWeight >= 0, "dataFileMseWeight 
must be >= 0");
     Preconditions.checkArgument(deleteFileNumberWeight >= 0, 
"deleteFileNumberWeight must be >= 0");
     Preconditions.checkArgument(maxPartitionNum > 0, "maxPartitionNum must be 
> 0");
+    Preconditions.checkArgument(
+        SUPPORTED_REWRITE_STRATEGIES.contains(rewriteStrategy),
+        "rewriteStrategy must be one of %s, but got '%s'",
+        SUPPORTED_REWRITE_STRATEGIES,
+        rewriteStrategy);
+    if (REWRITE_STRATEGY_SORT.equals(rewriteStrategy)) {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(sortOrder),
+          "sortOrder must be set when rewriteStrategy is '%s'",
+          REWRITE_STRATEGY_SORT);
+    } else {
+      Preconditions.checkArgument(
+          StringUtils.isBlank(sortOrder),
+          "sortOrder must be empty when rewriteStrategy is '%s'",
+          rewriteStrategy);
+    }
 
     rewriteOptions.forEach(
         (key, value) -> {
@@ -246,6 +319,8 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
         && Objects.equals(dataFileMseWeight, that.dataFileMseWeight)
         && Objects.equals(deleteFileNumberWeight, that.deleteFileNumberWeight)
         && Objects.equals(maxPartitionNum, that.maxPartitionNum)
+        && Objects.equals(rewriteStrategy, that.rewriteStrategy)
+        && Objects.equals(sortOrder, that.sortOrder)
         && Objects.equals(rewriteOptions, that.rewriteOptions);
   }
 
@@ -257,6 +332,8 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
         dataFileMseWeight,
         deleteFileNumberWeight,
         maxPartitionNum,
+        rewriteStrategy,
+        sortOrder,
         rewriteOptions);
   }
 
@@ -273,6 +350,12 @@ public class IcebergDataCompactionContent implements 
PolicyContent {
         + deleteFileNumberWeight
         + ", maxPartitionNum="
         + maxPartitionNum
+        + ", rewriteStrategy='"
+        + rewriteStrategy
+        + '\''
+        + ", sortOrder='"
+        + sortOrder
+        + '\''
         + ", rewriteOptions="
         + rewriteOptions
         + '}';
diff --git a/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java 
b/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java
index 50e16ac5fe..501475c51f 100644
--- a/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java
+++ b/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java
@@ -55,6 +55,8 @@ public class PolicyContents {
         IcebergDataCompactionContent.DEFAULT_DATA_FILE_MSE_WEIGHT,
         IcebergDataCompactionContent.DEFAULT_DELETE_FILE_NUMBER_WEIGHT,
         IcebergDataCompactionContent.DEFAULT_MAX_PARTITION_NUM,
+        IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY,
+        IcebergDataCompactionContent.DEFAULT_SORT_ORDER,
         IcebergDataCompactionContent.DEFAULT_REWRITE_OPTIONS);
   }
 
@@ -74,6 +76,8 @@ public class PolicyContents {
         IcebergDataCompactionContent.DEFAULT_DATA_FILE_MSE_WEIGHT,
         IcebergDataCompactionContent.DEFAULT_DELETE_FILE_NUMBER_WEIGHT,
         IcebergDataCompactionContent.DEFAULT_MAX_PARTITION_NUM,
+        IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY,
+        IcebergDataCompactionContent.DEFAULT_SORT_ORDER,
         rewriteOptions);
   }
 
@@ -95,12 +99,49 @@ public class PolicyContents {
       long deleteFileNumberWeight,
       long maxPartitionNum,
       Map<String, String> rewriteOptions) {
+    return icebergDataCompaction(
+        minDataFileMse,
+        minDeleteFileNumber,
+        dataFileMseWeight,
+        deleteFileNumberWeight,
+        maxPartitionNum,
+        IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY,
+        IcebergDataCompactionContent.DEFAULT_SORT_ORDER,
+        rewriteOptions);
+  }
+
+  /**
+   * Creates an iceberg compaction policy content with rewrite strategy and 
sort order.
+   *
+   * @param minDataFileMse minimum threshold for custom-data-file-mse
+   * @param minDeleteFileNumber minimum threshold for custom-delete-file-number
+   * @param dataFileMseWeight weight used for custom-data-file-mse score 
contribution
+   * @param deleteFileNumberWeight weight used for custom-delete-file-number 
score contribution
+   * @param maxPartitionNum maximum partition number selected for compaction
+   * @param rewriteStrategy Iceberg rewrite_data_files top-level strategy 
({@code binpack} or {@code
+   *     sort})
+   * @param sortOrder Iceberg rewrite_data_files top-level sort_order 
(required when strategy is
+   *     {@code sort})
+   * @param rewriteOptions rewrite options forwarded as job.options.*
+   * @return iceberg compaction policy content
+   */
+  public static PolicyContent icebergDataCompaction(
+      long minDataFileMse,
+      long minDeleteFileNumber,
+      long dataFileMseWeight,
+      long deleteFileNumberWeight,
+      long maxPartitionNum,
+      String rewriteStrategy,
+      String sortOrder,
+      Map<String, String> rewriteOptions) {
     return new IcebergDataCompactionContent(
         minDataFileMse,
         minDeleteFileNumber,
         dataFileMseWeight,
         deleteFileNumberWeight,
         maxPartitionNum,
+        rewriteStrategy,
+        sortOrder,
         rewriteOptions);
   }
 
diff --git 
a/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java 
b/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java
index 1112846146..4bac0a9d0f 100644
--- a/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java
+++ b/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java
@@ -42,6 +42,8 @@ public class TestPolicyContents {
     Assertions.assertEquals(1L, content.rules().get("dataFileMseWeight"));
     Assertions.assertEquals(100L, 
content.rules().get("deleteFileNumberWeight"));
     Assertions.assertEquals(50L, content.rules().get("max-partition-num"));
+    Assertions.assertEquals("binpack", content.rules().get("rewriteStrategy"));
+    Assertions.assertEquals("", content.rules().get("sortOrder"));
     
Assertions.assertNull(content.rules().get("job.options.target-file-size-bytes"));
     Assertions.assertNull(content.rules().get("job.options.min-input-files"));
     
Assertions.assertNull(content.rules().get("job.options.delete-file-threshold"));
@@ -71,6 +73,8 @@ public class TestPolicyContents {
         content.rules().get("score-expr"));
     Assertions.assertEquals("1048576", 
content.rules().get("job.options.target-file-size-bytes"));
     Assertions.assertEquals("1", 
content.rules().get("job.options.min-input-files"));
+    Assertions.assertEquals("binpack", content.rules().get("rewriteStrategy"));
+    Assertions.assertEquals("", content.rules().get("sortOrder"));
     Assertions.assertEquals(
         ImmutableSet.of(
             MetadataObject.Type.CATALOG, MetadataObject.Type.SCHEMA, 
MetadataObject.Type.TABLE),
@@ -95,6 +99,55 @@ public class TestPolicyContents {
     Assertions.assertDoesNotThrow(content::validate);
   }
 
+  @Test
+  void testIcebergCompactionContentSupportsSortStrategy() {
+    IcebergDataCompactionContent content =
+        (IcebergDataCompactionContent)
+            PolicyContents.icebergDataCompaction(
+                1000L, 1L, 1L, 100L, 50L, "sort", "zorder(c1,c2)", 
mapOf("min-input-files", "5"));
+
+    Assertions.assertEquals("sort", content.rewriteStrategy());
+    Assertions.assertEquals("zorder(c1,c2)", content.sortOrder());
+    Assertions.assertEquals("sort", content.rules().get("rewriteStrategy"));
+    Assertions.assertEquals("zorder(c1,c2)", content.rules().get("sortOrder"));
+    Assertions.assertEquals("5", 
content.rules().get("job.options.min-input-files"));
+    Assertions.assertDoesNotThrow(content::validate);
+  }
+
+  @Test
+  void testIcebergCompactionContentRejectsSortWithoutSortOrder() {
+    IcebergDataCompactionContent content =
+        (IcebergDataCompactionContent)
+            PolicyContents.icebergDataCompaction(1000L, 1L, 1L, 100L, 50L, 
"sort", "", Map.of());
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(IllegalArgumentException.class, 
content::validate);
+    Assertions.assertTrue(exception.getMessage().contains("sortOrder"));
+  }
+
+  @Test
+  void testIcebergCompactionContentRejectsBinpackWithSortOrder() {
+    IcebergDataCompactionContent content =
+        (IcebergDataCompactionContent)
+            PolicyContents.icebergDataCompaction(
+                1000L, 1L, 1L, 100L, 50L, "binpack", "zorder(c1)", Map.of());
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(IllegalArgumentException.class, 
content::validate);
+    Assertions.assertTrue(exception.getMessage().contains("sortOrder must be 
empty"));
+  }
+
+  @Test
+  void testIcebergCompactionContentRejectsUnsupportedStrategy() {
+    IcebergDataCompactionContent content =
+        (IcebergDataCompactionContent)
+            PolicyContents.icebergDataCompaction(1000L, 1L, 1L, 100L, 50L, 
"unknown", "", Map.of());
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(IllegalArgumentException.class, 
content::validate);
+    Assertions.assertTrue(exception.getMessage().contains("rewriteStrategy"));
+  }
+
   @Test
   void testIcebergCompactionContentRejectsInvalidRewriteOptionKey() {
     IcebergDataCompactionContent content =
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java
index e3f0f77583..65fae911ad 100644
--- a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java
@@ -105,6 +105,12 @@ public interface PolicyContentDTO extends PolicyContent {
     @JsonProperty("maxPartitionNum")
     private Long maxPartitionNum;
 
+    @JsonProperty("rewriteStrategy")
+    private String rewriteStrategy;
+
+    @JsonProperty("sortOrder")
+    private String sortOrder;
+
     @JsonProperty("rewriteOptions")
     private Map<String, String> rewriteOptions;
 
@@ -166,6 +172,26 @@ public interface PolicyContentDTO extends PolicyContent {
           : maxPartitionNum;
     }
 
+    /**
+     * Returns the Iceberg rewrite_data_files top-level strategy.
+     *
+     * @return rewrite strategy
+     */
+    public String rewriteStrategy() {
+      return rewriteStrategy == null
+          ? IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY
+          : rewriteStrategy;
+    }
+
+    /**
+     * Returns the Iceberg rewrite_data_files top-level sort_order.
+     *
+     * @return sort order expression
+     */
+    public String sortOrder() {
+      return sortOrder == null ? 
IcebergDataCompactionContent.DEFAULT_SORT_ORDER : sortOrder;
+    }
+
     /**
      * Returns rewrite options expanded to job.options.* during rule 
generation.
      *
@@ -205,6 +231,8 @@ public interface PolicyContentDTO extends PolicyContent {
           dataFileMseWeight(),
           deleteFileNumberWeight(),
           maxPartitionNum(),
+          rewriteStrategy(),
+          sortOrder(),
           rewriteOptions());
     }
   }
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java 
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index 9851e8e4cd..fd45252a6a 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -695,6 +695,8 @@ public class DTOConverters {
           .withDataFileMseWeight(icebergCompactionContent.dataFileMseWeight())
           
.withDeleteFileNumberWeight(icebergCompactionContent.deleteFileNumberWeight())
           .withMaxPartitionNum(icebergCompactionContent.maxPartitionNum())
+          .withRewriteStrategy(icebergCompactionContent.rewriteStrategy())
+          .withSortOrder(icebergCompactionContent.sortOrder())
           .withRewriteOptions(icebergCompactionContent.rewriteOptions())
           .build();
     }
@@ -1572,6 +1574,8 @@ public class DTOConverters {
           icebergCompactionContentDTO.dataFileMseWeight(),
           icebergCompactionContentDTO.deleteFileNumberWeight(),
           icebergCompactionContentDTO.maxPartitionNum(),
+          icebergCompactionContentDTO.rewriteStrategy(),
+          icebergCompactionContentDTO.sortOrder(),
           icebergCompactionContentDTO.rewriteOptions());
     }
 
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java 
b/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java
index 7bbe897cfc..8dc0292c10 100644
--- a/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java
+++ b/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java
@@ -119,6 +119,8 @@ public class TestPolicyDTO {
             .withDataFileMseWeight(2L)
             .withDeleteFileNumberWeight(150L)
             .withMaxPartitionNum(99L)
+            .withRewriteStrategy("sort")
+            .withSortOrder("zorder(c1,c2)")
             .withRewriteOptions(
                 ImmutableMap.of("target-file-size-bytes", "1048576", 
"min-input-files", "1"))
             .build();
@@ -169,6 +171,10 @@ public class TestPolicyDTO {
     Assertions.assertEquals(
         IcebergDataCompactionContent.DEFAULT_MAX_PARTITION_NUM, 
contentDTO.maxPartitionNum());
     Assertions.assertTrue(contentDTO.rewriteOptions().isEmpty());
+    Assertions.assertEquals(
+        IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY, 
contentDTO.rewriteStrategy());
+    Assertions.assertEquals(
+        IcebergDataCompactionContent.DEFAULT_SORT_ORDER, 
contentDTO.sortOrder());
     Assertions.assertDoesNotThrow(contentDTO::validate);
   }
 }
diff --git a/docs/iceberg-compaction-policy.md 
b/docs/iceberg-compaction-policy.md
index 25a5520213..5c78c22621 100644
--- a/docs/iceberg-compaction-policy.md
+++ b/docs/iceberg-compaction-policy.md
@@ -19,14 +19,19 @@ This policy supports `CATALOG`, `SCHEMA`, and `TABLE` 
metadata objects.
 
 The typed content for `system_iceberg_compaction` supports the following 
fields:
 
-| Field | Required | Default | Description |
-|---|---|---|---|
-| `minDataFileMse` | No | `405323966463344` | Minimum threshold for metric 
`custom-data-file-mse`. Must be `>= 0`. |
-| `minDeleteFileNumber` | No | `1` | Minimum threshold for metric 
`custom-delete-file-number`. Must be `>= 0`. |
-| `dataFileMseWeight` | No | `1` | Score weight of `custom-data-file-mse`. 
Must be `>= 0`. |
-| `deleteFileNumberWeight` | No | `100` | Score weight of 
`custom-delete-file-number`. Must be `>= 0`. |
-| `maxPartitionNum` | No | `50` | Maximum number of partitions selected by 
optimizer. Must be `> 0`. |
-| `rewriteOptions` | No | `{}` | Additional rewrite options, expanded as 
`job.options.*` rules. |
+| Field                    | Required | Default             | Description      
                                                                                
             |
+| ------------------------ | -------- | ------------------- | 
-------------------------------------------------------------------------------------------------------------
 |
+| `minDataFileMse`         | No       | `405323966463344`   | Minimum 
threshold for metric `custom-data-file-mse`. Must be `>= 0`.                    
                      |
+| `minDeleteFileNumber`    | No       | `1`                 | Minimum 
threshold for metric `custom-delete-file-number`. Must be `>= 0`.               
                      |
+| `dataFileMseWeight`      | No       | `1`                 | Score weight of 
`custom-data-file-mse`. Must be `>= 0`.                                         
              |
+| `deleteFileNumberWeight` | No       | `100`               | Score weight of 
`custom-delete-file-number`. Must be `>= 0`.                                    
              |
+| `maxPartitionNum`        | No       | `50`                | Maximum number 
of partitions selected by optimizer. Must be `> 0`.                             
               |
+| `rewriteStrategy`        | No       | `binpack`           | Iceberg 
`rewrite_data_files` top-level `strategy`. Supported values: `binpack`, `sort`. 
                      |
+| `sortOrder`              | No       | `""`                | Iceberg 
`rewrite_data_files` top-level `sort_order`. Required for `sort`; must be empty 
for `binpack`.        |
+| `rewriteOptions`         | No       | `{}`                | Iceberg 
`rewrite_data_files` `options` map entries, expanded as `job.options.*` rules.  
                      |
+
+`rewriteStrategy` and `sortOrder` are **not** part of `rewriteOptions`. 
Iceberg treats them as procedure
+parameters separate from the `options` map.
 
 ## Generated Rules and Properties
 
@@ -39,6 +44,8 @@ The policy content is converted to:
   - `trigger-expr=custom-data-file-mse >= minDataFileMse || 
custom-delete-file-number >= minDeleteFileNumber`
   - `score-expr=custom-data-file-mse * dataFileMseWeight + 
custom-delete-file-number * deleteFileNumberWeight`
   - `max-partition-num=<maxPartitionNum>`
+  - `rewriteStrategy=<rewriteStrategy>`
+  - `sortOrder=<sortOrder>`
   - `job.options.<key>=<value>` for each rewrite option
 
 ## Parameter Tuning Guide
@@ -90,6 +97,17 @@ Recommended `rewriteOptions`:
 - `min-input-files = 5`
 - `delete-file-threshold = 1`
 
+### Rewrite Strategy and Sort Order
+
+Use `rewriteStrategy = sort` with a non-empty `sortOrder` when compaction 
should rewrite files with
+Iceberg sort or z-order, for example:
+
+- `rewriteStrategy = sort`
+- `sortOrder = zorder(c1,c2)`
+- `sortOrder = id DESC NULLS LAST`
+
+Keep the defaults (`binpack` and empty `sortOrder`) for size-based binpack 
compaction.
+
 ## Policy Examples
 
 <Tabs groupId='language' queryString>
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionJobContext.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionJobContext.java
index 4e36c4f1a3..81d42345f5 100644
--- 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionJobContext.java
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionJobContext.java
@@ -34,6 +34,8 @@ public class CompactionJobContext implements 
JobExecutionContext {
   private final NameIdentifier name;
   private final Map<String, String> jobOptions;
   private final String jobTemplateName;
+  @Getter private final String rewriteStrategy;
+  @Getter private final String sortOrder;
   @Getter private final Column[] columns;
   @Getter private final Transform[] partitioning;
   @Getter private final List<PartitionPath> partitions;
@@ -63,6 +65,12 @@ public class CompactionJobContext implements 
JobExecutionContext {
         + ", jobTemplateName='"
         + jobTemplateName
         + '\''
+        + ", rewriteStrategy='"
+        + rewriteStrategy
+        + '\''
+        + ", sortOrder='"
+        + sortOrder
+        + '\''
         + ", jobOptions="
         + jobOptions
         + ", columnCount="
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionStrategyHandler.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionStrategyHandler.java
index 5a5c66d65e..613ebdc393 100644
--- 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionStrategyHandler.java
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/CompactionStrategyHandler.java
@@ -21,6 +21,7 @@ package 
org.apache.gravitino.maintenance.optimizer.recommender.handler.compactio
 
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import org.apache.gravitino.NameIdentifier;
@@ -63,8 +64,29 @@ public class CompactionStrategyHandler extends 
BaseExpressionStrategyHandler {
         nameIdentifier,
         jobOptions,
         strategy.jobTemplateName(),
+        readRewriteStrategy(strategy),
+        readSortOrder(strategy),
         tableMetadata.columns(),
         tableMetadata.partitioning(),
         resolvedPartitions);
   }
+
+  private static String readRewriteStrategy(Strategy strategy) {
+    Object value = 
strategy.rules().get(IcebergDataCompactionContent.REWRITE_STRATEGY_KEY);
+    if (value == null) {
+      return IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY;
+    }
+    String strategyValue = value.toString().trim().toLowerCase(Locale.ROOT);
+    return strategyValue.isEmpty()
+        ? IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY
+        : strategyValue;
+  }
+
+  private static String readSortOrder(Strategy strategy) {
+    Object value = 
strategy.rules().get(IcebergDataCompactionContent.SORT_ORDER_KEY);
+    if (value == null) {
+      return IcebergDataCompactionContent.DEFAULT_SORT_ORDER;
+    }
+    return value.toString().trim();
+  }
 }
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/job/GravitinoCompactionJobAdapter.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/job/GravitinoCompactionJobAdapter.java
index 6ca9e67181..8010957dde 100644
--- 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/job/GravitinoCompactionJobAdapter.java
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/job/GravitinoCompactionJobAdapter.java
@@ -29,6 +29,7 @@ import java.util.TreeMap;
 import 
org.apache.gravitino.maintenance.optimizer.api.recommender.JobExecutionContext;
 import org.apache.gravitino.maintenance.optimizer.common.util.IdentifierUtils;
 import 
org.apache.gravitino.maintenance.optimizer.recommender.handler.compaction.CompactionJobContext;
+import org.apache.gravitino.policy.IcebergDataCompactionContent;
 
 public class GravitinoCompactionJobAdapter implements GravitinoJobAdapter {
 
@@ -41,11 +42,19 @@ public class GravitinoCompactionJobAdapter implements 
GravitinoJobAdapter {
         jobExecutionContext instanceof CompactionJobContext,
         "jobExecutionContext must be CompactionJobExecutionContext");
     CompactionJobContext jobContext = (CompactionJobContext) 
jobExecutionContext;
+    String rewriteStrategy =
+        jobContext.getRewriteStrategy() == null
+            ? IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY
+            : jobContext.getRewriteStrategy();
+    String sortOrder =
+        jobContext.getSortOrder() == null
+            ? IcebergDataCompactionContent.DEFAULT_SORT_ORDER
+            : jobContext.getSortOrder();
     return ImmutableMap.of(
         "table_identifier", getTableName(jobContext),
         "where_clause", getWhereClause(jobContext),
-        "sort_order", "",
-        "strategy", "binpack",
+        "sort_order", sortOrder,
+        "strategy", rewriteStrategy,
         "options", getOptions(jobContext));
   }
 
diff --git 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
index 300949a679..2fb96959b8 100644
--- 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
+++ 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestCompactionStrategyHandler.java
@@ -358,6 +358,8 @@ class TestCompactionStrategyHandler {
             tableId,
             strategy.jobOptions(),
             strategy.jobTemplateName(),
+            "binpack",
+            "",
             tableMetadata.columns(),
             tableMetadata.partitioning(),
             List.of());
diff --git 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestGravitinoPolicyCompactionStrategy.java
 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestGravitinoPolicyCompactionStrategy.java
index f36c28702e..e1e24f4add 100644
--- 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestGravitinoPolicyCompactionStrategy.java
+++ 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/handler/compaction/TestGravitinoPolicyCompactionStrategy.java
@@ -26,6 +26,7 @@ import 
org.apache.gravitino.maintenance.optimizer.api.common.StatisticEntry;
 import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyEvaluation;
 import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandlerContext;
 import org.apache.gravitino.maintenance.optimizer.common.StatisticEntryImpl;
+import 
org.apache.gravitino.maintenance.optimizer.recommender.job.GravitinoCompactionJobAdapter;
 import 
org.apache.gravitino.maintenance.optimizer.recommender.strategy.GravitinoStrategy;
 import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.policy.PolicyContent;
@@ -89,6 +90,60 @@ class TestGravitinoPolicyCompactionStrategy {
     Assertions.assertEquals(
         Map.of("target-file-size-bytes", "1048576", "min-input-files", "4"),
         jobContext.jobOptions());
+    Assertions.assertEquals("binpack", jobContext.getRewriteStrategy());
+    Assertions.assertEquals("", jobContext.getSortOrder());
     Assertions.assertTrue(jobContext.getPartitions().isEmpty());
   }
+
+  @Test
+  void testPolicyPropagatesSortStrategyToJobContext() {
+    PolicyContent content =
+        PolicyContents.icebergDataCompaction(
+            1000L,
+            1L,
+            2L,
+            10L,
+            20L,
+            "sort",
+            "id DESC NULLS LAST",
+            Map.of("target-file-size-bytes", "1048576"));
+    Policy policy = Mockito.mock(Policy.class);
+    Mockito.when(policy.name()).thenReturn("iceberg-compaction-sort-policy");
+    Mockito.when(policy.content()).thenReturn(content);
+
+    GravitinoStrategy strategy = new GravitinoStrategy(policy);
+    Assertions.assertEquals("sort", strategy.rules().get("rewriteStrategy"));
+    Assertions.assertEquals("id DESC NULLS LAST", 
strategy.rules().get("sortOrder"));
+    Assertions.assertEquals(Map.of("target-file-size-bytes", "1048576"), 
strategy.jobOptions());
+
+    NameIdentifier tableId = NameIdentifier.of("catalog", "db", "table");
+    Table tableMetadata = Mockito.mock(Table.class);
+    Mockito.when(tableMetadata.partitioning()).thenReturn(new Transform[0]);
+    Mockito.when(tableMetadata.columns()).thenReturn(new Column[0]);
+    List<StatisticEntry<?>> tableStatistics =
+        List.of(
+            new StatisticEntryImpl("custom-data-file-mse", 
StatisticValues.longValue(3000L)),
+            new StatisticEntryImpl("custom-delete-file-number", 
StatisticValues.longValue(5L)));
+
+    StrategyHandlerContext context =
+        StrategyHandlerContext.builder(tableId, strategy)
+            .withTableMetadata(tableMetadata)
+            .withTableStatistics(tableStatistics)
+            .build();
+
+    CompactionStrategyHandler handler = new CompactionStrategyHandler();
+    handler.initialize(context);
+
+    CompactionJobContext jobContext =
+        (CompactionJobContext) 
handler.evaluate().jobExecutionContext().orElseThrow();
+    Assertions.assertEquals("sort", jobContext.getRewriteStrategy());
+    Assertions.assertEquals("id DESC NULLS LAST", jobContext.getSortOrder());
+    Assertions.assertEquals(Map.of("target-file-size-bytes", "1048576"), 
jobContext.jobOptions());
+
+    GravitinoCompactionJobAdapter adapter = new 
GravitinoCompactionJobAdapter();
+    Map<String, String> jobConfig = adapter.jobConfig(jobContext);
+    Assertions.assertEquals("sort", jobConfig.get("strategy"));
+    Assertions.assertEquals("id DESC NULLS LAST", jobConfig.get("sort_order"));
+    Assertions.assertEquals("{\"target-file-size-bytes\":\"1048576\"}", 
jobConfig.get("options"));
+  }
 }
diff --git 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestBuiltinIcebergRewriteDataFiles.java
 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestBuiltinIcebergRewriteDataFiles.java
index 1e49f27214..f1f0a54267 100644
--- 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestBuiltinIcebergRewriteDataFiles.java
+++ 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestBuiltinIcebergRewriteDataFiles.java
@@ -281,6 +281,8 @@ public class TestBuiltinIcebergRewriteDataFiles {
             NameIdentifier.of(SPARK_CATALOG_NAME, "db", tableName),
             jobOptions,
             JOB_TEMPLATE_NAME,
+            "binpack",
+            "",
             columns,
             partitioning,
             partitions);
diff --git 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestGravitinoCompactionJobAdapter.java
 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestGravitinoCompactionJobAdapter.java
index 500138a7fb..dbad1c111b 100644
--- 
a/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestGravitinoCompactionJobAdapter.java
+++ 
b/maintenance/optimizer/src/test/java/org/apache/gravitino/maintenance/optimizer/recommender/job/TestGravitinoCompactionJobAdapter.java
@@ -40,10 +40,23 @@ public class TestGravitinoCompactionJobAdapter {
             "sort_order", "",
             "strategy", "binpack",
             "options", "{\"target_file_size_bytes\":\"1073741824\"}"),
-        jobAdapter.jobConfig(mockCompactionJobContext()));
+        jobAdapter.jobConfig(mockCompactionJobContext("binpack", "")));
   }
 
-  private CompactionJobContext mockCompactionJobContext() {
+  @Test
+  public void testSortStrategyAndSortOrder() {
+    GravitinoCompactionJobAdapter jobAdapter = new 
GravitinoCompactionJobAdapter();
+    Assertions.assertEquals(
+        Map.of(
+            "table_identifier", "db.table",
+            "where_clause", "",
+            "sort_order", "zorder(c1,c2)",
+            "strategy", "sort",
+            "options", "{\"target_file_size_bytes\":\"1073741824\"}"),
+        jobAdapter.jobConfig(mockCompactionJobContext("sort", 
"zorder(c1,c2)")));
+  }
+
+  private CompactionJobContext mockCompactionJobContext(String 
rewriteStrategy, String sortOrder) {
     String jobTemplateName = "compaction-job-template";
     Column[] columns = new Column[0];
     Transform[] partitioning = new Transform[0];
@@ -52,6 +65,8 @@ public class TestGravitinoCompactionJobAdapter {
         NameIdentifier.of("catalog", "db", "table"),
         jobOptions,
         jobTemplateName,
+        rewriteStrategy,
+        sortOrder,
         columns,
         partitioning,
         List.of());

Reply via email to