Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr_build_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ jobs:
org.apache.comet.CometIcebergNativeSuite
org.apache.comet.CometIcebergEncryptionSuite
org.apache.comet.CometIcebergRewriteActionSuite
org.apache.comet.CometIcebergWriteActionSuite
org.apache.comet.iceberg.IcebergReflectionSuite
org.apache.comet.csv.CometCsvNativeReadSuite
org.apache.comet.CometFuzzTestSuite
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr_build_macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ jobs:
org.apache.comet.CometIcebergNativeSuite
org.apache.comet.CometIcebergEncryptionSuite
org.apache.comet.CometIcebergRewriteActionSuite
org.apache.comet.CometIcebergWriteActionSuite
org.apache.comet.iceberg.IcebergReflectionSuite
org.apache.comet.csv.CometCsvNativeReadSuite
org.apache.comet.CometFuzzTestSuite
Expand Down
56 changes: 56 additions & 0 deletions dev/diffs/iceberg/1.10.0.diff
Original file line number Diff line number Diff line change
Expand Up @@ -3025,3 +3025,59 @@ index 9d2ce2b388..5e23368848 100644
} else {
assertThat(planAsString).as("Should be no post scan filter").doesNotContain("Filter (");
}
diff --git a/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java b/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
index 5a0a04edb..afe92a8b7 100644
--- a/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
+++ b/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
@@ -38,6 +38,7 @@ import org.apache.spark.sql.catalyst.expressions.ApplyFunctionExpression;
import org.apache.spark.sql.catalyst.expressions.Expression;
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke;
import org.apache.spark.sql.execution.CommandResultExec;
+import org.apache.spark.sql.execution.SparkPlan;
import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -313,9 +314,13 @@ public class TestSystemFunctionPushDownInRowLevelOperations extends ExtensionsTe

private List<Expression> executeAndCollectFunctionCalls(String query, Object... args) {
CommandResultExec command = (CommandResultExec) executeAndKeepPlan(query, args);
- V2TableWriteExec write = (V2TableWriteExec) command.commandPhysicalPlan();
+ // Comet's split-operator Iceberg write plans the command as IcebergCommitExec, which is not
+ // a V2TableWriteExec; collect over the whole command plan in that case.
+ SparkPlan plan = command.commandPhysicalPlan();
+ SparkPlan queryPlan =
+ plan instanceof V2TableWriteExec ? ((V2TableWriteExec) plan).query() : plan;
return SparkPlanUtil.collectExprs(
- write.query(),
+ queryPlan,
expr -> expr instanceof StaticInvoke || expr instanceof ApplyFunctionExpression);
}

diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
index 8cb92224f..777b9bb1d 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
@@ -38,6 +38,7 @@ import org.apache.spark.sql.catalyst.expressions.ApplyFunctionExpression;
import org.apache.spark.sql.catalyst.expressions.Expression;
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke;
import org.apache.spark.sql.execution.CommandResultExec;
+import org.apache.spark.sql.execution.SparkPlan;
import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -313,9 +314,13 @@ public class TestSystemFunctionPushDownInRowLevelOperations extends ExtensionsTe

private List<Expression> executeAndCollectFunctionCalls(String query, Object... args) {
CommandResultExec command = (CommandResultExec) executeAndKeepPlan(query, args);
- V2TableWriteExec write = (V2TableWriteExec) command.commandPhysicalPlan();
+ // Comet's split-operator Iceberg write plans the command as IcebergCommitExec, which is not
+ // a V2TableWriteExec; collect over the whole command plan in that case.
+ SparkPlan plan = command.commandPhysicalPlan();
+ SparkPlan queryPlan =
+ plan instanceof V2TableWriteExec ? ((V2TableWriteExec) plan).query() : plan;
return SparkPlanUtil.collectExprs(
- write.query(),
+ queryPlan,
expr -> expr instanceof StaticInvoke || expr instanceof ApplyFunctionExpression);
}

