sunchao commented on code in PR #24680:
URL: https://github.com/apache/datafusion/pull/24680#discussion_r3875936414


##########
datafusion/physical-expr-adapter/src/schema_rewriter.rs:
##########
@@ -1623,36 +1753,492 @@ mod tests {
         (logical, physical)
     }
 
+    fn decimal_cast_leaf_types(data_type: DataType) -> Vec<DataType> {
+        let item = Arc::new(Field::new("item", data_type.clone(), true));
+        vec![
+            data_type.clone(),
+            DataType::List(Arc::clone(&item)),
+            DataType::LargeList(Arc::clone(&item)),
+            DataType::FixedSizeList(Arc::clone(&item), 2),
+            DataType::ListView(Arc::clone(&item)),
+            DataType::LargeListView(item),
+            DataType::Map(
+                Arc::new(Field::new(
+                    "entries",
+                    DataType::Struct(
+                        vec![
+                            Field::new("key", DataType::Utf8, false),
+                            Field::new("value", data_type.clone(), true),
+                        ]
+                        .into(),
+                    ),
+                    false,
+                )),
+                false,
+            ),
+            DataType::Dictionary(Box::new(DataType::Int8), 
Box::new(data_type.clone())),
+            DataType::new_list(
+                DataType::Struct(
+                    vec![Field::new("value", data_type.clone(), true)].into(),
+                ),
+                true,
+            ),
+            DataType::new_list(DataType::new_list(data_type, true), true),
+        ]
+    }
+
+    #[test]
+    fn test_narrow_struct_cast_preserves_struct_unwrapping() -> Result<()> {
+        use arrow::array::{
+            ArrayRef, Decimal128Array, DictionaryArray, Int8Array, ListArray,
+        };
+        use arrow::buffer::OffsetBuffer;
+        use arrow::datatypes::Int8Type;
+
+        let values = Arc::new(StructArray::new(
+            vec![Field::new("value", DataType::Int32, true)].into(),
+            vec![Arc::new(Int32Array::from(vec![1]))],
+            None,
+        )) as ArrayRef;
+        let dictionary = Arc::new(DictionaryArray::<Int8Type>::try_new(
+            Int8Array::from(vec![0]),
+            values,
+        )?) as ArrayRef;
+        let expected_struct = Arc::new(StructArray::new(
+            vec![Field::new("value", DataType::Decimal128(10, 2), 
true)].into(),
+            vec![Arc::new(
+                Decimal128Array::from(vec![100]).with_precision_and_scale(10, 
2)?,
+            )],
+            None,
+        )) as ArrayRef;
+        let wrap_list = |values: ArrayRef| -> ArrayRef {
+            Arc::new(ListArray::new(
+                Arc::new(Field::new("item", values.data_type().clone(), true)),
+                OffsetBuffer::from_lengths([1]),
+                values,
+                None,
+            ))
+        };
+        let wrap_dictionary = |values: ArrayRef| -> ArrayRef {
+            Arc::new(
+                DictionaryArray::<Int8Type>::try_new(Int8Array::from(vec![0]), 
values)
+                    .unwrap(),
+            )
+        };
+        for (label, physical, expected) in [
+            (
+                "direct Dictionary",
+                Arc::clone(&dictionary),
+                Arc::clone(&expected_struct),
+            ),
+            (
+                "List of Dictionary",
+                wrap_list(Arc::clone(&dictionary)),
+                wrap_list(Arc::clone(&expected_struct)),
+            ),
+            (
+                "Dictionary of Dictionary",
+                wrap_dictionary(dictionary),
+                wrap_dictionary(expected_struct),
+            ),
+        ] {
+            let (logical_schema, physical_schema) = struct_schemas(
+                vec![Field::new("x", physical.data_type().clone(), true)],
+                vec![Field::new("x", expected.data_type().clone(), true)],
+            );
+            let DataType::Struct(fields) = 
physical_schema.field(0).data_type() else {
+                unreachable!()
+            };
+            let batch = RecordBatch::try_new(
+                Arc::clone(&physical_schema),
+                vec![Arc::new(StructArray::new(
+                    fields.clone(),
+                    vec![physical],
+                    None,
+                ))],
+            )?;
+            let adapter = DefaultPhysicalExprAdapterFactory
+                .create(Arc::clone(&logical_schema), physical_schema)?;
+            let rewritten = adapter.rewrite(get_field_expr(&logical_schema, 
"s", "x"))?;
+            let actual = rewritten.evaluate(&batch)?.into_array(1)?;
+            assert_eq!(actual.to_data(), expected.to_data(), "{label}");
+        }
+        Ok(())
+    }
+
     /// `s['x']` where the file stores `x` as `Int32` and the table declares
     /// `Int64` must cast the extracted field, not the whole struct, so that
     /// the column stays visible under the `get_field`.
     ///
     /// See <https://github.com/apache/datafusion/issues/24109>.
     #[test]
     fn test_narrow_struct_cast_to_field_access() {
-        let (logical_schema, physical_schema) = struct_schemas(
-            vec![Field::new("x", DataType::Int32, true)],
-            vec![Field::new("x", DataType::Int64, true)],
-        );
+        for (physical_type, logical_type) in [
+            (DataType::Int32, DataType::Int64),
+            (
+                DataType::new_list(DataType::Int32, true),
+                DataType::new_list(DataType::Int64, true),
+            ),
+        ] {
+            let (logical_schema, physical_schema) = struct_schemas(
+                vec![Field::new("x", physical_type.clone(), true)],
+                vec![Field::new("x", logical_type.clone(), true)],
+            );
+
+            let adapter = DefaultPhysicalExprAdapterFactory
+                .create(Arc::clone(&logical_schema), physical_schema)
+                .unwrap();
+            let rewritten = adapter
+                .rewrite(get_field_expr(&logical_schema, "s", "x"))
+                .unwrap();
+
+            let cast = assert_cast_expr(&rewritten);
+            assert_eq!(cast.cast_type(), &logical_type);
+            let get_field = cast
+                .expr()
+                .downcast_ref::<ScalarFunctionExpr>()
+                .expect("Expected get_field under the cast");
+            assert_eq!(get_field.return_type(), &physical_type);
+            assert!(
+                get_field.args()[0].downcast_ref::<Column>().is_some(),
+                "the struct column must not be hidden behind a cast, got: 
{rewritten}"
+            );
+        }
+    }
 
