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

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


The following commit(s) were added to refs/heads/main by this push:
     new 52b2099f7f [VL][Iceberg] Add global iceberg sorted write (#13199)
52b2099f7f is described below

commit 52b2099f7f300504f4ca168ed803592a8ff40a13
Author: inf / Hazmi <[email protected]>
AuthorDate: Fri Oct 23 11:12:06 2026 +0300

    [VL][Iceberg] Add global iceberg sorted write (#13199)
---
 .../gluten/backendsapi/bolt/BoltRuleApi.scala      |   2 +-
 .../execution/AbstractIcebergWriteExec.scala       |   2 +
 .../enhanced/VeloxIcebergSortOrderSuite.scala      | 259 +++++++++++++++++++++
 .../execution/enhanced/VeloxIcebergSuite.scala     |  26 ---
 .../gluten/backendsapi/velox/VeloxRuleApi.scala    |   2 +-
 docs/backends/velox/get-started/VeloxIceberg.md    |  27 ++-
 .../apache/gluten/execution/IcebergWriteExec.scala |   9 +-
 .../iceberg/spark/source/IcebergWriteUtil.scala    |  31 ++-
 .../columnar/EnsureLocalSortRequirements.scala     |   4 +-
 9 files changed, 321 insertions(+), 41 deletions(-)

diff --git 
a/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltRuleApi.scala
 
b/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltRuleApi.scala
index f1fcf222b2..d89201899d 100644
--- 
a/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltRuleApi.scala
+++ 
b/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltRuleApi.scala
@@ -110,6 +110,7 @@ object BoltRuleApi {
     injector.injectPostTransform(_ => RemoveNativeWriteFilesSortAndProject())
     injector.injectPostTransform(_ => PushDownFilterToScan)
     injector.injectPostTransform(_ => PushDownInputFileExpression.PostOffload)
+    injector.injectPostTransform(_ => V2WritePostRule())
     injector.injectPostTransform(_ => EnsureLocalSortRequirements)
     injector.injectPostTransform(_ => EliminateLocalSort)
     injector.injectPostTransform(_ => PullOutDuplicateProject)
@@ -118,7 +119,6 @@ object BoltRuleApi {
     injector.injectPostTransform(c => 
HashAggregateIgnoreNullKeysRule.apply(c.session))
     injector.injectPostTransform(_ => CollectLimitTransformerRule())
     injector.injectPostTransform(_ => CollectTailTransformerRule())
-    injector.injectPostTransform(_ => V2WritePostRule())
     injector.injectPostTransform(c => 
InsertTransitions.create(c.outputsColumnar, BoltBatchType))
 
     // Gluten columnar: Fallback policies.
diff --git 
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
 
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
index 2fe2ce06ff..76143c8938 100644
--- 
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
+++ 
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
@@ -33,6 +33,8 @@ import scala.collection.JavaConverters._
 
 abstract class AbstractIcebergWriteExec extends IcebergWriteExec {
 
+  override protected def supportsSortedWrite: Boolean = true
+
   private val parquetPageRowLimitSession =
     "spark.gluten.sql.columnar.backend.velox.parquet_writer_page_row_limit"
 
diff --git 
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSortOrderSuite.scala
 
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSortOrderSuite.scala
new file mode 100644
index 0000000000..e6172af37e
--- /dev/null
+++ 
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSortOrderSuite.scala
@@ -0,0 +1,259 @@
+/*
+ * 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.gluten.execution.enhanced
+
+import org.apache.gluten.execution._
+import org.apache.gluten.tags.EnhancedFeaturesTest
+
+import org.apache.spark.SparkConf
+import org.apache.spark.sql.DataFrame
+import org.apache.spark.sql.execution.CommandResultExec
+
+import org.apache.hadoop.fs.Path
+import org.apache.iceberg.{DataFile, SortOrderComparators, Table}
+import org.apache.iceberg.data.{GenericRecord, Record}
+import org.apache.iceberg.shaded.org.apache.parquet.example.data.Group
+import org.apache.iceberg.shaded.org.apache.parquet.hadoop.ParquetReader
+import 
org.apache.iceberg.shaded.org.apache.parquet.hadoop.example.GroupReadSupport
+import org.apache.iceberg.spark.Spark3Util
+
+import scala.collection.JavaConverters._
+
+@EnhancedFeaturesTest
+class VeloxIcebergSortOrderSuite extends WholeStageTransformerSuite {
+
+  override protected val resourcePath: String = "/tpch-data-parquet"
+  override protected val fileFormat: String = "parquet"
+
+  override protected def sparkConf: SparkConf = super.sparkConf
+    .set("spark.gluten.sql.enable.enhancedFeatures", "true")
+    .set("spark.gluten.sql.columnar.maxBatchSize", "64")
+    .set("spark.sql.ansi.enabled", "false")
+    .set("spark.sql.shuffle.partitions", "4")
+    .set("spark.sql.adaptive.enabled", "true")
+    .set("spark.sql.adaptive.coalescePartitions.enabled", "false")
+    .set("spark.shuffle.manager", 
"org.apache.spark.shuffle.sort.ColumnarShuffleManager")
+    .set(
+      "spark.sql.extensions",
+      "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions")
+    .set("spark.sql.catalog.spark_catalog", 
"org.apache.iceberg.spark.SparkCatalog")
+    .set("spark.sql.catalog.spark_catalog.type", "hadoop")
+    .set(
+      "spark.sql.catalog.spark_catalog.warehouse",
+      
s"file://${getClass.getResource("/").getPath}/iceberg-sort-order-warehouse")
+
+  private def input: DataFrame = spark
+    .range(0, 1024, 1, 4)
+    .selectExpr("CAST(pmod(id * 37, 1024) AS INT) AS id")
+    .selectExpr(
+      "id",
+      "CASE WHEN id % 7 = 0 THEN NULL ELSE id % 5 END AS category",
+      "named_struct('rank', id % 11) AS nested",
+      "concat(sha2(CAST(id AS STRING), 256), sha2(CAST(id + 1024 AS STRING), 
256)) AS payload"
+    )
+
+  private def createTable(partitioning: String = ""): Unit = {
+    spark.sql(s"""
+                 |CREATE TABLE iceberg_sort_order (
+                 |  id INT, category INT, nested STRUCT<rank: INT>, payload 
STRING
+                 |) USING iceberg $partitioning
+                 |TBLPROPERTIES ('write.parquet.compression-codec' = 
'uncompressed')
+                 |""".stripMargin)
+  }
+
+  private def nativeWrite(sql: String): IcebergWriteExec = {
+    val result = spark.sql(sql)
+    val plan =
+      
result.queryExecution.executedPlan.asInstanceOf[CommandResultExec].commandPhysicalPlan
+    assert(plan.isInstanceOf[IcebergWriteExec], s"Expected native Iceberg 
write: $plan")
+    plan.asInstanceOf[IcebergWriteExec]
+  }
+
+  private def readFiles(table: Table): Vector[(DataFile, Vector[Record])] = {
+    table.refresh()
+    val tasks = table.newScan().planFiles()
+    val files =
+      try {
+        tasks.asScala.map(_.file().copy()).toVector
+      } finally {
+        tasks.close()
+      }
+    files.map {
+      file =>
+        val reader = ParquetReader
+          .builder[Group](new GroupReadSupport(), new 
Path(file.path().toString))
+          .withConf(spark.sessionState.newHadoopConf())
+          .build()
+        val rows =
+          try {
+            Iterator.continually(reader.read()).takeWhile(_ != null).map {
+              group =>
+                val row = GenericRecord.create(table.schema())
+                row.setField("id", Int.box(group.getInteger("id", 0)))
+                if (group.getFieldRepetitionCount("category") > 0) {
+                  row.setField("category", 
Int.box(group.getInteger("category", 0)))
+                }
+                val nested = 
GenericRecord.create(table.schema().findType("nested").asStructType())
+                nested.setField("rank", Int.box(group.getGroup("nested", 
0).getInteger("rank", 0)))
+                row.setField("nested", nested)
+                row.setField("payload", group.getString("payload", 0))
+                row: Record
+            }.toVector
+          } finally {
+            reader.close()
+          }
+        assert(rows.nonEmpty)
+        (file, rows)
+    }
+  }
+
+  private def assertSortedFiles(global: Boolean): Vector[(DataFile, 
Vector[Record])] = {
+    val table = Spark3Util.loadIcebergTable(spark, "iceberg_sort_order")
+    val files = readFiles(table)
+    assert(files.size > 1, "Sorting must be tested across multiple output 
files")
+    val comparator = SortOrderComparators.forSchema(table.schema(), 
table.sortOrder())
+    files.foreach {
+      case (file, rows) =>
+        assert(file.sortOrderId() == table.sortOrder().orderId())
+        rows.sliding(2).filter(_.size == 2).foreach {
+          pair =>
+            assert(comparator.compare(pair.head, pair.last) <= 0, s"Unsorted 
file: ${file.path()}")
+        }
+    }
+    val ids = 
files.flatMap(_._2.map(_.getField("id").asInstanceOf[Int])).sorted
+    assert(ids == (0 until 1024).toVector)
+
+    if (global) {
+      files.groupBy(_._1.partition().toString).values.foreach {
+        partitionFiles =>
+          val ranges = partitionFiles.map(_._2).sortWith {
+            (left, right) => comparator.compare(left.head, right.head) < 0
+          }
+          ranges.sliding(2).filter(_.size == 2).foreach {
+            pair =>
+              assert(
+                comparator.compare(pair.head.last, pair.last.head) <= 0,
+                "Output files have overlapping sort-key ranges")
+          }
+      }
+    }
+    files
+  }
+
+  for {
+    (name, partitioning, ordering, global) <- Seq(
+      ("global", "", "ORDERED BY category ASC NULLS LAST, id DESC NULLS 
FIRST", true),
+      ("local", "", "LOCALLY ORDERED BY category DESC NULLS FIRST, id ASC 
NULLS LAST", false),
+      (
+        "partitioned",
+        "PARTITIONED BY (bucket(2, id))",
+        "ORDERED BY category DESC NULLS LAST, id ASC",
+        true),
+      (
+        "transformed",
+        "",
+        "ORDERED BY truncate(3, category) DESC NULLS FIRST, id ASC",
+        true),
+      ("nested", "", "ORDERED BY nested.rank ASC, id DESC", true)
+    )
+  } {
+    test(s"iceberg native $name sort order") {
+      withTable("iceberg_sort_order") {
+        withTempView("sort_order_input") {
+          createTable(partitioning)
+          spark.sql(s"ALTER TABLE iceberg_sort_order WRITE $ordering")
+          if (!global) {
+            spark.sql("""
+                        |ALTER TABLE iceberg_sort_order SET TBLPROPERTIES (
+                        |  'write.distribution-mode' = 'none')
+                        |""".stripMargin)
+          }
+          input.createOrReplaceTempView("sort_order_input")
+          val write = nativeWrite("INSERT INTO iceberg_sort_order SELECT * 
FROM sort_order_input")
+          if (name == "global" || name == "local") {
+            assert(collect(write.query) { case s: SortExecTransformer => s 
}.nonEmpty)
+          }
+          assertSortedFiles(global)
+        }
+      }
+    }
+  }
+
+  test("iceberg sort order survives file rotation and overwrite") {
+    withSQLConf("spark.sql.adaptive.enabled" -> "false") {
+      withTable("iceberg_sort_order") {
+        withTempView("sort_order_input") {
+          createTable()
+          spark.sql("ALTER TABLE iceberg_sort_order WRITE ORDERED BY id DESC")
+          spark.sql("""
+                      |ALTER TABLE iceberg_sort_order SET TBLPROPERTIES (
+                      |  'write.target-file-size-bytes' = '8192',
+                      |  'write.parquet.page-size-bytes' = '1024')
+                      |""".stripMargin)
+          input.createOrReplaceTempView("sort_order_input")
+          nativeWrite("INSERT INTO iceberg_sort_order SELECT * FROM 
sort_order_input")
+          val files = assertSortedFiles(global = true)
+          assert(files.size > 4, "Expected file rotation within write tasks")
+
+          spark.sql("ALTER TABLE iceberg_sort_order WRITE ORDERED BY category, 
id")
+          nativeWrite("INSERT OVERWRITE iceberg_sort_order SELECT * FROM 
sort_order_input")
+          assertSortedFiles(global = true)
+        }
+      }
+    }
+  }
+
+  test("iceberg copy-on-write UPDATE preserves sort order") {
+    withTable("iceberg_sort_order") {
+      withTempView("sort_order_input") {
+        createTable()
+        spark.sql(
+          "ALTER TABLE iceberg_sort_order WRITE ORDERED BY category DESC NULLS 
LAST, id ASC")
+        input.createOrReplaceTempView("sort_order_input")
+        nativeWrite("INSERT INTO iceberg_sort_order SELECT * FROM 
sort_order_input")
+        nativeWrite("UPDATE iceberg_sort_order SET category = 20 - category 
WHERE id % 2 = 0")
+        assertSortedFiles(global = false)
+      }
+    }
+  }
+
+  for (aqe <- Seq(false, true)) {
+    test(s"iceberg local sort order survives native hash aggregation: 
aqe=$aqe") {
+      withSQLConf("spark.sql.adaptive.enabled" -> aqe.toString) {
+        withTable("iceberg_sort_order") {
+          withTempView("sort_order_input") {
+            createTable()
+            spark.sql("ALTER TABLE iceberg_sort_order WRITE LOCALLY ORDERED BY 
id")
+            spark.sql("""
+                        |ALTER TABLE iceberg_sort_order SET TBLPROPERTIES (
+                        |  'write.distribution-mode' = 'none')
+                        |""".stripMargin)
+            input.createOrReplaceTempView("sort_order_input")
+            val write = nativeWrite("""
+                                      |INSERT INTO iceberg_sort_order
+                                      |SELECT id, first(category), 
first(nested), max(payload)
+                                      |FROM sort_order_input GROUP BY id
+                                      |""".stripMargin)
+            assert(collect(write.query) { case a: HashAggregateExecTransformer 
=> a }.nonEmpty)
+            assertSortedFiles(global = false)
+            assert(collect(write.query) { case s: SortExecTransformer => s 
}.nonEmpty)
+          }
+        }
+      }
+    }
+  }
+}
diff --git 
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
 
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
index 145e9f6a49..94c8844612 100644
--- 
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
+++ 
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
@@ -24,7 +24,6 @@ import org.apache.gluten.tags.EnhancedFeaturesTest
 
 import org.apache.spark.sql.{DataFrame, Row}
 import org.apache.spark.sql.execution.{CommandExecutionMode, CommandResultExec}
-import org.apache.spark.sql.execution.GlutenImplicits._
 import org.apache.spark.sql.execution.datasources.v2.AppendDataExec
 import org.apache.spark.sql.execution.streaming.MemoryStream
 import org.apache.spark.sql.gluten.TestUtils
@@ -463,31 +462,6 @@ class VeloxIcebergSuite extends IcebergSuite {
     }
   }
 
-  test("iceberg native write fallback when validation fails - sort order") {
-    withTable("iceberg_sorted_tbl") {
-      spark.sql("CREATE TABLE iceberg_sorted_tbl (a INT, b STRING) USING 
iceberg")
-      spark.sql("ALTER TABLE iceberg_sorted_tbl WRITE ORDERED BY a")
-
-      val df = spark.sql("INSERT INTO iceberg_sorted_tbl VALUES (1, 'hello'), 
(2, 'world')")
-
-      // Should fallback to vanilla Spark's AppendDataExec.
-      val commandPlan =
-        
df.queryExecution.executedPlan.asInstanceOf[CommandResultExec].commandPhysicalPlan
-      assert(commandPlan.isInstanceOf[AppendDataExec])
-      assert(!commandPlan.isInstanceOf[VeloxIcebergAppendDataExec])
-
-      checkAnswer(
-        spark.sql("SELECT * FROM iceberg_sorted_tbl ORDER BY a"),
-        Seq(Row(1, "hello"), Row(2, "world")))
-
-      // Verify fallbackSummary reports the sort order fallback reason.
-      val summary = df.fallbackSummary()
-      assert(
-        summary.fallbackNodeToReason.exists(
-          _.values.exists(_.contains("Not support write table with sort 
order"))))
-    }
-  }
-
   test("iceberg read cow table - update after schema evolution") {
     withTable("iceberg_cow_update_evolved_tb") {
       spark.sql("""
diff --git 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxRuleApi.scala
 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxRuleApi.scala
index 63c8880e8a..07de0ab0f0 100644
--- 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxRuleApi.scala
+++ 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxRuleApi.scala
@@ -128,13 +128,13 @@ object VeloxRuleApi {
     injector.injectPostTransform(_ => PushDownFilterToScan)
     injector.injectPostTransform(_ => ScanMapKeyPruning)
     injector.injectPostTransform(_ => PushDownInputFileExpression.PostOffload)
+    injector.injectPostTransform(_ => V2WritePostRule())
     injector.injectPostTransform(_ => EnsureLocalSortRequirements)
     injector.injectPostTransform(_ => EliminateLocalSort)
     injector.injectPostTransform(_ => CollapseProjectExecTransformer)
     injector.injectPostTransform(c => 
FlushableHashAggregateRule.apply(c.session))
     injector.injectPostTransform(_ => CollectLimitTransformerRule())
     injector.injectPostTransform(_ => CollectTailTransformerRule())
-    injector.injectPostTransform(_ => V2WritePostRule())
     injector.injectPostTransform(c => 
InsertTransitions.create(c.outputsColumnar, VeloxBatchType))
 
     // Gluten columnar: Fallback policies.
diff --git a/docs/backends/velox/get-started/VeloxIceberg.md 
b/docs/backends/velox/get-started/VeloxIceberg.md
index b9f18256a4..929f3fda8b 100644
--- a/docs/backends/velox/get-started/VeloxIceberg.md
+++ b/docs/backends/velox/get-started/VeloxIceberg.md
@@ -30,16 +30,27 @@ Fallback
 Fallback
 
 ## Writing
-Fallback
-````
+Supported Parquet writes can offload when
+`spark.gluten.sql.enable.enhancedFeatures` and
+`spark.gluten.sql.columnar.iceberg.enableNativeWrite` are enabled.
+```sql
 INSERT INTO local.db.table VALUES (1, 'a'), (2, 'b'), (3, 'c');
-````
-PartialOffload
-
-The write is fallback while read is offload.
-````
 INSERT INTO local.db.table SELECT id, data FROM source WHERE length(data) = 1;
-````
+```
+
+### Sorted native writes
+
+With `spark.gluten.sql.enable.enhancedFeatures` and
+`spark.gluten.sql.columnar.iceberg.enableNativeWrite` enabled, the Velox writer
+supports Iceberg table sort orders. Spark plans the required distribution and
+ordering before the native write:
+
+```sql
+ALTER TABLE local.db.table WRITE ORDERED BY category ASC NULLS LAST, id DESC;
+-- Sort within each task without requiring global range distribution:
+ALTER TABLE local.db.table WRITE LOCALLY ORDERED BY category, id;
+ALTER TABLE local.db.table SET TBLPROPERTIES ('write.distribution-mode' = 
'none');
+```
 
 ## Reading
 ### Read data
diff --git 
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
 
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
index cf5c047514..f67490f28e 100644
--- 
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
+++ 
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
@@ -18,6 +18,8 @@ package org.apache.gluten.execution
 
 import org.apache.gluten.backendsapi.BackendsApiManager
 
+import org.apache.spark.sql.catalyst.expressions.SortOrder
+
 import org.apache.iceberg.{FileFormat, PartitionField, PartitionSpec, Schema, 
TableProperties}
 import org.apache.iceberg.TableProperties._
 import org.apache.iceberg.avro.AvroSchemaUtil
@@ -30,6 +32,11 @@ import scala.collection.JavaConverters._
 
 trait IcebergWriteExec extends ColumnarV2TableWriteExec {
 
+  protected def supportsSortedWrite: Boolean = false
+
+  override lazy val requiredChildOrdering: Seq[Seq[SortOrder]] =
+    Seq(IcebergWriteUtil.getRequiredOrdering(write, query.output))
+
   protected def getFileFormat(format: FileFormat): Int = {
     format match {
       case FileFormat.PARQUET => 1
@@ -130,7 +137,7 @@ trait IcebergWriteExec extends ColumnarV2TableWriteExec {
           "Not support write unsupported partition type, or is nested 
partition column")
       }
     }
-    if (IcebergWriteUtil.getTable(write).sortOrder().isSorted) {
+    if (!supportsSortedWrite && IcebergWriteUtil.getSortOrder(write).isSorted) 
{
       return ValidationResult.failed("Not support write table with sort order")
     }
     val format = IcebergWriteUtil.getFileFormat(write)
diff --git 
a/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/IcebergWriteUtil.scala
 
b/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/IcebergWriteUtil.scala
index 83ecb07037..e8792556d6 100644
--- 
a/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/IcebergWriteUtil.scala
+++ 
b/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/IcebergWriteUtil.scala
@@ -16,10 +16,15 @@
  */
 package org.apache.iceberg.spark.source
 
-import org.apache.spark.sql.connector.write.{Write, WriterCommitMessage}
+import org.apache.spark.sql.catalyst.expressions.{Attribute, SortOrder => 
CatalystSortOrder}
+import org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, Sort}
+import org.apache.spark.sql.connector.distributions.{Distribution, 
Distributions}
+import org.apache.spark.sql.connector.expressions.{SortOrder => 
ConnectorSortOrder}
+import org.apache.spark.sql.connector.write.{RequiresDistributionAndOrdering, 
Write, WriterCommitMessage}
+import 
org.apache.spark.sql.execution.datasources.v2.DistributionAndOrderingUtils
 
 import org.apache.iceberg._
-import org.apache.iceberg.spark.SparkWriteConf
+import org.apache.iceberg.spark.{SparkFunctionCatalog, SparkWriteConf}
 import org.apache.iceberg.spark.source.SparkWrite.TaskCommit
 import org.apache.iceberg.types.Type
 import org.apache.iceberg.types.Type.TypeID
@@ -114,7 +119,27 @@ object IcebergWriteUtil {
   }
 
   def getSortOrder(write: Write): SortOrder = {
-    getTable(write).sortOrder()
+    if (write.asInstanceOf[SparkWrite].requiredOrdering().nonEmpty) {
+      getTable(write).sortOrder()
+    } else {
+      SortOrder.unsorted()
+    }
+  }
+
+  def getRequiredOrdering(write: Write, output: Seq[Attribute]): 
Seq[CatalystSortOrder] = {
+    // Distribution may reference metadata columns, such as _file, absent from 
the writer's input.
+    val orderingOnly = new RequiresDistributionAndOrdering {
+      override def requiredDistribution(): Distribution = 
Distributions.unspecified()
+      override def requiredOrdering(): Array[ConnectorSortOrder] = write match 
{
+        case ordered: RequiresDistributionAndOrdering => 
ordered.requiredOrdering()
+        case _ => Array.empty
+      }
+    }
+    DistributionAndOrderingUtils
+      .prepareQuery(orderingOnly, LocalRelation(output), 
Some(SparkFunctionCatalog.get())) match {
+      case sort: Sort => sort.order
+      case _ => Nil
+    }
   }
 
   def getPartitionSpec(write: Write): PartitionSpec = {
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/EnsureLocalSortRequirements.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/EnsureLocalSortRequirements.scala
index 008735f760..bc2d762209 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/EnsureLocalSortRequirements.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/EnsureLocalSortRequirements.scala
@@ -16,7 +16,7 @@
  */
 package org.apache.gluten.extension.columnar
 
-import org.apache.gluten.execution.GlutenPlan
+import org.apache.gluten.execution.{ColumnarV2TableWriteExec, GlutenPlan}
 import org.apache.gluten.extension.columnar.heuristic.HeuristicTransform
 
 import org.apache.spark.sql.catalyst.expressions.SortOrder
@@ -75,6 +75,8 @@ object EnsureLocalSortRequirements extends Rule[SparkPlan] {
         transform.apply(newChild)
       case (parent: GlutenPlan, _) if parent.supportsColumnar =>
         transform.apply(newChild)
+      case (_: ColumnarV2TableWriteExec, _) =>
+        transform.apply(newChild)
       case _ =>
         newChild
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to