Joel Robin created SPARK-59147:
----------------------------------

             Summary: Nested SELECT * EXCEPT turns NULL structs into non-NULL 
structs
                 Key: SPARK-59147
                 URL: https://issues.apache.org/jira/browse/SPARK-59147
             Project: Spark
          Issue Type: Bug
          Components: SQL
    Affects Versions: 4.2.0, 4.3.0
            Reporter: Joel Robin


h3. Problem

Nested-field SELECT * EXCEPT reconstructs a nullable struct even when the 
original struct is NULL. This silently changes the parent struct from NULL to a 
non-NULL struct whose remaining fields are NULL.

h3. Reproduction

Reproduced on Apache Spark master at commit 
cdab5402f9f5890117d6156b6f0e7c0ed8e1aca6.

{code:sql}
WITH input AS (
  SELECT id,
    CASE
      WHEN id = 0 THEN CAST(NULL AS STRUCT<a: INT, b: INT>)
      ELSE named_struct('a', id, 'b', id + 10)
    END AS s
  FROM VALUES (0), (1) AS t(id)
),
actual AS (
  SELECT * EXCEPT (s.a) FROM input
)
SELECT
  i.id,
  i.s IS NULL AS before_except,
  a.s IS NULL AS after_except,
  a.s.b
FROM input i
JOIN actual a USING (id)
ORDER BY id;
{code}

h3. Actual behavior

{noformat}
(0, true,  false, NULL)
(1, false, false, 11)
{noformat}

For id 0, the input struct is NULL, but the struct returned by SELECT * EXCEPT 
(s.a) is non-NULL.

h3. Expected behavior

{noformat}
(0, true,  true,  NULL)
(1, false, false, 11)
{noformat}

Removing a nested field should preserve the nullness of the parent struct. For 
id 0, the result should remain CAST(NULL AS STRUCT<b: INT>), not 
named_struct('b', NULL).

h3. Customer impact

This is a silent SQL correctness issue. Nullable structs commonly come from 
parsed JSON and from the null-producing side of outer joins. Customers use 
nested-field SELECT * EXCEPT to remove unwanted fields from wide records; 
afterward, IS NULL checks, filters, joins, COALESCE expressions, and serialized 
output can behave differently because Spark has changed a NULL struct into a 
present struct containing NULL fields.

The same behavior was reproduced with NULL structs produced by from_json and 
with null-extended structs from outer joins.

h3. Technical analysis

UnresolvedStarExceptOrReplace.filterColumns handles a nested exclusion by 
extracting the retained fields and unconditionally wrapping them in 
CreateStruct. CreateStruct resolves to CreateNamedStruct, whose nullable 
property is always false. Consequently, the reconstructed struct cannot 
preserve the nullness of the original nullable parent expression.

Relevant locations:

* 
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/unresolved.scala,
 nested exclusion reconstruction in filterColumns
* 
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/complexTypeCreator.scala,
 CreateNamedStruct.nullable




--
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