Describe the bug
SparkPhysicalExprAdapter::replace_with_spark_cast (native/core/src/parquet/schema_adapter.rs) applies Spark's Parquet type-conversion rejection rules (the SchemaColumnConvertNotSupportedException matrix completed in #4229) only to the top-level physical/logical pair. Same-shape complex pairs (struct→struct, list→list, map→map; schema_adapter.rs:950-968) are wrapped in CometCastColumnExpr, whose parquet_convert_array (native/core/src/parquet/parquet_support.rs:234-237) recurses into every nested leaf with a plain Arrow cast_with_options(.., safe: true) whenever can_cast_types is true — and returns the unconverted array otherwise. Spark's vectorized reader runs getUpdater per leaf regardless of nesting, so every conversion Comet rejects at top level is silently performed inside a struct/array/map.
Observed with a test through the real DataSourceExec + adapter (file schema → read schema):
| file |
read schema |
Spark |
Comet |
struct<x:int64> (5000000000, 1) |
struct<x:int> |
error |
{null}, {1} (overflow silently NULLed) |
struct<x:int> |
struct<x:string> |
error |
"1", "2" |
struct<d:decimal(10,2)> 123456.78 |
struct<d:decimal(5,2)> |
error |
NULL |
struct<x:string> ("12","abc") |
struct<x:int> |
error |
12, NULL (string parsed) |
struct<x:int> |
struct<x:array<int>> |
error |
[1], [2] |
struct<x:array<int>> |
struct<x:int> |
error |
native panic in StructArray::new ("Incorrect datatype for StructArray field "x", expected Int32 got List(Int32)") |
The identical top-level pair (x: int64 read as int) is correctly rejected with "Parquet column cannot be converted … Expected: int, Found: INT64".
Steps to reproduce
val p = "/tmp/nested_evo"
Seq((5000000000L, 1)).toDF("x", "y").select(struct($"x").as("s")).write.mode("overwrite").parquet(p)
spark.read.schema("s struct<x:int>").parquet(p).show()
Spark: SchemaColumnConvertNotSupportedException (Column: [s, x], Expected: int, Found: INT64). Comet: prints {null} and {1}.
For the panic: write s struct<x: array<int>> and read it with spark.read.schema("s struct<x:int>").
Expected behavior
Nested leaves follow the same conversion rules as top-level columns: the same rejections as Spark, never a silent NULL/parse, and never a native panic.
Proposed solution
Apply the rejection matrix recursively when building a CometCastColumnExpr for a nested pair: walk the physical vs logical nested types (matching struct fields with the same field-id / case-fold rules as parquet_convert_struct_to_struct) and run the scalar-pair checks (string/binary ↔ non-string, primitive → string, decimal/int → decimal narrowing, long → int, float → int, date → LTZ, complex ↔ scalar, allow_type_promotion gating) on every leaf, returning parquet_schema_convert_err / RejectOnNonEmpty with the dotted column path. Make the _ => Ok(array) fallthrough in parquet_convert_array an error instead of returning a mismatched array, and use StructArray::try_new so a mismatch surfaces as a DataFusion error rather than a panic.
Additional context
Default (and only) V1 native Parquet path; requires a schema mismatch (explicit read schema, or an evolved catalog schema over older files). Related: #4297 / #4229 (top-level rules only), #5553 (nested TIMESTAMP_MILLIS overflow is unchecked for the same reason), #1633 (earlier StructArray::new panic from the nested convert).
Describe the bug
SparkPhysicalExprAdapter::replace_with_spark_cast(native/core/src/parquet/schema_adapter.rs) applies Spark's Parquet type-conversion rejection rules (theSchemaColumnConvertNotSupportedExceptionmatrix completed in #4229) only to the top-level physical/logical pair. Same-shape complex pairs (struct→struct, list→list, map→map;schema_adapter.rs:950-968) are wrapped inCometCastColumnExpr, whoseparquet_convert_array(native/core/src/parquet/parquet_support.rs:234-237) recurses into every nested leaf with a plain Arrowcast_with_options(.., safe: true)whenevercan_cast_typesis true — and returns the unconverted array otherwise. Spark's vectorized reader runsgetUpdaterper leaf regardless of nesting, so every conversion Comet rejects at top level is silently performed inside a struct/array/map.Observed with a test through the real
DataSourceExec+ adapter (file schema → read schema):struct<x:int64>(5000000000, 1)struct<x:int>{null},{1}(overflow silently NULLed)struct<x:int>struct<x:string>"1","2"struct<d:decimal(10,2)>123456.78struct<d:decimal(5,2)>NULLstruct<x:string>("12","abc")struct<x:int>12,NULL(string parsed)struct<x:int>struct<x:array<int>>[1],[2]struct<x:array<int>>struct<x:int>StructArray::new("Incorrect datatype for StructArray field "x", expected Int32 got List(Int32)")The identical top-level pair (
x: int64read asint) is correctly rejected with "Parquet column cannot be converted … Expected: int, Found: INT64".Steps to reproduce
Spark:
SchemaColumnConvertNotSupportedException(Column: [s, x], Expected: int, Found: INT64). Comet: prints{null}and{1}.For the panic: write
s struct<x: array<int>>and read it withspark.read.schema("s struct<x:int>").Expected behavior
Nested leaves follow the same conversion rules as top-level columns: the same rejections as Spark, never a silent NULL/parse, and never a native panic.
Proposed solution
Apply the rejection matrix recursively when building a
CometCastColumnExprfor a nested pair: walk the physical vs logical nested types (matching struct fields with the same field-id / case-fold rules asparquet_convert_struct_to_struct) and run the scalar-pair checks (string/binary ↔ non-string, primitive → string, decimal/int → decimal narrowing, long → int, float → int, date → LTZ, complex ↔ scalar,allow_type_promotiongating) on every leaf, returningparquet_schema_convert_err/RejectOnNonEmptywith the dotted column path. Make the_ => Ok(array)fallthrough inparquet_convert_arrayan error instead of returning a mismatched array, and useStructArray::try_newso a mismatch surfaces as a DataFusion error rather than a panic.Additional context
Default (and only) V1 native Parquet path; requires a schema mismatch (explicit read schema, or an evolved catalog schema over older files). Related: #4297 / #4229 (top-level rules only), #5553 (nested TIMESTAMP_MILLIS overflow is unchecked for the same reason), #1633 (earlier
StructArray::newpanic from the nested convert).