-        let adapter = DefaultPhysicalExprAdapterFactory
-            .create(Arc::clone(&logical_schema), physical_schema)
-            .unwrap();
-        let rewritten = adapter
-            .rewrite(get_field_expr(&logical_schema, "s", "x"))
-            .unwrap();
+    /// Selecting one field of an explicit cast must still evaluate sibling
+    /// conversions, even when schema adaptation inserts another cast below it.
+    #[test]
+    fn test_narrow_struct_cast_preserves_explicit_cast_errors() -> Result<()> {

Review Comment:
   Added SQL coverage in `schema_evolution_nested.slt` in 
[ec4b3f282](https://github.com/apache/datafusion/pull/24680/commits/ec4b3f282cfb82693cd1fac89eced5f7f5a2dd14):
 all-null scalar and List decimal schema evolution, plus an explicit Struct 
cast whose unselected `y` conversion must still fail.
   
   I kept the Rust tests as well. They directly distinguish adapter-generated 
and pre-existing casts and check logical Field metadata/nullability, expression 
shape, and Arrow container cases that SQL alone does not cover. The 
explicit-cast SQL case is a useful integration control; the direct adapter test 
remains the reproduction of the provenance bug.
   



##########
datafusion/datasource-parquet/src/row_filter.rs:
##########
@@ -1294,6 +1300,68 @@ mod test {
             candidate.read_plan.projection_mask, expected_mask,
             "projection_mask should select only the accessed struct field leaf"
         );
+
+        // Schema adaptation can leave a Struct cast intact. Its runtime filter
+        // must read every sibling, while planning still rejects explicit 
casts.

Review Comment:
   Clarified in 
[ec4b3f282](https://github.com/apache/datafusion/pull/24680/commits/ec4b3f282cfb82693cd1fac89eced5f7f5a2dd14).
 Schema adaptation retains Struct ancestors around some decimal conversions so 
an entirely null parent can skip converting its children.
   
   The comment now also explains why this test must still convert `label`: it 
is named by the cast target, even though `get_field` selects `value`. The 
extended fixture has an unused third sibling, which is pruned while the 
required `label` conversion still fails.
   



##########
datafusion/core/tests/parquet/expr_adapter.rs:
##########
@@ -790,6 +790,212 @@ async fn 
test_physical_expr_adapter_with_non_null_defaults() {
     assert_batches_eq!(expected, &batches);
 }
 
+#[tokio::test]
+async fn test_explicit_struct_cast_projection_preserves_sibling_errors() -> 
Result<()> {

Review Comment:
   Added these as Parquet-backed SQL cases in `schema_evolution_nested.slt` in 
[ec4b3f282](https://github.com/apache/datafusion/pull/24680/commits/ec4b3f282cfb82693cd1fac89eced5f7f5a2dd14).
 The fixtures are generated with `COPY` and reopened with evolved schemas. They 
cover the explicit sibling-cast error and scalar/List decimal conversions 
beneath an entirely null Struct, including null predicates with filter pushdown 
disabled and enabled. Statistics and pruning shortcuts are disabled for the 
filter cases, and the settings are reset afterward.
   
   I retained the Rust integration tests for their additional physical-plan and 
adapter assertions; the SLTs now cover the user-visible SQL behavior alongside 
them.
   



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