sunchao commented on code in PR #6428:
URL: https://github.com/apache/datafusion-comet/pull/6428#discussion_r4212259778


##########
native/core/src/execution/planner.rs:
##########
@@ -1254,13 +1257,83 @@ impl PhysicalPlanner {
         }
     }
 
+    /// Merge compatible nested types by ordinal, retaining the left field 
names and physical
+    /// leaf types. Return None when a metadata-only Spark cast cannot safely 
align the layouts.
+    fn positional_nullability_union(left: &DataType, right: &DataType) -> 
Option<DataType> {

Review Comment:
   Consolidated this in 6d8a0a912. `positional_common_type` and 
`cast_to_common_type` now live in `case_when.rs` and serve IF, CASE, 
comparisons, and IN. The planner's `create_case_expr` wrapper is gone. 
Conditionals retain leaf-type promotion and combine map sort flags, while 
comparisons and IN only align compatible metadata. Both use the same 
dictionary-target checks and UTC cast options.
   
   Final validation after merging current main in ba8f2a5cd: 20 native 
conditional tests and four constructor tests pass; all 29 conditional/struct 
SQL-file cases pass on Spark 4.0.4. Focused Clippy, Rust formatting, Spotless, 
and Scalastyle pass. Local Spark 3.5/4.1 tests are dependency-DNS blocked; 
hosted CI has started.



##########
spark/src/test/resources/sql-tests/expressions/struct/create_named_struct_positional_comparison.sql:
##########
@@ -0,0 +1,38 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements.  See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership.  The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License.  You may obtain a copy of the License at
+--
+--   http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied.  See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+-- MinSparkVersion: 4.0
+
+-- Config: spark.comet.exec.range.enabled=true
+-- Config: spark.comet.sparkToColumnar.enabled=true
+-- Config: spark.comet.sparkToColumnar.supportedOperatorList=Range
+-- Config: spark.sql.caseSensitive=false
+
+-- Spark 4.x compares struct fields by position, irrespective of field names.
+query
+SELECT named_struct('x', CAST(id AS DOUBLE), 'y', CAST(NULL AS DOUBLE)) =
+       named_struct('y', CAST(NULL AS DOUBLE), 'x', CAST(id AS DOUBLE))
+FROM range(8)
+
+-- Equal nullability must not bypass field-name alignment. Native Range makes 
every field
+-- non-nullable; id=1 compares equal, while id=2 detects an accidental 
name-based reorder.
+query expect_native(equalto)

Review Comment:
   Added `<` and `<=>` alongside `=` in 6d8a0a912, with explicit 
native-expression assertions. The computed-struct query includes id=0 to catch 
the wrong ordering result you identified. The fixture also covers Parquet 
columns, so it exercises the BinaryExpr path as well as nested floating-point 
equality.
   
   Both isolated queries return wrong results with the merge-base native 
library and match Spark with this revision.
   
   Final validation after merging current main in ba8f2a5cd: 20 native 
conditional tests and four constructor tests pass; all 29 conditional/struct 
SQL-file cases pass on Spark 4.0.4. Focused Clippy, Rust formatting, Spotless, 
and Scalastyle pass. Local Spark 3.5/4.1 tests are dependency-DNS blocked; 
hosted CI has started.



##########
spark/src/main/scala/org/apache/comet/serde/structs.scala:
##########
@@ -56,6 +56,8 @@ object CometCreateNamedStruct extends 
CometExpressionSerde[CreateNamedStruct] {
       val structBuilder = ExprOuterClass.CreateNamedStruct.newBuilder()
       structBuilder.addAllValues(valExprs.map(_.get).asJava)
       structBuilder.addAllNames(expr.names.map(_.toString).asJava)
+      // Native expressions can conservatively report nullable even when 
Catalyst proves otherwise.
+      structBuilder.addAllFieldNullable(expr.valExprs.map(v => 
Boolean.box(v.nullable)).asJava)

Review Comment:
   You're right about the original example: `CometCreateArray` already 
normalizes it, and the direct `array_array` test did not establish a SQL 
regression. I replaced those fixtures with this path, which bypasses 
CreateArray:
   
   ```sql
   SELECT array_union(
     array_repeat(named_struct('x', CAST(id AS INT)), 2),
     array_repeat(named_struct('x', IF(id = 0, 0, 1)), 2))
   FROM range(3)
   ```
   
   On Spark 4.0.4 with ordinary constant folding and native Range, the PR 
matches Spark. Changing only `CreateNamedStruct::fields` back to 
`expr.nullable(schema)` fails in array_union because the first struct field is 
nullable and the second is not. The revised SQL covers this and its 
nested-struct variant. 6d8a0a912 retains `field_nullable`, makes the unit test 
check the constructor contract directly, corrects the PR rationale, and updates 
the literal-expansion docs.
   
   Final validation after merging current main in ba8f2a5cd: 20 native 
conditional tests and four constructor tests pass; all 29 conditional/struct 
SQL-file cases pass on Spark 4.0.4. Focused Clippy, Rust formatting, Spotless, 
and Scalastyle pass. Local Spark 3.5/4.1 tests are dependency-DNS blocked; 
hosted CI has started.



##########
spark/src/test/resources/sql-tests/expressions/struct/create_named_struct_nullability.sql:
##########
@@ -0,0 +1,55 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements.  See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership.  The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License.  You may obtain a copy of the License at
+--
+--   http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied.  See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+-- Config: spark.comet.sparkToColumnar.enabled=true
+-- Config: spark.comet.sparkToColumnar.supportedOperatorList=Range
+-- Config: spark.sql.caseSensitive=false
+
+-- Catalyst proves BIGINT -> DOUBLE nonnullable; native cast inference is 
conservative.
+-- The computed struct must have the same field flags as the typed NULL array 
element.
+query
+SELECT array(named_struct('score', CAST(id AS DOUBLE),
+                          'amount', CAST(id AS DECIMAL(18,2))), NULL)
+FROM range(8)
+
+-- The field contract must also survive a nested constructor with an actually 
nullable child.
+query
+SELECT array(named_struct('nested', named_struct('score', CAST(id AS DOUBLE),
+                         'optional', CASE WHEN id % 2 = 0 THEN id END)), NULL)
+FROM range(8)
+
+-- CASE unions nullability by ordinal, even when case-insensitive names swap 
places.
+query
+SELECT CASE WHEN id % 2 = 0

Review Comment:
   Added your map-valued CASE reproducer in 6d8a0a912, plus a variant with an 
untyped NULL branch. These replace the two weaker struct-only CASE queries. 
They remain ungated for Spark 3.x and run with case sensitivity disabled.
   
   The isolated CASE query fails Arrow validation with the merge-base native 
library and matches Spark with this revision.
   
   Final validation after merging current main in ba8f2a5cd: 20 native 
conditional tests and four constructor tests pass; all 29 conditional/struct 
SQL-file cases pass on Spark 4.0.4. Focused Clippy, Rust formatting, Spotless, 
and Scalastyle pass. Local Spark 3.5/4.1 tests are dependency-DNS blocked; 
hosted CI has started.



##########
native/core/src/execution/planner.rs:
##########
@@ -1254,13 +1257,83 @@ impl PhysicalPlanner {
         }
     }
 
+    /// Merge compatible nested types by ordinal, retaining the left field 
names and physical
+    /// leaf types. Return None when a metadata-only Spark cast cannot safely 
align the layouts.
+    fn positional_nullability_union(left: &DataType, right: &DataType) -> 
Option<DataType> {
+        fn merge_field(left: &FieldRef, right: &FieldRef) -> Option<FieldRef> {
+            Some(Arc::new(
+                left.as_ref()
+                    .clone()
+                    .with_data_type(merge_type(left.data_type(), 
right.data_type())?)
+                    .with_nullable(left.is_nullable() || right.is_nullable()),
+            ))
+        }
+        fn merge_type(left: &DataType, right: &DataType) -> Option<DataType> {
+            // Comet's Spark cast cannot target a dictionary, even for an 
unchanged child.
+            if matches!(left, DataType::Dictionary(_, _))
+                || matches!(right, DataType::Dictionary(_, _))
+            {
+                return None;
+            }
+            if matches!(left, DataType::Null) {
+                return Some(right.clone());
+            }
+            if matches!(right, DataType::Null) {
+                return Some(left.clone());
+            }
+            if left == right {
+                return Some(left.clone());
+            }
+            Some(match (left, right) {
+                (DataType::Struct(left), DataType::Struct(right)) if 
left.len() == right.len() => {
+                    DataType::Struct(
+                        left.iter()
+                            .zip(right)
+                            .map(|(left, right)| merge_field(left, right))
+                            .collect::<Option<_>>()?,
+                    )
+                }
+                (DataType::List(left), DataType::List(right)) => {
+                    DataType::List(merge_field(left, right)?)
+                }
+                (DataType::Map(left, sorted), DataType::Map(right, 
right_sorted))
+                    if sorted == right_sorted =>
+                {
+                    DataType::Map(merge_field(left, right)?, *sorted)
+                }
+                _ => return None,
+            })
+        }
+        merge_type(left, right)
+    }
+
+    /// Cast nested metadata by ordinal. Arrow's native struct cast matches 
field names and can
+    /// silently reorder values when Spark considers differently named structs 
compatible.
+    fn cast_to_positional_nested_type(
+        expr: Arc<dyn PhysicalExpr>,
+        from_type: &DataType,
+        target: &DataType,
+    ) -> Arc<dyn PhysicalExpr> {
+        if from_type == target {
+            expr
+        } else {
+            Arc::new(Cast::new(
+                expr,
+                target.clone(),
+                SparkCastOptions::new_without_timezone(EvalMode::Legacy, 
false),
+                None,
+                None,
+            ))
+        }
+    }
+
     /// DataFusion's nested comparison kernel (`apply_cmp_for_nested`) 
requires both operands to
-    /// have identical data types, including nested field nullability, whereas 
Spark comparisons
-    /// ignore nullability. When a comparison's operands are nested types that 
differ only in
-    /// nullability (e.g. a higher-order `transform` produces `List(non-null 
Struct)` while the
-    /// other side is `List(nullable Struct)`), cast both to their 
nullability-union type so the
-    /// kernel accepts them. Non-comparison ops and non-nested or 
already-matching types are left
-    /// untouched.
+    /// have identical data types, including nested field names and 
nullability, whereas Spark
+    /// compares compatible struct fields by ordinal. When operands differ in 
nested metadata,
+    /// cast both to the left field names and their nullability-union type so 
the kernel accepts
+    /// them without changing field positions. Exact equality is required 
here: Arrow's
+    /// `equals_datatype` ignores nested field names, even when nullability 
already matches.
+    /// Non-comparison ops and non-nested or already-matching types are left 
untouched.
     pub fn reconcile_nested_comparison_types(

Review Comment:
   Confirmed the failure on both the merge-base native library and the previous 
PR head and included the fix in 6d8a0a912. The planner now computes one common 
positional type across the IN value and every candidate before calling 
`spark_in_list`. A new SQL fixture covers integer and floating-point paths, 
multiple candidates, NOT IN, nested structs/lists, and NULL candidates. It is 
ungated because Spark 3.x also accepts differently named structs in IN.
   
   Final validation after merging current main in ba8f2a5cd: 20 native 
conditional tests and four constructor tests pass; all 29 conditional/struct 
SQL-file cases pass on Spark 4.0.4. Focused Clippy, Rust formatting, Spotless, 
and Scalastyle pass. Local Spark 3.5/4.1 tests are dependency-DNS blocked; 
hosted CI has started.



##########
spark/src/test/resources/sql-tests/expressions/struct/create_named_struct_positional_comparison.sql:
##########
@@ -0,0 +1,38 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements.  See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership.  The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License.  You may obtain a copy of the License at
+--
+--   http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied.  See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+-- MinSparkVersion: 4.0
+
+-- Config: spark.comet.exec.range.enabled=true
+-- Config: spark.comet.sparkToColumnar.enabled=true
+-- Config: spark.comet.sparkToColumnar.supportedOperatorList=Range
+-- Config: spark.sql.caseSensitive=false
+
+-- Spark 4.x compares struct fields by position, irrespective of field names.
+query
+SELECT named_struct('x', CAST(id AS DOUBLE), 'y', CAST(NULL AS DOUBLE)) =
+       named_struct('y', CAST(NULL AS DOUBLE), 'x', CAST(id AS DOUBLE))
+FROM range(8)

Review Comment:
   Added a Parquet table in 6d8a0a912 with swapped struct names, distinct field 
values, NULL structs, NULL leaves, NaN, and -0.0 versus +0.0. A native query 
checks `=`, `<`, and `<=>` against Spark for all nine rows.
   
   The first run exposed the existing nested signed-zero gap #6157. I 
subsequently merged current main (e76c35f1e), including its independent fix 
#6447, and restored the signed-zero row to all three comparisons. Nothing is 
excluded from the final fixture.
   
   Final validation at ba8f2a5cd: all 29 conditional/struct SQL-file cases pass 
on Spark 4.0.4, including this full Parquet fixture. The 20 native conditional 
and four constructor tests, focused Clippy, Rust formatting, Spotless, and 
Scalastyle pass. Local Spark 3.5/4.1 tests remain dependency-DNS blocked; 
hosted CI has started.



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