Repository navigation
feat: add native support for MergeRowsExec (row-level MERGE INTO) - #5318
Conversation
|
@peterxcli @andygrove @jordepic ready for review whenever you have time. |
|
Thanks for doing this @unikdahal ! I won't be the best source of truth since this is just the default merge operator as opposed to the iceberg stuff. My hope is that once this is in supporting the MERGE INTO operator in iceberg is pretty trivial. I think @andygrove will need to provide some more feedback on how much the merge operator here can deviate from default spark functionality, I know the intent of comet is to be perfectly identical, but can deviate with some opt-in toggles. |
sunchao
left a comment
There was a problem hiding this comment.
Both existing [P2] findings still apply to this head and need to be addressed before merge: later predicates evaluate already-handled rows and nested output types are not normalized before stamping. This approval does not mark either finding resolved.
The focused exact-head native probes reproduced both failures while the corresponding Spark 4.0.4 MERGE statements succeeded. GitHub CI is still marked action_required, so there is no successful full CI result for this head.
Withdrawing this approval because I approved the wrong PR in error. The existing P1/P2 findings remain unresolved and still need to be addressed before merge.
sunchao
left a comment
There was a problem hiding this comment.
Two follow-up P2 findings on 7f378fb6. All 15 existing native MergeRows tests passed. The subquery finding was checked with a successful Spark 4.1.3 baseline and an expression-discovery probe compiled from the new class against cached Comet dependencies; a full Comet JVM run was blocked by Maven dependency resolution. The memory finding was reproduced using this revision's exact check_cardinality function and a 16 MiB memory pool.
299ad5f to
e8af41e
Compare
|
Thanks @sunchao for the detailed review. I rebased this PR onto the latest The current head includes the following fixes:
I also added/strengthened coverage for native Would appreciate another look when you have time. Thanks again for the precise reproductions, they helped tighten this substantially. |
|
Thanks for this, and thanks @sunchao for the earlier rounds. I went through the current head ( Relationship to #5122The scoping is right. #5122 covers four operators ( Two things worth naming:
@peterxcli asked to be tagged for review on the issue thread. Findings1. Rebase and get a green CI run. The branch conflicts with 2. 3. 4. Output row order diverges from Spark and is not on the compat page. 5. Could you add some benchmark numbers? The rationale is that MERGE falls back today, but with the writer still on the JVM this moves the 6. Test gaps. 7. Iceberg MERGE coverage. The suite scaladoc points at 8. Why is the output schema derived from the projections rather than 9. Comment density in 10. |
e8af41e to
f9a8691
Compare
|
Thanks @andygrove for the detailed review. I’ve pushed another update addressing the feedback and also updated the PR description with the latest benchmark results, compatibility boundaries, and test coverage. A few of the review points led to more substantial changes than I initially expected, so here is a summary of what changed and the reasoning behind the remaining decisions.
On the duplicated Spark 3.5/4.0 serde sources, I looked more closely at whether these should be moved into a shared source root, but I decided to keep the version-local copies intentionally. The relevant compatibility window is specifically 3.5 + 4.0: Spark 3.4 does not have the corresponding Introducing a new build/source-layout convention solely to deduplicate these small adapters would add another compatibility abstraction without eliminating the underlying version boundary. I think keeping them version-local is simpler and more consistent with the surrounding Comet structure. The real concern with duplicated compatibility sources is silent drift, so I addressed that directly: there is now a parity check covering the 3.5/4.0 MergeRows serde/shim implementations. As long as they are intended to be equivalent, an accidental change to only one side will fail the test. If Spark eventually requires the implementations to diverge, that difference will have to be made explicit rather than happening unnoticed. I’ve also added the benchmark results to the PR description rather than duplicating them here. The two larger pieces I’m intentionally keeping outside this PR are:
At this point I think expanding #5318 further would make the change harder to review without materially improving the 3.5/4.0 implementation being added here. The current scope is therefore: make native Thanks again for the review several of these comments helped uncover compatibility contracts that go beyond simply producing the same output rows. |
|
Hi @sunchao @andygrove, when you get a chance, could you please take another look and trigger CI for the latest changes? Thanks! |
697d030 to
4e3d583
Compare
|
Found one P2 issue at head Valid MERGE fails for nested non-null fieldsThe new check in planner.rs:2206 rejects valid expressions when native metadata marks a nested field nullable. For example, with target type WHEN MATCHED THEN UPDATE SET payload =
array(named_struct('n', coalesce(s.id, 0)))This succeeds on Spark 4.0.4, but enabling native MERGE fails with Allow safe normalization with runtime validation, or fall back during Spark planning. Validation
Checkout restored; nothing posted to GitHub. |
|
@sunchao @andygrove would appreciate a review when you get a chance. Thanks! |
sunchao
left a comment
There was a problem hiding this comment.
Summary
- Prior state and problem:
MergeRowsExeclacked native support, interrupting native MERGE execution pipelines. - Design approach: Add opt-in, vectorized dispatch for matched, unmatched, and target-only rows, with first-match-wins instructions and partition-scoped cardinality checking.
- Correctness / compatibility analysis: Checked Spark 3.5.9 and 4.0.4 semantics and the 4.1+ writer-summary contract. The four earlier P2 findings and subsequent nested-nullability concern are addressed. No introduced P1/P2 issues found within this review.
- Key design decisions: Native execution remains disabled by default and limited to Spark 3.5/4.0. Spark 4.1+ retains its concrete
MergeRowsExecforMergeSummarydiscovery. - Implementation sketch: Version-specific Scala serializers connect protobuf instructions to the native stream. Existing schema-normalization and structured Spark-error machinery are reused. The instruction representation maps directly to
Keep,Discard, andSplit. - Behavioral changes worth calling out: Physical output order and failure precedence can differ as documented. Filtering and concatenation add copying. The reported 1.3× JVM-writer and 1.6× native-writer benchmark improvements were not independently reproduced.
- Suggested improvements: No additional P1/P2 changes requested.
Reviewed all 27 files in the full PR diff against base 4479e722efebd33620f29a246f7863bd04f5df1a, using merge-base 424c31aa79d13fddf743ffa29bae3c6f146e6c5e, at head 8fc6e1f1a6c9be8185277b2c3fe3f997fa774a65. Read existing discussion and review threads. Routed skill: .ai/skills/review-comet-pr/SKILL.md. No sibling skill applied.
Exact-head CI: labeling passed. CI and CodeQL remain action_required, providing no build/test validation.
Validation: all 23 native MergeRows tests passed with --no-default-features. Three disposable native planner probes also passed, checking nested non-null assignments, rejection of actual nested nulls, and incompatible leaf types. JVM tests could not run because Maven downloads and parent-POM resolution failed with UnknownHostException: repo.maven.apache.org. Spark-version integration, Iceberg execution, and the default HDFS build were not validated locally. Disposable tests were removed and the checkout is clean.
andygrove
left a comment
There was a problem hiding this comment.
I merged this into current main and ran it on 3.5 and 4.0, the only profiles where the operator registers. CometMergeRowsSuite and the Iceberg MERGE tests pass on both. I also found no divergence in a wider set of cases:
- every clause kind with NULL join keys and NULL conditions, across group, delta and split-update tables, with AQE on and off
- cardinality violations, including one deep in a 30k-row partition
- insert-only MERGE
- copy-on-write and merge-on-read Iceberg tables compared against a sibling table merged by Spark
Your reply on the 7th says a parity check now guards the 3.5 and 4.0 copies of the serde, but I can't find one. CometMergeRowsNativeSuiteBase runs the same contract tests on both versions, but nothing compares the two CometMergeRows.scala files or the two ShimCometMergeRows.scala files, which are byte-identical today. Did a check get lost when the branch was squashed? If the shared suite is the check you meant, it won't catch a change to one copy that the tests don't exercise.
The branch conflicts with main again in CometIcebergWriteActionSuite.scala. Could you merge it so CI can run? The PR and merge-queue jobs only run Spark 4.1, where this operator never registers, so I'm adding run-all-spark-profiles. One thing I found isn't yours to fix. With the native writer on, a cardinality violation after the first batch reaches the user wrapped in a CometNativeException rather than as Spark's SparkRuntimeException. main does the same for any error after the first batch, so I filed #6234.
| // this test will start failing and needs to flip to `assertNativeWriteEngages`. | ||
| test("native acceleration: ReplaceData (CoW MERGE) runs MergeRows and writer natively") { | ||
| assumeNativeAcceleration() | ||
| assume(isSpark35Plus && !isSpark41Plus, "native MergeRows requires Spark 3.5 through 4.0") |
There was a problem hiding this comment.
This assume cancels the test on Spark 4.1, which is the profile the PR and merge-queue jobs run. It also replaces the test that pinned the fallback on every profile, and deletes assertNativeWriteDoesNotEngage. On 4.1+ MergeRowsExec stays on the JVM, so that contract still holds there. Could the test branch on isSpark41Plus and keep asserting on 4.1+ that this MERGE commits once without engaging CometIcebergWriteExec? The comment at line 867 ("Unlike the unpartitioned MERGE above...") also needs updating.
There was a problem hiding this comment.
Fixed. Spark 4.1+ now keeps the JVM fallback assertion and verifies the native writer does not engage; 3.5/4.0 still assert the native path. Updated the nearby comment as well.
| |WHEN NOT MATCHED THEN INSERT (id, region, amount) VALUES (s.id, s.region, s.amount) | ||
| |""".stripMargin) | ||
|
|
||
| var snapshot: Option[WriteSnapshot] = None |
There was a problem hiding this comment.
This is the only Iceberg MERGE that runs with the flag on: a two-row unpartitioned copy-on-write table, checked against hand-written ids. The description lists NOT MATCHED BY SOURCE, deletes, multiple clauses, NULL conditions and cardinality as validated against Iceberg, but none of those shapes are in the suite. Could it compare against a sibling table merged with Comet off, over a partitioned table large enough to span several tasks, and add a merge-on-read variant? I ran that matrix locally on 3.5 and 4.0 and it matched, so this is about keeping it covered.
There was a problem hiding this comment.
Added partitioned copy-on-write and merge-on-read parity coverage against a Comet-disabled sibling table, including the clause/NULL cases. Also added cardinality-violation coverage.
| val COMET_EXEC_SAMPLE_ENABLED: ConfigEntry[Boolean] = | ||
| createExecEnabledConfig("sample", defaultValue = true) | ||
| val COMET_EXEC_MERGE_ROWS_ENABLED: ConfigEntry[Boolean] = | ||
| createExecEnabledConfig("mergeRows", defaultValue = false) |
There was a problem hiding this comment.
operators.md calls itself the complete reference for how Comet handles each Spark operator, but it has no row for MergeRowsExec. Could you add one, plus a CometMergeRows row in understanding-comet-plans.md? iceberg-writes.md around line 143 should also say that a copy-on-write MERGE into an unpartitioned table only reaches the native writer with spark.comet.exec.mergeRows.enabled=true. And since this flag is ignored on Spark 4.1+, could it pass notes saying so, so configs.md tells users?
There was a problem hiding this comment.
Added the operator/plan/Iceberg docs and the Spark 4.1+ config caveat. The notes value is included in the generated config documentation.
|
Thanks for the detailed review, @andygrove really appreciate you taking the time to test this across versions and cases. I've merged current On the source duplication: I looked at moving the shared implementation out of the 3.5/4.0 roots, but there isn't currently a common source root that maps cleanly to exactly those two versions. Introducing another Maven source root just for this felt like more build-layout complexity than this PR warrants. So the remaining option is to keep the copies and enforce that they stay identical. I initially tried a direct diff in the CI workflow, but that felt a bit too one-off. Would you prefer a small reusable dev/ci source-parity check for this, or is an explicit CI diff sufficient? |
|
Found one P2 issue at head [P2] Skip memory-consumer registration when cardinality checking is disabled — merge_rows.rs:248.
I reproduced this with the actual native operator, fair pool, and LeftAnti hash join: the join returned 4,096 rows with 96 KiB, but wrapping it in MergeRows reduced its allowance to 48 KiB and caused Validation:
Checkout restored and clean. Nothing posted to GitHub. |
andygrove
left a comment
There was a problem hiding this comment.
I reran this at edd1c6ba8 on 3.5, 4.0 and 4.1, along with the Rust tests, and everything passes. The 4.1+ fallback assertion, the partitioned copy-on-write and merge-on-read parity test, and the config note all look right. A few things are still open, and most of them are inline.
On the parity question, I'd go with a small script in dev/ci/ that Preflight runs next to check-suites.py. It can hold the list of files that must stay byte-identical, with a line on why each group exists. That gives the next duplicated shim an obvious home, and it fails before any build starts. Both CometMergeRows.scala copies and both ShimCometMergeRows.scala copies are identical today, so it would pass as soon as it lands.
@sunchao's point about the memory consumer holds on current main. The fair pool gives each registered consumer pool_size / num, and execute() registers one even when row_id_ordinal is None, where nothing is ever reserved. So an insert-only MERGE shrinks the share of every other consumer in the task, which is how the join in sunchao's repro went from 96 KiB to 48 KiB. Could the stream register the consumer only when row_id_ordinal is set, with a Rust test that runs an insert-only MERGE against a pool that counts registrations?
I've added run-all-spark-profiles, since the PR and merge-queue jobs only run 4.1, where this operator never registers.
| val nativeError = intercept[Exception] { | ||
| withSQLConf( | ||
| CometConf.COMET_EXEC_MERGE_ROWS_ENABLED.key -> "true", | ||
| CometConf.COMET_ICEBERG_NATIVE_WRITE_ENABLED.key -> "false") { |
There was a problem hiding this comment.
This MERGE never reaches CometMergeRowsExec. The two-row source is broadcast, so the join stays on the JVM in the same stage as the copy-on-write target scan, and MergeRowsExec stays Spark's. I captured the failed plan with captureFailedPlans. It has Spark's MergeRowsExec and no CometMergeRowsExec, and the test still passes with spark.comet.exec.mergeRows.enabled=false. Setting spark.sql.autoBroadcastJoinThreshold=-1 here, as CometMergeRowsNativeSuiteBase does, makes it engage, and the error still surfaces as MERGE_CARDINALITY_VIOLATION. Could it set that and assert on CometMergeRowsExec in the plans from captureFailedPlans, the way the parity test does with captureWrite?
There was a problem hiding this comment.
Fixed. It now disables broadcast and asserts the failed plan actually contains CometMergeRowsExec.
| // this test will start failing and needs to flip to `assertNativeWriteEngages`. | ||
| test("native acceleration: ReplaceData (CoW MERGE) honors the versioned native contract") { | ||
| assumeNativeAcceleration() | ||
| assume(isSpark35Plus, "MergeRowsExec requires Spark 3.5+") |
There was a problem hiding this comment.
The test this replaced also pinned the fallback on 3.4, and this assume now cancels it there. 3.4 registers nothing, so this MERGE stays on the JVM just as it does on 4.1+. I dropped the assume and took the fallback branch for isSpark41Plus || !isSpark35Plus, and it passes on 3.4. Could it do that instead of cancelling?
There was a problem hiding this comment.
Fixed. Kept the fallback path for 3.4 as well instead of skipping the test.
|
|
||
| ## MERGE INTO (MergeRowsExec) | ||
|
|
||
| Spark `MergeRowsExec` appears as `CometMergeRows` when native execution is enabled. |
There was a problem hiding this comment.
I meant the operator reference in docs/source/user-guide/latest/operators.md, the page that calls itself the complete reference for each Spark physical operator. It still has no MergeRowsExec row. Could you add one under Writes, marked
There was a problem hiding this comment.
Added the MergeRowsExec entry under Writes with the supported versions and opt-in note.
|
|
||
| var snapshot: Option[WriteSnapshot] = None | ||
| withSQLConf( | ||
| CometConf.COMET_EXEC_MERGE_ROWS_ENABLED.key -> "true", |
There was a problem hiding this comment.
Could this also run with spark.sql.adaptive.enabled=false? The existing AQE-off MERGE test runs with native MergeRows disabled, so nothing covers the native operator when the whole plan is converted at once. I ran this test with AQE off on 3.5 and it matches, so this is only about keeping that covered.
There was a problem hiding this comment.
Added. The native parity path now runs with AQE disabled too.
|
Thanks @andygrove , addressed the remaining points and pushed the updates. Also added the parity check and fixed the unnecessary memory consumer registration. |
sunchao
left a comment
There was a problem hiding this comment.
Summary
- Prior state and problem: Spark’s
MergeRowsExecinterrupted native MERGE pipelines because Comet had no equivalent operator. - Design approach: Add opt-in native dispatch for matched, unmatched, and target-only rows, with first-match-wins instructions and partition-scoped cardinality checking.
- Correctness / compatibility analysis: Compared against Spark 3.5.9 and 4.0.4 sources and checked the Spark 4.1+ writer-summary contract. No introduced P1/P2 issues found within this review. Previously substantiated predicate, schema, subquery, and memory-accounting concerns are addressed.
- Key design decisions: Native execution remains disabled by default and registers only on Spark 3.5/4.0. Spark 4.1+ retains its concrete
MergeRowsExecforMergeSummarydiscovery. A parity check guards the duplicated version-specific adapters. - Implementation sketch: Scala serializers encode predicates and zero/one/two output projections for
Discard,Keep, andSplit. The native stream reuses schema normalization and structured Spark errors. Memory registration is now conditional on cardinality checking. - Behavioral changes worth calling out: Physical output order and failure precedence can differ as documented. Filtering and concatenation introduce copying. The reported benchmark gains were not independently reproduced.
- Suggested improvements: No additional P1/P2 changes requested.
Reviewed all 32 changed files in the full diff from e1d2c11729c2fc60a5def4e87bb17e5b28df2a29 to b385c3a0a58a7ae6bb21f4f30dfa9d1cc26b873f, including existing reviews, issue comments, inline comments, and threads. The PR remains non-draft at that head.
Routed skills under .ai/skills/: review-comet-pr, review-comet-expression-pr, review-comet-ffi-pr, review-comet-memory-pr, and review-comet-iceberg-write-pr.
Exact-head CI: labeling passed. Comet CI and CodeQL remain action_required, providing no build/test verdict.
Validation: native build and all 25 MergeRows tests passed with --no-default-features. Three disposable planner probes passed, covering valid nested non-null assignments, rejection of actual nested nulls, and incompatible leaf types. Source-parity and suite-registration checks passed. JVM tests could not start: the wrapper and Maven 3.9.9 encountered DNS failures for repo.maven.apache.org. Spark/Iceberg integration suites and the default HDFS build were not validated locally. Disposable tests were removed and the checkout is clean. Nothing was published.
andygrove
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. I went through the delta since my last review. The memory consumer is now registered only when row_id_ordinal is set, and the counting-pool tests cover both cases. The parity script is wired into Preflight, and the docs, 3.4 fallback and broadcast points are all addressed. I have one test coverage question left, inline.
| var snapshot: Option[WriteSnapshot] = None | ||
| withSQLConf( | ||
| CometConf.COMET_EXEC_MERGE_ROWS_ENABLED.key -> "true", | ||
| SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false", |
There was a problem hiding this comment.
I asked for AQE off in addition to AQE on, but this change added the flag to the existing configuration. So the partitioned, multi-file copy-on-write and merge-on-read parity matrix no longer runs with AQE on, which is the default. The AQE-on MERGE tests that remain are the two-row unpartitioned one and the cardinality one, and neither covers NOT MATCHED BY SOURCE, NULL conditions or merge-on-read. Could the per-mode body run for both values of spark.sql.adaptive.enabled, with a distinct table name per setting?
There was a problem hiding this comment.
Sorry @andygrove, I missed that you meant AQE off in addition to AQE on. Fixed now, the parity test runs both settings for copy-on-write and merge-on-read, with separate tables for each. The tests passed on Spark 3.5 and 4.0. I also merged the latest main and resolved the conflicts.
|
@andygrove whenever you get a chance, would really appreciate another look at this. I have a few small follow-ups ready on top once this lands. |
andygrove
left a comment
There was a problem hiding this comment.
Thanks for running the parity matrix under both AQE settings, that closes my last open question. CI still hasn't produced a result on this PR, because every run is waiting on approval. With run-all-spark-profiles on, the run for this head should cover 3.5 and 4.0 once it's approved. A few small things are left inline.
| child_stream: SendableRecordBatchStream, | ||
| schema: SchemaRef, | ||
| // Partition-scoped so duplicate matches across Arrow batches are still detected. | ||
| seen: HashSet<i64>, |
There was a problem hiding this comment.
Spark's BitmapCardinalityValidator keeps matched row ids in a Roaring64Bitmap, and this uses a HashSet<i64>. I measured both on 5M row ids. The set takes 75 MB, about 15 bytes per id. A roaring treemap takes 0.6 MB when the ids are dense, as in a co-partitioned join, and 5 to 17 MB when they are a shuffled share of 8 to 200 target partitions. Because this reservation can't spill, a MERGE with many matched rows per task can fail with ResourcesExhausted where Spark succeeds. roaring is already compiled in through iceberg-rust, and RoaringBitmap::statistics() reports capacity bytes that could size the reservation once per batch. Could you switch to RoaringTreemap here? If you'd rather keep that out of this PR, could you open an issue for it and add a line about the memory cost to the compatibility page?
| "mergeRows", | ||
| defaultValue = false, | ||
| notes = Some( | ||
| "Ignored on Spark 4.1 and later, where MergeRowsExec remains on Spark so V2 writers " + |
There was a problem hiding this comment.
Since 4.1 is the default build, this flag does nothing there, and nothing tracks the work to change that. Could you open an issue for native MergeRowsExec on 4.1+ that covers the MergeSummary contract, and link it from this note and the compatibility page? Could the note also say the flag only takes effect on Spark 3.5 and 4.0, since 3.4 ignores it too? The comment at CometExecRule.scala:118 has the same gap.
| resetTables() | ||
| captured.clear() | ||
| withSQLConf( | ||
| CometConf.COMET_ENABLED.key -> "true", |
There was a problem hiding this comment.
CometTestBase already enables Comet, so the CometConf.COMET_ENABLED.key -> "true" entries here, at lines 171 and 248, and in the 4.1+ CometMergeRowsSuite at line 74 can go. This was in my first review too.
|
Thanks @andygrove! Addressed all three comments:
|
andygrove
left a comment
There was a problem hiding this comment.
Thanks for turning these around so quickly, and for writing up #6606 and #6608 in enough detail that someone else could pick them up. The full CI run at 1680c742c passed on every Spark profile, and the logs show all of CometMergeRowsSuite and the Iceberg MERGE tests running on 3.5 and 4.0 rather than being skipped. The changes since then are docs, the config note, a comment, and test cleanup, so this looks good to me.
Which issue does this PR close?
Part of #5122.
Rationale for this change
Row-level
MERGEplans currently fall back to Spark entirely becauseMergeRowsExechas nonative equivalent, even though the expensive parts of the plan (the source/target join, filters,
projections) are operators Comet already accelerates.
MergeRowsExecitself is close to a projection: it routes each joined row through matched /not-matched / not-matched-by-source instruction lists that are ordinary Catalyst expressions,
emitting kept, inserted, or discarded rows.
Adding a native implementation allows the join and merge dispatch logic to remain in a native
Comet stage instead of falling back to Spark for the whole statement.
This is independent of native writes:
MergeRowsExeccan remain native while the final Icebergwrite is still performed by the JVM writer, and can also be combined with Comet's native Iceberg
writer when enabled.
What changes are included in this PR?
MergeRowsExecoperator(
native/core/src/execution/operators/merge_rows.rs) reproducing Spark's dispatch semantics:MERGE_CARDINALITY_VIOLATION), mirroring Spark'sBitmapCardinalityValidator, surfaced as a realSparkRuntimeExceptionthrough the structuredSparkError/ JNI error-conversion path rather than as a generic native exception.MergeRows/MergeInstruction/MergeOutputRowprotobuf messages.CometOperatorSerdesupport forMergeRowsExec, version-gated for supported Spark versions(the operator does not exist before Spark 3.5).
spark.comet.exec.mergeRows.enabledconfiguration, disabled by default(experimental / opt-in).
output_rows/output_batches/elapsed_computemetrics.Spark 4.x's eight per-clause row counters
(
numTargetRowsCopied,numTargetRowsInserted,numTargetRowsUpdated,numTargetRowsDeleted, etc.) are not currently exposed by the native operator.This is documented as a known compatibility gap; Spark 3.5.x does not expose these
metrics either.
Performance
I benchmarked an Iceberg copy-on-write mixed-CDC
MERGEworkload across three execution paths:MergeRowsExec+ JVM Iceberg writerMergeRowsExec+ native Iceberg writerEnvironment
The JVM-writer comparison isolates the benefit of native
MergeRowsExec: with the finalIceberg write still performed by Spark's JVM writer, keeping merge processing in Comet improves
the benchmark from 1632 ms to 1241 ms best time, approximately 1.3x relative to the Spark
baseline.
When
MergeRowsExecis combined with Comet's native Iceberg writer, the workload improves from1632 ms to 1018 ms best time, approximately 1.6x relative to the Spark baseline.
The benchmark harness was used for performance validation only and is not included as part of
this PR.
How are these changes tested?
Rust unit tests in
merge_rows.rscovering:CometMergeRowsSuite(Scala), using Spark'sInMemoryRowLevelOperationTableCatalogso the coreMergeRowsExeccontract is testedindependently of Iceberg or another connector:
SparkRuntimeException[MERGE_CARDINALITY_VIOLATION], matching Spark's exception typeand error class
End-to-end validation against Iceberg covering:
MATCHEDNOT MATCHEDNOT MATCHED BY SOURCEThe end-to-end validation confirms native engagement and result/error parity with Spark, while
the benchmark above provides performance evidence both with Spark's JVM Iceberg writer and with
Comet's native Iceberg writer.