[ 
https://issues.apache.org/jira/browse/SPARK-58478?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Andreas Neumann updated SPARK-58478:
------------------------------------
    Description: 
h2. Summary

SCD Type 1 AutoCDC is internally inconsistent about subtractive schema 
evolution across runs:

* Dropping a *top-level* column from the flow's source (or narrowing the 
{{COLUMNS}} selection) is *accepted*: the column is preserved on 
already-written rows and set to {{NULL}} on newly-inserted rows. Covered by 
{{AutoCdcScd1SchemaEvolutionSuite}} ("a top-level column dropped from the 
source DF between runs is preserved on existing rows and left NULL on new 
rows").
* Dropping a *nested* struct field, or a field inside an {{array<struct>}} 
element, is *rejected* with {{INCOMPATIBLE_DATA_FOR_TABLE.CANNOT_FIND_DATA}} 
(same suite, "dropping a nested struct field between runs fails with 
INCOMPATIBLE_DATA_FOR_TABLE" and the {{array<struct>}} analog).

So the same logical operation -- the flow stops emitting a column that still 
exists on the target -- succeeds at the top level but fails one level down.

h2. Why

This is not a deliberate "reject subtractive evolution" policy; it is an 
artifact of where the failure surfaces in the MERGE write path:

* For a dropped top-level column, the SCD1 MERGE simply omits that column from 
its INSERT/UPDATE assignment maps, so nothing references the missing data and 
the write succeeds.
* For a dropped nested field, the v2 writer's {{TableOutputResolver}} walks 
into the target's struct and cannot find data for the nested path (e.g. 
{{value.b.c}}), so it throws {{CANNOT_FIND_DATA}}.

The nested case is stricter purely because the resolver descends into nested 
paths while the top-level assignment map does not.

h2. Contrast with SCD2

SPARK-58418 makes SCD2 handle both cases consistently: 
{{Scd2ForeachBatchHandler}} uses {{unionByName(..., allowMissingColumns = 
true)}}, which pads a missing column -- top-level or nested (recursing into 
structs/arrays) -- with {{NULL}} before the union/MERGE. As a result SCD2 
preserves the field on existing records and writes {{NULL}} on new ones for a 
nested drop too, matching its own top-level behavior. SCD1 does not have this 
padding on its MERGE source, so it retains the nested-drop failure. 
SPARK-58418's tests document this as a deliberate SCD2-vs-SCD1 divergence
({{AutoCdcScd2ColumnEvolutionSuite}}).

h2. Proposal

Make SCD1 accept a dropped nested struct / array-element field the same 
additive-tolerant way it already accepts a dropped top-level column (preserve 
on existing rows, {{NULL}} on new rows), so its subtractive-evolution behavior 
is consistent across nesting levels and matches SCD2. Likely approach: pad the 
MERGE source with the missing nested fields (analogous to SCD2's 
{{allowMissingColumns}}) before the write, rather than letting the resolver 
reject it.

When this is fixed, the two "fails with INCOMPATIBLE_DATA_FOR_TABLE" tests in 
{{AutoCdcScd1SchemaEvolutionSuite}} should be converted to assert the 
preserve/NULL behavior, and the SCD2-vs-SCD1 divergence note in 
{{AutoCdcScd2ColumnEvolutionSuite}} can be removed.

Not a regression and not blocking; captured for consistency while implementing 
SPARK-58418.


> SCD1 AutoCDC rejects dropping a nested struct/array field but accepts 
> dropping a top-level column
> -------------------------------------------------------------------------------------------------
>
>                 Key: SPARK-58478
>                 URL: https://issues.apache.org/jira/browse/SPARK-58478
>             Project: Spark
>          Issue Type: Sub-task
>          Components: SQL
>    Affects Versions: 5.0.0
>            Reporter: Andreas Neumann
>            Priority: Major
>
> h2. Summary
> SCD Type 1 AutoCDC is internally inconsistent about subtractive schema 
> evolution across runs:
> * Dropping a *top-level* column from the flow's source (or narrowing the 
> {{COLUMNS}} selection) is *accepted*: the column is preserved on 
> already-written rows and set to {{NULL}} on newly-inserted rows. Covered by 
> {{AutoCdcScd1SchemaEvolutionSuite}} ("a top-level column dropped from the 
> source DF between runs is preserved on existing rows and left NULL on new 
> rows").
> * Dropping a *nested* struct field, or a field inside an {{array<struct>}} 
> element, is *rejected* with {{INCOMPATIBLE_DATA_FOR_TABLE.CANNOT_FIND_DATA}} 
> (same suite, "dropping a nested struct field between runs fails with 
> INCOMPATIBLE_DATA_FOR_TABLE" and the {{array<struct>}} analog).
> So the same logical operation -- the flow stops emitting a column that still 
> exists on the target -- succeeds at the top level but fails one level down.
> h2. Why
> This is not a deliberate "reject subtractive evolution" policy; it is an 
> artifact of where the failure surfaces in the MERGE write path:
> * For a dropped top-level column, the SCD1 MERGE simply omits that column 
> from its INSERT/UPDATE assignment maps, so nothing references the missing 
> data and the write succeeds.
> * For a dropped nested field, the v2 writer's {{TableOutputResolver}} walks 
> into the target's struct and cannot find data for the nested path (e.g. 
> {{value.b.c}}), so it throws {{CANNOT_FIND_DATA}}.
> The nested case is stricter purely because the resolver descends into nested 
> paths while the top-level assignment map does not.
> h2. Contrast with SCD2
> SPARK-58418 makes SCD2 handle both cases consistently: 
> {{Scd2ForeachBatchHandler}} uses {{unionByName(..., allowMissingColumns = 
> true)}}, which pads a missing column -- top-level or nested (recursing into 
> structs/arrays) -- with {{NULL}} before the union/MERGE. As a result SCD2 
> preserves the field on existing records and writes {{NULL}} on new ones for a 
> nested drop too, matching its own top-level behavior. SCD1 does not have this 
> padding on its MERGE source, so it retains the nested-drop failure. 
> SPARK-58418's tests document this as a deliberate SCD2-vs-SCD1 divergence
> ({{AutoCdcScd2ColumnEvolutionSuite}}).
> h2. Proposal
> Make SCD1 accept a dropped nested struct / array-element field the same 
> additive-tolerant way it already accepts a dropped top-level column (preserve 
> on existing rows, {{NULL}} on new rows), so its subtractive-evolution 
> behavior is consistent across nesting levels and matches SCD2. Likely 
> approach: pad the MERGE source with the missing nested fields (analogous to 
> SCD2's {{allowMissingColumns}}) before the write, rather than letting the 
> resolver reject it.
> When this is fixed, the two "fails with INCOMPATIBLE_DATA_FOR_TABLE" tests in 
> {{AutoCdcScd1SchemaEvolutionSuite}} should be converted to assert the 
> preserve/NULL behavior, and the SCD2-vs-SCD1 divergence note in 
> {{AutoCdcScd2ColumnEvolutionSuite}} can be removed.
> Not a regression and not blocking; captured for consistency while 
> implementing SPARK-58418.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to