Uh oh!
There was an error while loading. Please reload this page.
[SPARK-55953][SQL] Compute net changes in ResolveChangelogTable for batch CDC reads - #55583
[SPARK-55953][SQL] Compute net changes in ResolveChangelogTable for batch CDC reads#55583SanJSp wants to merge 4 commits into
Conversation
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| computeUpdates: Boolean): LogicalPlan = { | ||
| val windowedPlan = addNetChangesWindow(plan, cl) | ||
| val filteredAndRelabeledPlan = | ||
| removeIntermediateChangelogEntriesAndRelabelChangeTypes(windowedPlan, computeUpdates) |
There was a problem hiding this comment.
nit:
| removeIntermediateChangelogEntriesAndRelabelChangeTypes(windowedPlan, computeUpdates) | |
| removeIntermediateChanges(windowedPlan, computeUpdates) |
There was a problem hiding this comment.
I'd prefer to keep the longer name. It makes both responsibilities of the function explicit (filter + relabel)
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
gengliangwang
left a comment
There was a problem hiding this comment.
Net-change implementation looks correct and the per-cell tests are thorough. Two notes:
Dead code after the rejection path was removed:
cdcNetChangesNotYetSupportedatsql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryCompilationErrors.scala:3872-3876and the correspondingINVALID_CDC_OPTION.NET_CHANGES_NOT_YET_SUPPORTEDentry atcommon/utils/src/main/resources/error/error-conditions.json:3295-3299are now unreferenced — the only call site inevaluateRequirementsis gone and the two..._is_rejectedtests inResolveChangelogTablePostProcessingSuitewere deleted in this PR. Consider removing both in this PR (or as a quick follow-up) so the error catalog stays accurate.One inline comment below on test coverage of the combined post-processing pipeline.
| cat.setChangelogProperties(ident, ChangelogProperties( | ||
| containsIntermediateChanges = true, | ||
| containsCarryoverRows = false, | ||
| representsUpdateAsDeleteAndInsert = false, |
There was a problem hiding this comment.
The trait pins representsUpdateAsDeleteAndInsert = false, which keeps addRowLevelPostProcessing (update detection) out of the pipeline. As a result, the chained path where update detection's relabel produces update_preimage/update_postimage rows that then feed injectNetChangeComputation is not exercised end-to-end. Consider at least one variant with representsUpdateAsDeleteAndInsert = true so the integration of the two passes is covered.
There was a problem hiding this comment.
Done — see the new WithUpdateDetectionSuite (16 tests × representsUpdateAsDeleteAndInsert = true, computeUpdates = true).
dongjoon-hyun
left a comment
There was a problem hiding this comment.
Please hold on this CDC PR because the master branch was broken at CDC commit and we are recovering it.
@gengliangwang regarding your top level comment:
Feel free to give it another check, thanks in advance 🙏 |
As Gengliang has resolved the issues, this PR should no longer be blocked @dongjoon-hyun ? |
- ResolveChangelogTable: move the hoisted V2 rowId resolution inside the `if (req.requiresNetChanges)` branch. The hoist was needed to resolve against the bare DataSourceV2Relation (V2ExpressionUtils.resolveRefs doesn't work on the wrapped Project/Window plan), but it should not run unconditionally: connectors that report all of containsCarryoverRows / containsIntermediateChanges / representsUpdateAsDeleteAndInsert as false are allowed by the Changelog contract to inherit the default rowId() impl, which throws. Gating preserves the previous "only call when needed" behavior while keeping the V2-resolution fix. - pom.xml: revert to upstream/master. The diff was stale-rebase noise (maven 3.9.15->3.9.14, scala-maven-plugin 4.9.10->4.9.9, commons-codec 1.22.0->1.21.0, guava 33.6.0->33.5.0, etc.), unrelated to this PR.
5d71af6 to
312495bComparegengliangwang
commented
Apr 30, 2026
Tests passed in https://github.com/gengliangwang/spark/actions/runs/25135903704. |
### What changes were proposed in this pull request? This PR implements row-level CDC post-processing (carry-over removal and update detection) for DSv2 streaming reads. Previously, streaming `changes()` rejected any post-processing with a blanket `INVALID_CDC_OPTION.STREAMING_POST_PROCESSING_NOT_SUPPORTED` error. The batch path (added in #55508 and #55583) uses a Catalyst `Window` keyed by `(rowId, _commit_version)`, which `UnsupportedOperationChecker` rejects on streaming queries (`NON_TIME_WINDOW_NOT_SUPPORTED_IN_STREAMING`). The streaming rewrite in `ResolveChangelogTable` now expresses the same logic with streaming-allowed primitives: ``` EventTimeWatermark(_commit_timestamp, 0s) -> Aggregate keyed by (rowId..., _commit_version, _commit_timestamp) (count_if delete/insert, [min/max/count rowVersion,] collect_list(struct(*))) -> [Filter on the carry-over predicate] -> Generate(Inline(events)) -> [Project relabeling _change_type for delete+insert pairs] -> Project dropping __spark_cdc_* helpers ``` Including `_commit_timestamp` in the grouping keys is required to satisfy the Append-mode streaming aggregation contract (the watermark attribute must appear among the grouping expressions). By CDC convention all rows in a single commit share `_commit_timestamp`, so this is a no-op semantically relative to the batch `(rowId, _commit_version)` grouping. `deduplicationMode = netChanges` is still rejected -- net change computation partitions by `rowId` alone and reasons over the entire requested range, which is fundamentally cross-batch. The existing error class `INVALID_CDC_OPTION.STREAMING_POST_PROCESSING_NOT_SUPPORTED` is replaced with the more specific `INVALID_CDC_OPTION.STREAMING_NET_CHANGES_NOT_SUPPORTED`, which now names the offending option and points users at the supported streaming alternatives. Doc updates: - `Changelog.java` clarifies that all rows of a single `_commit_version` must share `_commit_timestamp`, and that streaming reads expect non-decreasing `_commit_timestamp` across micro-batches. - `Changelog.java` notes that `containsIntermediateChanges()` is range-scoped, hence the streaming limitation for `netChanges`. - `DataStreamReader.changes()` Scaladoc lists the `netChanges` streaming limitation. ### Why are the changes needed? Without this PR, any streaming CDC read against a connector that emits CoW carry-over pairs (`containsCarryoverRows = true`) or represents updates as raw delete+insert (`representsUpdateAsDeleteAndInsert = true`) raises an analysis error, forcing users to fall back to batch reads. The batch-only restriction is unnecessary for these passes -- they don't need cross-version state -- and it surprises users since the same options work on batch reads. ### Does this PR introduce _any_ user-facing change? Yes. - Streaming `spark.readStream.changes(...)` now supports `computeUpdates = true` and `deduplicationMode = dropCarryovers`. Previously these threw `INVALID_CDC_OPTION.STREAMING_POST_PROCESSING_NOT_SUPPORTED`. - The error class `INVALID_CDC_OPTION.STREAMING_POST_PROCESSING_NOT_SUPPORTED` is renamed to `INVALID_CDC_OPTION.STREAMING_NET_CHANGES_NOT_SUPPORTED` with a more specific message. The new error fires only for `deduplicationMode = netChanges` on streaming reads. - `DataStreamReader.changes()` Scaladoc is updated accordingly. - `Changelog.java` Scaladoc clarifies the `_commit_timestamp` contract for streaming. ### How was this patch tested? 86 tests across 4 CDC suites (all passing): - `ResolveChangelogTableStreamingPostProcessingSuite` (new, 5 tests) -- plan-shape assertions covering carry-over only, update detection only, both fused, and the no-rewrite pass-through cases. Verifies the `EventTimeWatermark` + `Aggregate` + `Generate(Inline)` rewrite shape. - `ChangelogResolutionSuite` -- the two existing streaming throw-tests are flipped to plan-shape assertions; a new test covers the `netChanges` streaming throw. - `ResolveChangelogTablePostProcessingSuite` -- the existing streaming throw test is updated to cover the `netChanges`-only case. - `ChangelogEndToEndSuite` -- three new streaming end-to-end tests using `InMemoryChangelogCatalog`: carry-over removal drops CoW pairs, update detection relabels delete+insert as update, and `netChanges` throws. Also confirmed `UnsupportedOperationsSuite` (216 tests) still passes -- the rewritten plan does not contain `Window` or any other streaming-rejected operator. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (claude-opus-4-7) Closes#55636 from gengliangwang/streamingCDC. Authored-by: Gengliang Wang <gengliang@apache.org> Signed-off-by: Gengliang Wang <gengliang@apache.org>
…reads tests ### What changes were proposed in this pull request? Add 5 tests verifying CACHE TABLE impact on reads for DSv2 tables. Write operations ignore the cache, so these tests verify how reads behave when a cached table is mutated by session SQL or external catalog API calls. Tests run in both classic and Connect modes. `DSv2CacheTableReadTests` extends `DSv2ExternalMutationTestBase`, following the same pattern as `DSv2TempViewWithStoredPlanTests` (PR #55571) and `DSv2RepeatedTableAccessTests` (PR #55583). The tests cover five scenarios from the design doc: - **Scenario 1 (external write)**: Cache pins the read, external write invisible until `REFRESH TABLE`. - **Scenario 2 (session write and more external changes)**: Session write rebuilds cache, subsequent external write invisible until `REFRESH TABLE`. - **Scenario 3 (external schema changes)**: Cache pinned at original schema, external ADD COLUMN invisible until `REFRESH TABLE`. - **Scenario 4 (session schema changes and more external changes)**: Session ALTER rebuilds cache with new schema, subsequent external write invisible until `REFRESH TABLE`. - **Scenario 5 (external drop and recreate table)**: Query sees the new empty table. #### New files - **`DSv2CacheTableReadTests`**: Shared trait containing all 5 tests, using `session.sql(...)` with `.collect()` calls (harmless in classic mode, required for Connect). #### Modified files - **`DataSourceV2DataFrameSuite`**: Mixes in `DSv2CacheTableReadTests` alongside existing traits (classic runner, `testPrefix = ""`). - **`DataSourceV2DataFrameConnectSuite`**: Mixes in `DSv2CacheTableReadTests` alongside existing traits (Connect runner, `testPrefix = "[connect] "`). - **`DSv2ExternalMutationTestBase`**: Updated Scaladoc to reference all three consumer traits. - **`DSv2TempViewWithStoredPlanTests`**: Removed stale comment about inherited vals. ### Why are the changes needed? These tests document and lock down the expected CACHE TABLE behavior with DSv2 tables: cached reads are pinned against external mutations, and `REFRESH TABLE` invalidates the cache. This prevents regressions if cache invalidation logic changes. ### Does this PR introduce _any_ user-facing change? No. This PR is test-only. ### How was this patch tested? 5 new tests run in both classic and Connect modes (10 total): Classic (175 total, all pass): ``` build/sbt 'sql/testOnly *DataSourceV2DataFrameSuite' ``` Connect (35 total, all pass): ``` build/sbt 'connect/testOnly *DataSourceV2DataFrameConnectSuite' ``` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (claude-opus-4-6) Closes#55536 from longvu-db/spark-dsv2-cache-scenario-5. Authored-by: Thang Long Vu <long.vu@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…reads tests ### What changes were proposed in this pull request? Add 5 tests verifying CACHE TABLE impact on reads for DSv2 tables. Write operations ignore the cache, so these tests verify how reads behave when a cached table is mutated by session SQL or external catalog API calls. Tests run in both classic and Connect modes. `DSv2CacheTableReadTests` extends `DSv2ExternalMutationTestBase`, following the same pattern as `DSv2TempViewWithStoredPlanTests` (PR #55571) and `DSv2RepeatedTableAccessTests` (PR #55583). The tests cover five scenarios from the design doc: - **Scenario 1 (external write)**: Cache pins the read, external write invisible until `REFRESH TABLE`. - **Scenario 2 (session write and more external changes)**: Session write rebuilds cache, subsequent external write invisible until `REFRESH TABLE`. - **Scenario 3 (external schema changes)**: Cache pinned at original schema, external ADD COLUMN invisible until `REFRESH TABLE`. - **Scenario 4 (session schema changes and more external changes)**: Session ALTER rebuilds cache with new schema, subsequent external write invisible until `REFRESH TABLE`. - **Scenario 5 (external drop and recreate table)**: Query sees the new empty table. #### New files - **`DSv2CacheTableReadTests`**: Shared trait containing all 5 tests, using `session.sql(...)` with `.collect()` calls (harmless in classic mode, required for Connect). #### Modified files - **`DataSourceV2DataFrameSuite`**: Mixes in `DSv2CacheTableReadTests` alongside existing traits (classic runner, `testPrefix = ""`). - **`DataSourceV2DataFrameConnectSuite`**: Mixes in `DSv2CacheTableReadTests` alongside existing traits (Connect runner, `testPrefix = "[connect] "`). - **`DSv2ExternalMutationTestBase`**: Updated Scaladoc to reference all three consumer traits. - **`DSv2TempViewWithStoredPlanTests`**: Removed stale comment about inherited vals. ### Why are the changes needed? These tests document and lock down the expected CACHE TABLE behavior with DSv2 tables: cached reads are pinned against external mutations, and `REFRESH TABLE` invalidates the cache. This prevents regressions if cache invalidation logic changes. ### Does this PR introduce _any_ user-facing change? No. This PR is test-only. ### How was this patch tested? 5 new tests run in both classic and Connect modes (10 total): Classic (175 total, all pass): ``` build/sbt 'sql/testOnly *DataSourceV2DataFrameSuite' ``` Connect (35 total, all pass): ``` build/sbt 'connect/testOnly *DataSourceV2DataFrameConnectSuite' ``` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (claude-opus-4-6) Closes#55536 from longvu-db/spark-dsv2-cache-scenario-5. Authored-by: Thang Long Vu <long.vu@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit 5a47a30) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…reads tests ### What changes were proposed in this pull request? Add 5 tests verifying CACHE TABLE impact on reads for DSv2 tables. Write operations ignore the cache, so these tests verify how reads behave when a cached table is mutated by session SQL or external catalog API calls. Tests run in both classic and Connect modes. `DSv2CacheTableReadTests` extends `DSv2ExternalMutationTestBase`, following the same pattern as `DSv2TempViewWithStoredPlanTests` (PR #55571) and `DSv2RepeatedTableAccessTests` (PR #55583). The tests cover five scenarios from the design doc: - **Scenario 1 (external write)**: Cache pins the read, external write invisible until `REFRESH TABLE`. - **Scenario 2 (session write and more external changes)**: Session write rebuilds cache, subsequent external write invisible until `REFRESH TABLE`. - **Scenario 3 (external schema changes)**: Cache pinned at original schema, external ADD COLUMN invisible until `REFRESH TABLE`. - **Scenario 4 (session schema changes and more external changes)**: Session ALTER rebuilds cache with new schema, subsequent external write invisible until `REFRESH TABLE`. - **Scenario 5 (external drop and recreate table)**: Query sees the new empty table. #### New files - **`DSv2CacheTableReadTests`**: Shared trait containing all 5 tests, using `session.sql(...)` with `.collect()` calls (harmless in classic mode, required for Connect). #### Modified files - **`DataSourceV2DataFrameSuite`**: Mixes in `DSv2CacheTableReadTests` alongside existing traits (classic runner, `testPrefix = ""`). - **`DataSourceV2DataFrameConnectSuite`**: Mixes in `DSv2CacheTableReadTests` alongside existing traits (Connect runner, `testPrefix = "[connect] "`). - **`DSv2ExternalMutationTestBase`**: Updated Scaladoc to reference all three consumer traits. - **`DSv2TempViewWithStoredPlanTests`**: Removed stale comment about inherited vals. ### Why are the changes needed? These tests document and lock down the expected CACHE TABLE behavior with DSv2 tables: cached reads are pinned against external mutations, and `REFRESH TABLE` invalidates the cache. This prevents regressions if cache invalidation logic changes. ### Does this PR introduce _any_ user-facing change? No. This PR is test-only. ### How was this patch tested? 5 new tests run in both classic and Connect modes (10 total): Classic (175 total, all pass): ``` build/sbt 'sql/testOnly *DataSourceV2DataFrameSuite' ``` Connect (35 total, all pass): ``` build/sbt 'connect/testOnly *DataSourceV2DataFrameConnectSuite' ``` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (claude-opus-4-6) Closes#55536 from longvu-db/spark-dsv2-cache-scenario-5. Authored-by: Thang Long Vu <long.vu@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit 5a47a30) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
What changes were proposed in this pull request?
This PR adds the
netChangesdeduplication mode to theResolveChangelogTableanalyzer rule (SPARK-55668 / #55508 ). When a CDC read setsdeduplicationMode = 'netChanges', intermediate changes per row identity are collapsed into a single net effect, per the SPIPDeduplication Semanticsin B.8.Net change collapse rules (per SPIP)
Implementation: 2x2 matrix on
(existedBefore, existsAfter)The four SPIP rules map onto a 2x2 matrix that the implementation evaluates per
rowIdpartition:existedBeforeistrueiff the partition's first event isdeleteorupdate_preimage.existsAfteristrueiff the partition's last event isinsertorupdate_postimage.These two booleans are sufficient to reproduce the SPIP rules above, because the SPIP only cares about whether the row existed at the boundaries of the version range — never about the intermediate events.
If
computeUpdates = false, theupdate_preimage+update_postimagepair is emitted asdelete+insertinstead.Pipeline:
Window(per-rowIdaggregates: row number, row count, first/last_change_type) →Filter(keep first and/or last row per partition) →Project(relabel_change_type, drop helper columns).Why are the changes needed?
This completes the net-change post-processing capability of the DSv2 CDC API per the SPIP. Without it, connectors that surface intermediate changes cannot expose a deduplicated change feed to users via the standard CDC API.
Does this PR introduce any user-facing change?
Yes. Requesting
deduplicationMode = 'netChanges'on a CDC read now produces a deduplicated change stream. Previously the same request was rejected up-front.How was this patch tested?
Added
ResolveChangelogTableNetChangesSuite— a trait + 2 concrete suite classes (...WithComputeUpdatesSuite,...WithoutComputeUpdatesSuite) running the same 16-test body under both modes (32 invocations total). Coverage:Removed 2 obsolete tests in
ResolveChangelogTablePostProcessingSuitethat asserted the previous "not supported" rejection.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (claude-opus-4-7)