28 changes: 28 additions & 0 deletions dev/diffs/iceberg/1.11.0.diff
Original file line number Diff line number Diff line change
Expand Up @@ -784,3 +784,31 @@ index 8cdcf22e58..820f9fc03b 100644
.enableHiveSupport()
.getOrCreate();

diff --git a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
index 934220e5d..92132d4b8 100644
--- a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
+++ b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
@@ -40,6 +40,7 @@ import org.apache.spark.sql.catalyst.expressions.ApplyFunctionExpression;
import org.apache.spark.sql.catalyst.expressions.Expression;
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke;
import org.apache.spark.sql.execution.CommandResultExec;
+import org.apache.spark.sql.execution.SparkPlan;
import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -330,9 +331,13 @@ public class TestSystemFunctionPushDownInRowLevelOperations extends ExtensionsTe

private List<Expression> executeAndCollectFunctionCalls(String query, Object... args) {
CommandResultExec command = (CommandResultExec) executeAndKeepPlan(query, args);
- V2TableWriteExec write = (V2TableWriteExec) command.commandPhysicalPlan();
+ // Comet's split-operator Iceberg write plans the command as IcebergCommitExec, which is not
+ // a V2TableWriteExec; collect over the whole command plan in that case.
+ SparkPlan plan = command.commandPhysicalPlan();
+ SparkPlan queryPlan =
+ plan instanceof V2TableWriteExec ? ((V2TableWriteExec) plan).query() : plan;
return SparkPlanUtil.collectExprs(
- write.query(),
+ queryPlan,
expr -> expr instanceof StaticInvoke || expr instanceof ApplyFunctionExpression);
}

56 changes: 56 additions & 0 deletions dev/diffs/iceberg/1.8.1.diff
Original file line number Diff line number Diff line change
Expand Up @@ -3261,3 +3261,59 @@ index 6719c45ca9..2515454401 100644
sourceColumnName,
tableName,
tableName(OTHER_TABLE_NAME),
diff --git a/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java b/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
index 49119d319..04fd3d485 100644
--- a/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
+++ b/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
@@ -37,6 +37,7 @@ import org.apache.spark.sql.catalyst.expressions.ApplyFunctionExpression;
import org.apache.spark.sql.catalyst.expressions.Expression;
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke;
import org.apache.spark.sql.execution.CommandResultExec;
+import org.apache.spark.sql.execution.SparkPlan;
import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec;
import org.junit.After;
import org.junit.Before;
@@ -316,9 +317,13 @@ public class TestSystemFunctionPushDownInRowLevelOperations extends SparkExtensi

private List<Expression> executeAndCollectFunctionCalls(String query, Object... args) {
CommandResultExec command = (CommandResultExec) executeAndKeepPlan(query, args);
- V2TableWriteExec write = (V2TableWriteExec) command.commandPhysicalPlan();
+ // Comet's split-operator Iceberg write plans the command as IcebergCommitExec, which is not
+ // a V2TableWriteExec; collect over the whole command plan in that case.
+ SparkPlan plan = command.commandPhysicalPlan();
+ SparkPlan queryPlan =
+ plan instanceof V2TableWriteExec ? ((V2TableWriteExec) plan).query() : plan;
return SparkPlanUtil.collectExprs(
- write.query(),
+ queryPlan,
expr -> expr instanceof StaticInvoke || expr instanceof ApplyFunctionExpression);
}

diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
index 8cb92224f..777b9bb1d 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
@@ -38,6 +38,7 @@ import org.apache.spark.sql.catalyst.expressions.ApplyFunctionExpression;
import org.apache.spark.sql.catalyst.expressions.Expression;
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke;
import org.apache.spark.sql.execution.CommandResultExec;
+import org.apache.spark.sql.execution.SparkPlan;
import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -313,9 +314,13 @@ public class TestSystemFunctionPushDownInRowLevelOperations extends ExtensionsTe

