This is an automated email from the ASF dual-hosted git repository.
jackylee-ch 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 ca82b7721f [GLUTEN-12698][CORE] Add Iceberg read/write offload
switches (#12699)
ca82b7721f is described below
commit ca82b7721f93e068caab8c96efbbdf2a9499bdb0
Author: jackylee <[email protected]>
AuthorDate: Tue Aug 11 09:31:48 2026 +0800
[GLUTEN-12698][CORE] Add Iceberg read/write offload switches (#12699)
---
.../gluten/extension/OffloadIcebergWrite.scala | 67 +++++++++++++++-------
.../execution/enhanced/VeloxIcebergSuite.scala | 31 ++++++++++
docs/get-started/VeloxIceberg.md | 8 +++
.../apache/gluten/config/GlutenIcebergConfig.scala | 49 ++++++++++++++++
.../gluten/extension/OffloadIcebergScan.scala | 15 +++--
.../org/apache/gluten/execution/IcebergSuite.scala | 51 ++++++++++++++++
6 files changed, 195 insertions(+), 26 deletions(-)
diff --git
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/extension/OffloadIcebergWrite.scala
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/extension/OffloadIcebergWrite.scala
index e0fcbae6c6..05d19334d2 100644
---
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/extension/OffloadIcebergWrite.scala
+++
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/extension/OffloadIcebergWrite.scala
@@ -16,7 +16,7 @@
*/
package org.apache.gluten.extension
-import org.apache.gluten.config.GlutenConfig
+import org.apache.gluten.config.{GlutenConfig, GlutenIcebergConfig}
import org.apache.gluten.execution._
import org.apache.gluten.extension.columnar.heuristic.HeuristicTransform
import org.apache.gluten.extension.columnar.offload.OffloadSingleNode
@@ -29,42 +29,67 @@ import org.apache.spark.sql.execution.datasources.v2._
import org.apache.iceberg.spark.source.IcebergWriteUtil.supportsWrite
case class OffloadIcebergAppend() extends OffloadSingleNode {
- override def offload(plan: SparkPlan): SparkPlan = plan match {
- case a: AppendDataExec if supportsWrite(a.write) =>
- VeloxIcebergAppendDataExec(a)
- case other => other
+ override def offload(plan: SparkPlan): SparkPlan = {
+ if (!GlutenIcebergConfig.get.enableNativeWrite) {
+ return plan
+ }
+ plan match {
+ case a: AppendDataExec if supportsWrite(a.write) =>
+ VeloxIcebergAppendDataExec(a)
+ case other => other
+ }
}
}
case class OffloadIcebergReplaceData() extends OffloadSingleNode {
- override def offload(plan: SparkPlan): SparkPlan = plan match {
- case r: ReplaceDataExec if supportsWrite(r.write) =>
- VeloxIcebergReplaceDataExec(r)
- case other => other
+ override def offload(plan: SparkPlan): SparkPlan = {
+ if (!GlutenIcebergConfig.get.enableNativeWrite) {
+ return plan
+ }
+ plan match {
+ case r: ReplaceDataExec if supportsWrite(r.write) =>
+ VeloxIcebergReplaceDataExec(r)
+ case other => other
+ }
}
}
case class OffloadIcebergOverwrite() extends OffloadSingleNode {
- override def offload(plan: SparkPlan): SparkPlan = plan match {
- case r: OverwriteByExpressionExec if supportsWrite(r.write) =>
- VeloxIcebergOverwriteByExpressionExec(r)
- case other => other
+ override def offload(plan: SparkPlan): SparkPlan = {
+ if (!GlutenIcebergConfig.get.enableNativeWrite) {
+ return plan
+ }
+ plan match {
+ case r: OverwriteByExpressionExec if supportsWrite(r.write) =>
+ VeloxIcebergOverwriteByExpressionExec(r)
+ case other => other
+ }
}
}
case class OffloadIcebergOverwritePartitionsDynamic() extends
OffloadSingleNode {
- override def offload(plan: SparkPlan): SparkPlan = plan match {
- case r: OverwritePartitionsDynamicExec if supportsWrite(r.write) =>
- VeloxIcebergOverwritePartitionsDynamicExec(r)
- case other => other
+ override def offload(plan: SparkPlan): SparkPlan = {
+ if (!GlutenIcebergConfig.get.enableNativeWrite) {
+ return plan
+ }
+ plan match {
+ case r: OverwritePartitionsDynamicExec if supportsWrite(r.write) =>
+ VeloxIcebergOverwritePartitionsDynamicExec(r)
+ case other => other
+ }
}
}
case class OffloadIcebergWriteToDataSourceV2() extends OffloadSingleNode {
- override def offload(plan: SparkPlan): SparkPlan = plan match {
- case r: WriteToDataSourceV2Exec =>
- VeloxIcebergWriteToDataSourceV2Exec(r).getOrElse(r)
- case other => other
+ override def offload(plan: SparkPlan): SparkPlan = {
+ if (!GlutenIcebergConfig.get.enableNativeWrite) {
+ return plan
+ }
+ plan match {
+ case r: WriteToDataSourceV2Exec =>
+ VeloxIcebergWriteToDataSourceV2Exec(r).getOrElse(r)
+ case other => other
+ }
}
}
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 3a8c48d5e9..f3d8e290d4 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
@@ -16,6 +16,7 @@
*/
package org.apache.gluten.execution.enhanced
+import org.apache.gluten.config.GlutenIcebergConfig
import org.apache.gluten.config.VeloxConfig.MAX_TARGET_FILE_SIZE_SESSION
import org.apache.gluten.execution._
import org.apache.gluten.tags.EnhancedFeaturesTest
@@ -775,4 +776,34 @@ class VeloxIcebergSuite extends IcebergSuite {
}
}
}
+
+ test("iceberg write falls back when native write is disabled") {
+ withTable("iceberg_write_switch_tbl") {
+ spark.sql("CREATE TABLE iceberg_write_switch_tbl (a INT, b STRING) USING
iceberg")
+
+ withSQLConf(GlutenIcebergConfig.ENABLE_NATIVE_WRITE.key -> "false") {
+ val df = spark.sql("INSERT INTO iceberg_write_switch_tbl VALUES (1,
'hello')")
+ val commandPlan =
+
df.queryExecution.executedPlan.asInstanceOf[CommandResultExec].commandPhysicalPlan
+ assert(
+ !commandPlan.isInstanceOf[VeloxIcebergAppendDataExec],
+ s"Iceberg write should not be offloaded when native write is
disabled: $commandPlan")
+ assert(commandPlan.isInstanceOf[AppendDataExec])
+ }
+
+ // Reads stay offloaded: the write switch must not affect the read path.
+ runQueryAndCompare("SELECT * FROM iceberg_write_switch_tbl") {
+ checkGlutenPlan[IcebergScanTransformer]
+ }
+
+ // The switch is dynamic: offload resumes once it is back to the default.
+ TestUtils.checkExecutedPlanContains[VeloxIcebergAppendDataExec](
+ spark,
+ "INSERT INTO iceberg_write_switch_tbl VALUES (2, 'world')")
+
+ checkAnswer(
+ spark.sql("SELECT * FROM iceberg_write_switch_tbl ORDER BY a"),
+ Seq(Row(1, "hello"), Row(2, "world")))
+ }
+ }
}
diff --git a/docs/get-started/VeloxIceberg.md b/docs/get-started/VeloxIceberg.md
index aea2e89c76..42901a26cf 100644
--- a/docs/get-started/VeloxIceberg.md
+++ b/docs/get-started/VeloxIceberg.md
@@ -98,6 +98,14 @@ Gluten uses column name to match the parquet file, so if the
column is renamed o
the added column name is same to the deleted column, the scan will fall back.
## Configuration
+### Gluten Options
+| Gluten option | Default | Description |
+| --- | --- | --- |
+| spark.gluten.sql.columnar.iceberg.enableNativeRead | true | Enable
offloading Iceberg scans to the native backend. When disabled, Iceberg scans
fall back to vanilla Spark while scans of other formats stay offloaded. |
+| spark.gluten.sql.columnar.iceberg.enableNativeWrite | true | Enable
offloading Iceberg writes to the native backend. When disabled, Iceberg writes
fall back to vanilla Spark. Note the Velox backend additionally requires
`spark.gluten.sql.enable.enhancedFeatures` to be enabled. |
+
+Both options are runtime modifiable, so they can be flipped per session with
`SET`.
+
### Catalogs
All the catalog configurations are transparent to Gluten
diff --git
a/gluten-iceberg/src/main/scala/org/apache/gluten/config/GlutenIcebergConfig.scala
b/gluten-iceberg/src/main/scala/org/apache/gluten/config/GlutenIcebergConfig.scala
new file mode 100644
index 0000000000..c148ff135b
--- /dev/null
+++
b/gluten-iceberg/src/main/scala/org/apache/gluten/config/GlutenIcebergConfig.scala
@@ -0,0 +1,49 @@
+/*
+ * 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.config
+
+import org.apache.spark.sql.internal.SQLConf
+
+class GlutenIcebergConfig(conf: SQLConf) extends GlutenCoreConfig(conf) {
+ import GlutenIcebergConfig._
+
+ def enableNativeRead: Boolean = getConf(ENABLE_NATIVE_READ)
+
+ def enableNativeWrite: Boolean = getConf(ENABLE_NATIVE_WRITE)
+}
+
+object GlutenIcebergConfig extends ConfigRegistry {
+
+ def get: GlutenIcebergConfig = {
+ new GlutenIcebergConfig(SQLConf.get)
+ }
+
+ val ENABLE_NATIVE_READ: ConfigEntry[Boolean] =
+ buildConf("spark.gluten.sql.columnar.iceberg.enableNativeRead")
+ .doc("Enable offloading Iceberg scans to the native backend. When
disabled, Iceberg scans" +
+ " fall back to vanilla Spark while scans of other formats stay
offloaded.")
+ .booleanConf
+ .createWithDefault(true)
+
+ val ENABLE_NATIVE_WRITE: ConfigEntry[Boolean] =
+ buildConf("spark.gluten.sql.columnar.iceberg.enableNativeWrite")
+ .doc("Enable offloading Iceberg writes to the native backend. When
disabled, Iceberg" +
+ " writes fall back to vanilla Spark. Note the Velox backend
additionally requires" +
+ " spark.gluten.sql.enable.enhancedFeatures to be enabled.")
+ .booleanConf
+ .createWithDefault(true)
+}
diff --git
a/gluten-iceberg/src/main/scala/org/apache/gluten/extension/OffloadIcebergScan.scala
b/gluten-iceberg/src/main/scala/org/apache/gluten/extension/OffloadIcebergScan.scala
index 505e063146..c043449dc9 100644
---
a/gluten-iceberg/src/main/scala/org/apache/gluten/extension/OffloadIcebergScan.scala
+++
b/gluten-iceberg/src/main/scala/org/apache/gluten/extension/OffloadIcebergScan.scala
@@ -16,7 +16,7 @@
*/
package org.apache.gluten.extension
-import org.apache.gluten.config.GlutenConfig
+import org.apache.gluten.config.{GlutenConfig, GlutenIcebergConfig}
import org.apache.gluten.execution.IcebergScanTransformer
import org.apache.gluten.extension.columnar.heuristic.HeuristicTransform
import org.apache.gluten.extension.columnar.offload.OffloadSingleNode
@@ -27,10 +27,15 @@ import org.apache.spark.sql.execution.SparkPlan
import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
case class OffloadIcebergScan() extends OffloadSingleNode {
- override def offload(plan: SparkPlan): SparkPlan = plan match {
- case scan: BatchScanExec if
IcebergScanTransformer.supportsBatchScan(scan.scan) =>
- IcebergScanTransformer(scan)
- case other => other
+ override def offload(plan: SparkPlan): SparkPlan = {
+ if (!GlutenIcebergConfig.get.enableNativeRead) {
+ return plan
+ }
+ plan match {
+ case scan: BatchScanExec if
IcebergScanTransformer.supportsBatchScan(scan.scan) =>
+ IcebergScanTransformer(scan)
+ case other => other
+ }
}
}
diff --git
a/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala
b/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala
index 7f9b0c533e..56f3fbdace 100644
---
a/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala
+++
b/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala
@@ -16,8 +16,11 @@
*/
package org.apache.gluten.execution
+import org.apache.gluten.config.GlutenIcebergConfig
+
import org.apache.spark.SparkConf
import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
abstract class IcebergSuite extends WholeStageTransformerSuite {
protected val rootPath: String = getClass.getResource("/").getPath
@@ -717,4 +720,52 @@ abstract class IcebergSuite extends
WholeStageTransformerSuite {
e.getCause != null && e.getCause.getMessage.contains("null"))
}
}
+
+ test("iceberg scan falls back when native read is disabled") {
+ withTable("iceberg_read_switch_tb") {
+ spark.sql("""
+ |create table iceberg_read_switch_tb using iceberg as
+ |(select 1 as col1, 2 as col2)
+ |""".stripMargin)
+
+ withSQLConf(GlutenIcebergConfig.ENABLE_NATIVE_READ.key -> "false") {
+ val df = spark.sql("select * from iceberg_read_switch_tb")
+ checkSparkPlan[BatchScanExec](df)
+ assert(
+ !getExecutedPlan(df).exists(_.isInstanceOf[IcebergScanTransformer]),
+ "Iceberg scan should not be offloaded when native read is disabled")
+ checkAnswer(df, Seq(Row(1, 2)))
+ }
+
+ // The switch is dynamic: offload resumes once it is back to the default.
+ runQueryAndCompare("select * from iceberg_read_switch_tb") {
+ checkGlutenPlan[IcebergScanTransformer]
+ }
+ }
+ }
+
+ test("disabling iceberg native read keeps other scans offloaded") {
+ withTable("iceberg_read_switch_tb") {
+ spark.sql("""
+ |create table iceberg_read_switch_tb using iceberg as
+ |(select 1 as col1)
+ |""".stripMargin)
+
+ withTempPath {
+ path =>
+ spark.range(5).toDF("col1").write.parquet(path.getCanonicalPath)
+
+ withSQLConf(GlutenIcebergConfig.ENABLE_NATIVE_READ.key -> "false") {
+ val icebergDf = spark.sql("select * from iceberg_read_switch_tb")
+ assert(
+
!getExecutedPlan(icebergDf).exists(_.isInstanceOf[IcebergScanTransformer]),
+ "Iceberg scan should fall back")
+
+ val parquetDf = spark.read.parquet(path.getCanonicalPath)
+ checkGlutenPlan[FileSourceScanExecTransformer](parquetDf)
+ assert(parquetDf.count() == 5)
+ }
+ }
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]