cloud-fan commented on code in PR #59271:
URL: https://github.com/apache/spark/pull/59271#discussion_r4232446762


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala:
##########
@@ -1759,15 +1759,26 @@ case class Unpivot(
 
   def canBeCoercioned: Boolean = values.exists(_.nonEmpty) &&
     values.exists(_.forall(_.forall(_.resolved))) &&
+    // values are coerced by position, so every value group must have one 
value per value column,
+    // otherwise CheckAnalysis reports UNPIVOT_VALUE_SIZE_MISMATCH
+    values.exists(_.forall(_.length == valueColumnNames.length)) &&
     // when no ids are given, values must be Attributes (column names) to 
allow detecting ids
     // coercion will add aliases, would disallow detecting ids, so defer 
coercion after id detection
     ids.exists(_.forall(_.resolved))
 
-  def valuesTypeCoercioned: Boolean = canBeCoercioned &&
-    // all inner values at position idx must have the same data type
+  def valuesTypeCoercioned: Boolean = canBeCoercioned && {
+    // all inner values at position idx must have exactly the same data type, 
including nested

Review Comment:
   **Nit (P3):** This comment describes exact equality as an unconditional 
invariant, but the legacy branch below deliberately uses 
DataTypeUtils.sameType, which ignores nested nullability. In that mode 
valuesTypeCoercioned can therefore be true for types that are not exactly 
equal, so the current wording gives maintainers the wrong contract.
   
   **Verification:**
   - **Inspection:** The predicate documentation accurately states both 
configured comparison modes and does not imply that the legacy sameType result 
is exact equality.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala:
##########
@@ -1759,15 +1759,26 @@ case class Unpivot(
 
   def canBeCoercioned: Boolean = values.exists(_.nonEmpty) &&
     values.exists(_.forall(_.forall(_.resolved))) &&
+    // values are coerced by position, so every value group must have one 
value per value column,
+    // otherwise CheckAnalysis reports UNPIVOT_VALUE_SIZE_MISMATCH
+    values.exists(_.forall(_.length == valueColumnNames.length)) &&
     // when no ids are given, values must be Attributes (column names) to 
allow detecting ids
     // coercion will add aliases, would disallow detecting ids, so defer 
coercion after id detection
     ids.exists(_.forall(_.resolved))
 
-  def valuesTypeCoercioned: Boolean = canBeCoercioned &&
-    // all inner values at position idx must have the same data type
+  def valuesTypeCoercioned: Boolean = canBeCoercioned && {
+    // all inner values at position idx must have exactly the same data type, 
including nested
+    // nullability, because the value column takes the data type of the first 
value
+    val sameType: (DataType, DataType) => Boolean =
+      if 
(conf.getConf(SQLConf.LEGACY_UNPIVOT_USE_FIRST_VALUE_NESTED_NULLABILITY)) {
+        DataTypeUtils.sameType
+      } else {
+        _ == _

Review Comment:
   **Non-blocking (P2):** This also inserts runtime Casts when only nested 
nullability differs. SimplifyCasts leaves struct and deeper collection cases 
intact, and Cast's identity check still compares nullability, so every affected 
Expand branch rebuilds the full complex value per row. For wide structs or 
large nested collections that adds payload-linear CPU and allocation even 
though the physical representation and contents are unchanged.
   
   **Recommended change:** Treat casts whose source and target complex types 
are structurally identical when nullability is ignored as 
representation-preserving in both interpreted evaluation and code generation, 
while retaining the Cast node and its widened target DataType; add 
expression-level coverage for structs and recursively nested arrays and maps.
   
   **Why this works:** Extend Cast's structural identity test to ignore 
nullability for physical evaluation. The interpreted cast function returns the 
input complex value, and the code-generated path emits the child expression 
directly, while the unresolved and analyzed Cast expression continues to 
advertise the wider target type to UnpivotTransformer and downstream consumers.
   
   **Scope:** Keep nullability-only Casts as logical type declarations but make 
their physical evaluation an identity operation.
   
   **Compatibility:** Default UNPIVOT continues to merge nested nullability and 
return nested NULL values correctly, and the legacy switch continues to 
preserve first-value nested nullability.
   
   **Risks:** The Cast-level optimization applies beyond UNPIVOT, so coverage 
must confirm that forwarding the physical value is safe for every complex 
container shape and for both interpreted and generated execution. A too-broad 
structural predicate could suppress a real nested conversion or an 
ANSI/try-cast failure.
   
   **Constraints:** Apply the identity path only when source and target are 
structurally equal after ignoring nullability; field, element, key, or value 
type conversions must still execute. Preserve the target DataType and nullable 
contract even when evaluation forwards the original physical value. Keep 
legacy, ANSI, and try-cast error behavior unchanged for conversions that can 
fail.
   
   **Success:** Nullability-only complex casts retain their target logical 
DataType without rebuilding InternalRow, ArrayData, or MapData values. 
Wide-struct and deep-collection UNPIVOT plans no longer add payload-linear copy 
work solely to widen nested nullability. Casts that change an actual nested 
data type continue to perform conversion and preserve existing failure 
semantics.
   
   **Verification:**
   - **Regression:** Nullability-only casts of structs and recursively nested 
arrays and maps evaluate without allocating replacement complex containers in 
both interpreted and generated paths.
   - **Behavior:** The forwarded values remain readable under the widened 
target schema and retain correct NULL contents.
   - **Compatibility:** Representative complex casts with real nested type 
conversions and failure cases continue to convert or fail exactly as before.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala:
##########
@@ -1759,15 +1759,26 @@ case class Unpivot(
 
   def canBeCoercioned: Boolean = values.exists(_.nonEmpty) &&
     values.exists(_.forall(_.forall(_.resolved))) &&
+    // values are coerced by position, so every value group must have one 
value per value column,
+    // otherwise CheckAnalysis reports UNPIVOT_VALUE_SIZE_MISMATCH
+    values.exists(_.forall(_.length == valueColumnNames.length)) &&
     // when no ids are given, values must be Attributes (column names) to 
allow detecting ids
     // coercion will add aliases, would disallow detecting ids, so defer 
coercion after id detection
     ids.exists(_.forall(_.resolved))
 
-  def valuesTypeCoercioned: Boolean = canBeCoercioned &&
-    // all inner values at position idx must have the same data type
+  def valuesTypeCoercioned: Boolean = canBeCoercioned && {
+    // all inner values at position idx must have exactly the same data type, 
including nested
+    // nullability, because the value column takes the data type of the first 
value
+    val sameType: (DataType, DataType) => Boolean =
+      if 
(conf.getConf(SQLConf.LEGACY_UNPIVOT_USE_FIRST_VALUE_NESTED_NULLABILITY)) {
+        DataTypeUtils.sameType
+      } else {
+        _ == _

Review Comment:
   **Non-blocking (P2):** For struct values that differ only in nested 
nullability, this comparison now activates common-type coercion. 
findTypeForComplex rebuilds the target StructFields through the metadata-free 
add overload, and UnpivotTransformer exposes that rebuilt type, so nested 
comments and custom metadata from the first value are silently lost even though 
the field contents are unchanged.
   
   **Verification:**
   - **Regression:** Dataset.unpivot over metadata-bearing struct columns 
preserves the selected nested metadata while widening nullable fields, 
including both value orders.
   - **Behavior:** Struct, array, and map nested-nullability results and NULL 
values remain correct in default mode.
   - **Compatibility:** The legacy configuration continues to derive nested 
nullability from the first value.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to