private List<Expression> executeAndCollectFunctionCalls(String query, Object... args) {
CommandResultExec command = (CommandResultExec) executeAndKeepPlan(query, args);
- V2TableWriteExec write = (V2TableWriteExec) command.commandPhysicalPlan();
+ // Comet's split-operator Iceberg write plans the command as IcebergCommitExec, which is not
+ // a V2TableWriteExec; collect over the whole command plan in that case.
+ SparkPlan plan = command.commandPhysicalPlan();
+ SparkPlan queryPlan =
+ plan instanceof V2TableWriteExec ? ((V2TableWriteExec) plan).query() : plan;
return SparkPlanUtil.collectExprs(
- write.query(),
+ queryPlan,
expr -> expr instanceof StaticInvoke || expr instanceof ApplyFunctionExpression);
}

56 changes: 56 additions & 0 deletions dev/diffs/iceberg/1.9.1.diff
Original file line number Diff line number Diff line change
Expand Up @@ -3219,3 +3219,59 @@ index 6719c45ca9..2515454401 100644
sourceColumnName,
tableName,
tableName(OTHER_TABLE_NAME),
diff --git a/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java b/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
index 5a0a04edb..afe92a8b7 100644
--- a/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
+++ b/spark/v3.4/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
@@ -38,6 +38,7 @@ import org.apache.spark.sql.catalyst.expressions.ApplyFunctionExpression;
import org.apache.spark.sql.catalyst.expressions.Expression;
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke;
import org.apache.spark.sql.execution.CommandResultExec;
+import org.apache.spark.sql.execution.SparkPlan;
import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -313,9 +314,13 @@ public class TestSystemFunctionPushDownInRowLevelOperations extends ExtensionsTe

private List<Expression> executeAndCollectFunctionCalls(String query, Object... args) {
CommandResultExec command = (CommandResultExec) executeAndKeepPlan(query, args);
- V2TableWriteExec write = (V2TableWriteExec) command.commandPhysicalPlan();
+ // Comet's split-operator Iceberg write plans the command as IcebergCommitExec, which is not
+ // a V2TableWriteExec; collect over the whole command plan in that case.
+ SparkPlan plan = command.commandPhysicalPlan();
+ SparkPlan queryPlan =
+ plan instanceof V2TableWriteExec ? ((V2TableWriteExec) plan).query() : plan;
return SparkPlanUtil.collectExprs(
- write.query(),
+ queryPlan,
expr -> expr instanceof StaticInvoke || expr instanceof ApplyFunctionExpression);
}

diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
index 8cb92224f..777b9bb1d 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSystemFunctionPushDownInRowLevelOperations.java
@@ -38,6 +38,7 @@ import org.apache.spark.sql.catalyst.expressions.ApplyFunctionExpression;
import org.apache.spark.sql.catalyst.expressions.Expression;
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke;
import org.apache.spark.sql.execution.CommandResultExec;
+import org.apache.spark.sql.execution.SparkPlan;
import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -313,9 +314,13 @@ public class TestSystemFunctionPushDownInRowLevelOperations extends ExtensionsTe

private List<Expression> executeAndCollectFunctionCalls(String query, Object... args) {
CommandResultExec command = (CommandResultExec) executeAndKeepPlan(query, args);
- V2TableWriteExec write = (V2TableWriteExec) command.commandPhysicalPlan();
+ // Comet's split-operator Iceberg write plans the command as IcebergCommitExec, which is not
+ // a V2TableWriteExec; collect over the whole command plan in that case.
+ SparkPlan plan = command.commandPhysicalPlan();
+ SparkPlan queryPlan =
+ plan instanceof V2TableWriteExec ? ((V2TableWriteExec) plan).query() : plan;
return SparkPlanUtil.collectExprs(
- write.query(),
+ queryPlan,
expr -> expr instanceof StaticInvoke || expr instanceof ApplyFunctionExpression);
}

97 changes: 97 additions & 0 deletions docs/source/user-guide/latest/iceberg-writes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<!---
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.
-->

# Iceberg Writes: Comet's Split-Operator Plan (Experimental)

**This feature is experimental and enabled by default.** Set
`spark.comet.write.iceberg.splitOperator.enabled=false` to restore Spark's stock combined
write operator.

## Overview

