[SPARK-58247][CONNECT][SDP] Support SCD Type 2 in the Auto CDC Python and Spark Connect APIs#57412
[SPARK-58247][CONNECT][SDP] Support SCD Type 2 in the Auto CDC Python and Spark Connect APIs#57412anew wants to merge 6 commits into
Conversation
… and Spark Connect APIs ### What changes were proposed in this pull request? Expose Auto CDC SCD Type 2 through the Python and Spark Connect APIs. `create_auto_cdc_flow` now accepts `stored_as_scd_type=2` (or `"2"`) and the SCD2-only `track_history_column_list` / `track_history_except_column_list` parameters, which flow through to `ChangeArgs(storedAsScdType = Type2, trackHistorySelection = ...)` on the server. Specifically: - Proto: add `SCD_TYPE_2` to the `SCDType` enum and `track_history_column_list` (field 11) / `track_history_except_column_list` (field 12) to `AutoCdcFlowDetails`. Field numbers match the Databricks runtime extension proto for wire compatibility. Regenerate the Python stubs. - Python API (`api.py`, `flow.py`): widen `stored_as_scd_type`, add and normalize the track-history parameters, and reject track-history columns unless SCD2 is selected. - Serialization (`spark_connect_graph_element_registry.py`): map the SCD type and serialize the track-history columns. - Server (`PipelinesHandler.scala`): map `SCD_TYPE_2` to `ScdType.Type2`, build `trackHistorySelection`, and reject specifying both track-history lists. - Add error condition `AUTOCDC_BOTH_TRACK_HISTORY_COLUMN_LIST_AND_EXCEPT_COLUMN_LIST`. The engine's SCD2 execution path is landing in a parallel workstream and still rejects SCD2 during graph analysis (`AUTOCDC_SCD2_NOT_SUPPORTED`). This change wires the API to the `ChangeArgs` contract those changes plug into; end-to-end SCD2 execution starts working once the engine drops those throws, with no further API change required. ### Why are the changes needed? The Python and Spark Connect Auto CDC APIs previously only supported SCD Type 1. SCD Type 2 (history tracking) is a common CDC requirement and is already modeled in the engine's `ChangeArgs`/`ScdType`. ### Does this PR introduce any user-facing change? Yes. `create_auto_cdc_flow` accepts `stored_as_scd_type=2` and the `track_history_column_list` / `track_history_except_column_list` parameters. ### How was this patch tested? New unit tests in `test_auto_cdc_flow.py` (SCD2 accept as int/str, track-history include/exclude, track-history-requires-SCD2 rejection) and `PythonPipelineSuite.scala` (SCD2 registration forwards to `ChangeArgs`, both-lists rejection). Ran the Python and Scala suites plus ruff/mypy. Co-authored-by: Isaac
3d8641a to
d1253ee
Compare
…er-side Address review feedback: the handler already re-checked the both-lists case (AUTOCDC_BOTH_TRACK_HISTORY_...) but did not mirror the "track-history requires SCD2" check that the Python client performs. A raw (non-Python) Connect client sending SCD_TYPE_1 with a populated track-history list would reach ChangeArgs, whose validateTrackHistoryOnlyForScd2 guard raises SparkException.internalError (INTERNAL_ERROR) rather than a clean user-facing error. Mirror the check in PipelinesHandler.buildAutoCdcFlow with a new AUTOCDC_TRACK_HISTORY_REQUIRES_SCD2 AnalysisException, making the two track-history validations symmetric. Cover both the new requires-SCD2 guard and the existing both-lists guard with raw-proto tests in SparkDeclarativePipelinesServerSuite (the Python client rejects these client-side, so the raw stub is needed to exercise the server path). Co-authored-by: Isaac
| # a notion that only exists under SCD2. Reject them for any other SCD type. The engine | ||
| # enforces this too, but failing here gives a clearer, client-side error. | ||
| is_scd2 = stored_as_scd_type is not None and str(stored_as_scd_type) == "2" | ||
| if not is_scd2 and ( |
There was a problem hiding this comment.
The empty-list case is treated as "provided". This gate uses is not None, so track_history_column_list=[] (or [] for the except variant) raises INVALID_MULTIPLE_ARGUMENT_CONDITIONS when the flow is not SCD2 -- even though an empty list serializes identically to an omitted one (to_plans([]) -> [] -> unset repeated field on the wire) and is thus a no-op. Consider gating on non-empty (e.g. truthiness of the lists) so an empty list is accepted/ignored consistently with how it is serialized.
There was a problem hiding this comment.
Good point - addressed
| errorClass="INVALID_MULTIPLE_ARGUMENT_CONDITIONS", | ||
| messageParameters={ | ||
| "arg_names": "track_history_column_list, track_history_except_column_list", | ||
| "condition": "specified unless stored_as_scd_type is 2 (or '2')", |
There was a problem hiding this comment.
Minor wording nit: the rendered message always names both parameters. With the template "[<arg_names>] cannot be <condition>.", a caller who passed only track_history_column_list still gets [track_history_column_list, track_history_except_column_list] cannot be specified unless stored_as_scd_type is 2 (or '2'). Accurate as a rule about the pair, but less precise than naming the argument actually supplied. Optional.
| except_column_list = _normalize_optional_column_list( | ||
| arg_name="except_column_list", column_list=except_column_list | ||
| ) | ||
| track_history_column_list = _normalize_optional_column_list( |
There was a problem hiding this comment.
Asymmetry with the docstring's "Only one ... can be specified" promise: the client validates the SCD2 requirement, but specifying both track_history_column_list and track_history_except_column_list is only rejected server-side (AUTOCDC_BOTH_TRACK_HISTORY_COLUMN_LIST_AND_EXCEPT_COLUMN_LIST in PipelinesHandler.scala). This mirrors the existing column_list/except_column_list handling (also server-only), so it is a consistent choice -- flagging only because the "both" case costs a server round-trip. Fine to leave as-is for consistency.
There was a problem hiding this comment.
Went ahead and enforced this client-side for both pairs (b0f36da) rather than leaving it server-only — added a
_reject_include_and_except_together helper raising CANNOT_SET_TOGETHER, covering column_list/except_column_list and the track-history pair, so it now matches the docstrings' "only one" promise without a round-trip. Kept the server-side checks as defense for raw Connect clients and moved their coverage to raw-proto tests in SparkDeclarativePipelinesServerSuite.
…args Two review nits on the client-side "track history requires SCD2" check in create_auto_cdc_flow: - The gate used `is not None`, so an empty list (`track_history_column_list=[]`) raised INVALID_MULTIPLE_ARGUMENT_CONDITIONS even though an empty list serializes identically to an omitted one (an unset repeated field) and is a no-op. Gate on non-empty lists instead, so `[]` is accepted and ignored consistently with how it is serialized. - The error named both parameters regardless of which was supplied. Build the arg_names list from the arguments actually passed, so a caller who supplied only one sees only that one. Add a test that an empty track-history list is accepted without SCD2. Co-authored-by: Isaac
Reject specifying both an include list and its except counterpart client-side in create_auto_cdc_flow, for both the column_list/except_column_list pair and the track_history_column_list/track_history_except_column_list pair, raising CANNOT_SET_TOGETHER (a PySparkValueError). Previously only the server rejected this, costing a round-trip; the docstrings already promised "only one ... can be specified". Add a shared _reject_include_and_except_together helper that gates on non-empty lists, so an empty list (a no-op that serializes to an unset repeated field) is not treated as "specified". The server-side defenses (AUTOCDC_BOTH_COLUMN_LIST_AND_EXCEPT_COLUMN_LIST and AUTOCDC_BOTH_TRACK_HISTORY_COLUMN_LIST_AND_EXCEPT_COLUMN_LIST) remain for raw Connect clients; move their coverage to raw-proto tests in SparkDeclarativePipelinesServerSuite and update the Python-driven tests in PythonPipelineSuite to assert the client-side CANNOT_SET_TOGETHER error. Co-authored-by: Isaac
The sql/connect modules enforce scalafmt in CI; reformat the AutoCDC raw-proto test helper and test to satisfy it. Formatting only, no behavior change. Co-authored-by: Isaac
| except_column_list: Optional[Union[List[str], List[Column]]] = None, | ||
| stored_as_scd_type: Optional[Literal[1, "1"]] = None, | ||
| stored_as_scd_type: Optional[Literal[1, 2, "1", "2"]] = None, | ||
| track_history_column_list: Optional[Union[List[str], List[Column]]] = None, |
There was a problem hiding this comment.
The new track_history_* parameters are inserted before the
existing name parameter in a public function that accepts positional arguments. Existing calls that
pass name positionally now bind that string to track_history_column_list, then fail validation as
“expected list[str] or list[Column]”. To preserve compatibility, keep name in its old positional slot
and add the new parameters after it, ideally keyword-only:
name: Optional[str] = None,
*,
track_history_column_list: Optional[...] = None,
track_history_except_column_list: Optional[...] = None,
There was a problem hiding this comment.
[Flagged by Codex 5.5]
There was a problem hiding this comment.
Restored name to its original positional slot and made both track_history_* params keyword-only (after *), so existing positional calls keep working and the new params can't be mis-bound positionally.
The track_history_column_list / track_history_except_column_list parameters were inserted before the existing `name` parameter, so an existing caller passing `name` positionally would bind that string to track_history_column_list and fail validation. Restore `name` to its original positional slot and add the new parameters after it as keyword-only (after `*`), preserving backward compatibility and preventing positional misuse. Co-authored-by: Isaac
… and Spark Connect APIs ### What changes were proposed in this pull request? Expose Auto CDC SCD Type 2 through the Python and Spark Connect APIs. `create_auto_cdc_flow` now accepts `stored_as_scd_type=2` (or `"2"`) and the SCD2-only `track_history_column_list` / `track_history_except_column_list` parameters, which flow through to `ChangeArgs(storedAsScdType = Type2, trackHistorySelection = ...)` on the server. Specifically: - Proto: add `SCD_TYPE_2` to the `SCDType` enum and `track_history_column_list` (field 11) / `track_history_except_column_list` (field 12) to `AutoCdcFlowDetails`. Field numbers match the Databricks runtime extension proto for wire compatibility. Regenerate the Python stubs. - Python API (`api.py`, `flow.py`): widen `stored_as_scd_type`, add and normalize the track-history parameters, and reject track-history columns unless SCD2 is selected. - Serialization (`spark_connect_graph_element_registry.py`): map the SCD type and serialize the track-history columns. - Server (`PipelinesHandler.scala`): map `SCD_TYPE_2` to `ScdType.Type2`, build `trackHistorySelection`, and reject specifying both track-history lists. - Add error condition `AUTOCDC_BOTH_TRACK_HISTORY_COLUMN_LIST_AND_EXCEPT_COLUMN_LIST`. The engine's SCD2 execution path is landing in a parallel workstream and still rejects SCD2 during graph analysis (`AUTOCDC_SCD2_NOT_SUPPORTED`). This change wires the API to the `ChangeArgs` contract those changes plug into; end-to-end SCD2 execution starts working once the engine drops those throws, with no further API change required. ### Why are the changes needed? The Python and Spark Connect Auto CDC APIs previously only supported SCD Type 1. SCD Type 2 (history tracking) is a common CDC requirement and is already modeled in the engine's `ChangeArgs`/`ScdType`. ### Does this PR introduce any user-facing change? Yes. `create_auto_cdc_flow` accepts `stored_as_scd_type=2` and the `track_history_column_list` / `track_history_except_column_list` parameters. ### How was this patch tested? New unit tests in `test_auto_cdc_flow.py` (SCD2 accept as int/str, track-history include/exclude, track-history-requires-SCD2 rejection) and `PythonPipelineSuite.scala` (SCD2 registration forwards to `ChangeArgs`, both-lists rejection). Ran the Python and Scala suites plus ruff/mypy. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Opus 4.8 Closes #57412 from anew/spark-58247-autocdc-scd2-python-api. Authored-by: Andreas Neumann <anew@apache.org> Signed-off-by: Szehon Ho <szehon.apache@gmail.com> (cherry picked from commit 3d59361) Signed-off-by: Szehon Ho <szehon.apache@gmail.com>
What changes were proposed in this pull request?
Expose Auto CDC SCD Type 2 through the Python and Spark Connect APIs.
create_auto_cdc_flownow acceptsstored_as_scd_type=2(or"2") and the SCD2-onlytrack_history_column_list/track_history_except_column_listparameters, which flow through toChangeArgs(storedAsScdType = Type2, trackHistorySelection = ...)on the server.Specifically:
SCD_TYPE_2to theSCDTypeenum andtrack_history_column_list(field 11) /track_history_except_column_list(field 12) toAutoCdcFlowDetails. Field numbers match the Databricks runtime extension proto for wire compatibility. Regenerate the Python stubs.api.py,flow.py): widenstored_as_scd_type, add and normalize the track-history parameters, and reject track-history columns unless SCD2 is selected.spark_connect_graph_element_registry.py): map the SCD type and serialize the track-history columns.PipelinesHandler.scala): mapSCD_TYPE_2toScdType.Type2, buildtrackHistorySelection, and reject specifying both track-history lists.AUTOCDC_BOTH_TRACK_HISTORY_COLUMN_LIST_AND_EXCEPT_COLUMN_LIST.The engine's SCD2 execution path is landing in a parallel workstream and still rejects SCD2 during graph analysis (
AUTOCDC_SCD2_NOT_SUPPORTED). This change wires the API to theChangeArgscontract those changes plug into; end-to-end SCD2 execution starts working once the engine drops those throws, with no further API change required.Why are the changes needed?
The Python and Spark Connect Auto CDC APIs previously only supported SCD Type 1. SCD Type 2 (history tracking) is a common CDC requirement and is already modeled in the engine's
ChangeArgs/ScdType.Does this PR introduce any user-facing change?
Yes.
create_auto_cdc_flowacceptsstored_as_scd_type=2and thetrack_history_column_list/track_history_except_column_listparameters.How was this patch tested?
New unit tests in
test_auto_cdc_flow.py(SCD2 accept as int/str, track-history include/exclude, track-history-requires-SCD2 rejection) andPythonPipelineSuite.scala(SCD2 registration forwards toChangeArgs, both-lists rejection). Ran the Python and Scala suites plus ruff/mypy.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Opus 4.8