[SPARK-59147][SQL] Preserve struct nullness when SELECT * EXCEPT drops a nested field - #58477
Vivek1106-04 wants to merge 2 commits into
Conversation
…s a nested field Nested-field `SELECT * EXCEPT (col.field)` rebuilds the enclosing struct with `CreateStruct`, which is never nullable. A NULL struct was therefore rewritten into a non-NULL struct whose remaining fields are NULL, silently changing query results for `IS NULL` checks, filters, joins and serialized output. Guard the reconstruction with `If(IsNull(col), Literal(null, dataType), ...)` when the original column is nullable, matching what `UpdateFields.evalExpr` already does for `DropField`. The pipe `|> DROP <col>.<field>` operator shares this code path and is fixed as well.
uros-b
left a comment
There was a problem hiding this comment.
The fix seems correct, minimal, and well-tested to me. Thank you @Vivek1106-04
Please ping @viirya and/or @cloud-fan for additional review!
|
@viirya ! and @cloud-fan - requesting for a review. Thank You |
viirya
left a comment
There was a problem hiding this comment.
The null-preserving fix makes sense when some fields remain. Could we clarify the intended behavior when all fields are excluded?
For s of type STRUCT<a: INT, b: INT>, SELECT * EXCEPT (s.a, s.b) currently produces an empty struct even when s is NULL. This patch changes that result to NULL, while the documentation says excluding all fields produces an empty struct.
I think preserving the empty-struct result is also reasonable: with no fields remaining, the result can be constructed without knowing the original value. Could we keep that behavior and apply the null guard only when fields remain? If changing this behavior is intentional, could we discuss the semantics explicitly and add documentation and tests covering both NULL and non-NULL inputs?
…ry field Address review feedback: apply the NULL guard only when fields remain. Excluding every field of a STRUCT produces an empty STRUCT that carries no information from the input, so it can be built without reading the original value. Keep that documented behavior for a NULL input instead of rewriting it to NULL, and restrict the guard to the case where some fields remain. Note that UpdateFields never reaches this case: it rejects dropping all fields with CANNOT_DROP_ALL_FIELDS, so it offers no precedent here. Document both rules and cover the all-excluded case for NULL and non-NULL inputs, including a nested struct whose every field is excluded.
|
Thanks @viirya — agreed, changed the patch accordingly. You are right that the The latest commit applies the null guard only when fields remain:
Docs now state both rules explicitly, and |
What changes were proposed in this pull request?
UnresolvedStarExceptOrReplace.filterColumnsrewrites a struct column when the EXCEPT listnames a nested field: it extracts the retained fields and wraps them back into a
CreateStruct.CreateStructresolves toCreateNamedStruct, which is never nullable, sothe rebuilt struct is always non-NULL even when the original struct expression evaluated to
NULL.
This PR guards the reconstruction with
If(IsNull(col), Literal(null, dataType), ...)whenthe original column is nullable, the same way
UpdateFields.evalExpralready does forDropField.Why are the changes needed?
Dropping a nested field silently turns a NULL struct into a non-NULL struct whose remaining
fields are NULL. This is a silent correctness bug: nullable structs commonly come from
parsed JSON and from the null-producing side of outer joins, and afterwards
IS NULLchecks, filters, joins,
COALESCEand serialized output all behave differently.Before:
After:
The same path backs the pipe
|> DROP <col>.<field>operator, which had the same problem.Does this PR introduce any user-facing change?
Yes.
SELECT * EXCEPT (col.field)(and|> DROP col.field) now returns NULL for a row whosecolis NULL, instead of a struct of NULL fields. The result type is unchanged. Thiscorrects wrong results; there is no behavior change for non-nullable structs or for rows
whose struct is not NULL.
How was this patch tested?
Added cases to
selectExcept.sqlcovering a NULL top-level struct, a NULL nested struct, andstruct expansion (
SELECT data.* EXCEPT (s2.c)), and regenerated the golden files. The newcases fail on master and pass with the fix.
pipe-operators.sqlanalyzer results wereregenerated to reflect the added null guard; its query results are unchanged.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5)