Spark writes an Iceberg table through a single physical operator that combines data-file
writing with metadata writing, committing, and catalog validation. Because that operator sits
outside Spark's Adaptive Query Execution (AQE), the sub-query feeding the write — the scans,
projects, sorts, and exchanges producing the rows — cannot be re-planned at runtime.

When `spark.comet.write.iceberg.splitOperator.enabled=true`, Comet rewrites eligible Iceberg
writes into two operators:

1. **`IcebergWrite`** — writes the data files on the executors, exactly as iceberg-java does
today, and returns each task's serialized commit message. This operator and the sub-query
feeding it run inside AQE.
2. **`IcebergCommit`** — collects the commit messages on the driver and performs the normal
Iceberg commit (including commit-time validation), outside AQE, exactly once.

Data files are still written by iceberg-java; only the plan shape changes. The split makes the
write's input visible to AQE and to Comet's columnar rules, and it is the groundwork for a
planned follow-up in which Comet writes the data files natively via
[iceberg-rust](https://github.com/apache/iceberg-rust).

## Configuration

Standard Comet + Iceberg setup (see [`iceberg.md`](iceberg.md)) is all that is required; the
split-operator plan is applied automatically. To turn it off:

```
# Standard Comet / Iceberg wiring
spark.plugins=org.apache.spark.CometPlugin
spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
spark.sql.catalog.<name>=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.<name>.type=hadoop # or hive / glue / rest / ...
spark.sql.catalog.<name>.warehouse=...

# Split-operator plan (experimental, on by default); set to false to opt out
spark.comet.write.iceberg.splitOperator.enabled=false
```

## Supported operations

The split-operator plan supports the following operations on every Spark version Comet
supports:

Comment on lines +64 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we revisit the "identical coverage on every Spark version" claim? The copy-on-write ReplaceData path looks like it differs by version: 4.0+ uses operation-coded rows with projections, while 3.4/3.5 use a plain row stream. Would it be more accurate to state that the row-level DML mechanism differs by Spark version so the docs match the shim behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded -- the docs now say the supported operations are the same on every version but the row-level DML mechanism differs (4.0+ operation-coded rows with projections, 3.4/3.5 a plain row stream).

- `INSERT INTO` / DataFrame `append` (`AppendData`)
- `INSERT OVERWRITE`, static and dynamic (`OverwriteByExpression`,
`OverwritePartitionsDynamic`)
- Copy-on-write `DELETE` / `UPDATE` / `MERGE` (`ReplaceData`)

The mechanism behind row-level DML differs by Spark version: on Spark 4.0+ the analyzer emits
operation-coded rows that Comet's writer dispatches through `ReplaceData`'s projections, while
on Spark 3.4/3.5 the rewritten rows are written as a plain row stream. The supported set of
operations is the same either way.

On Spark 4.1+ the split plan matches two further stock-Spark behaviours: MERGE metrics are
forwarded to the writer's commit (Iceberg 1.11+ records them in the snapshot summary), and
cached catalog tables are recached by name after a write so cache entries survive schema
changes.

## When Comet falls back to Spark's write operator

The rewrite is skipped — and the write runs through Spark's stock combined operator — when:

- `spark.comet.write.iceberg.splitOperator.enabled` is `false`;
- the write is not an Iceberg `SparkWrite` (any other V2 data source);
- the table uses merge-on-read: delta writes (Iceberg `WriteDelta`) are not intercepted;
- the write requires Spark's commit coordinator, which Comet's per-task commit protocol does
not use;
- Comet cannot reflect the Iceberg internals needed to build the two-operator plan (for
example an unrecognised write class or a `ReplaceData` projection it cannot map).

In every fallback case the write is planned as if Comet were absent; there is no correctness
trade-off, only no plan change.
1 change: 1 addition & 0 deletions docs/source/user-guide/latest/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ to read more.
:hidden:

Iceberg Guide <iceberg>
Iceberg Writes <iceberg-writes>
S3 Credential Providers <s3-credential-providers>
Kubernetes Guide <kubernetes>

Expand Down
Loading
Loading