Conversation
sunchao
left a comment
There was a problem hiding this comment.
Correctness
This fixes the missing error in #5741: a manually declared ordinary struct can reach the native reader even when the Parquet field is annotated as VARIANT. Checking the physical extension marker against the requested marker is the right distinction. Matching only the storage child names would incorrectly classify ordinary structs. The change carries the ignore setting through the scan protocol, gates the Parquet datasource check to Spark 4.1+, and translates the new error through the Spark 4.x shim.
I found two [P2] correctness issues, detailed inline. The factory validates the full relation schema rather than the actual projection, so selecting an unrelated scalar column can fail because an omitted column has a VARIANT annotation. The map recursion also matches the entries children by name or field ID, while the reader consumes map keys and values by position. A differently named physical value field can therefore bypass the annotation check.
The existing field-ID remap preserves physical metadata, and absent requested fields do not acquire an annotation to reject. Marked Variant requests and the explicit ignore setting remain accepted by the guard. The PR documents a remaining version limitation: the Arrow marker loses the annotation's spec version. That limitation predates this guard's acceptance path and is not counted as a new finding here.
Validation
Reviewed 03f9da00857d469afb7dccdf6bcfdd1f3dd8dcb4 against 38a6ec362096c7c205b379c5a5d7a7fc81c6b9e9, including all 13 changed files. I checked maintained Spark 3.5/4.0 sources for schema clipping and map matching, plus the exact DataFusion 55.1.0 and parquet-rs 59.3.0 sources verified against the lockfile checksums. An isolated Rust probe compiled and exercised the exact guard and factory block with minimal type doubles. It confirmed the two control-flow cases and checked marked requests, the ignore setting, and renamed or missing field IDs. It did not execute Arrow, DataFusion, JNI, Spark, or a real Parquet scan. Maintained Spark 3.4/4.1 branches were unavailable, so no coverage of those sources is claimed.
The PR adds 15 native cases and reports seven passing Spark 4.1.3 suite tests. I have not independently rerun them. At the September 15, 04:40 UTC refresh, only labeling passed. CI, CodeQL and the Delta gate were approval-required with zero jobs. The cached merge has the exact reviewed base/head parents and the same whole tree as the head, but there is no executed product CI to credit.
Performance
The guard walks in-memory schema metadata when the adapter is created. It adds no data-row loop or metadata fetch. It does allocate paths and lookup maps while visiting nested fields, including currently unrequested roots. Restricting validation to the actual read schema addresses that unnecessary work together with the first correctness issue. The existing ASCII fast path and cached non-ASCII name folding remain available. No benchmark or measured speedup is claimed for this validation change.
Design
The native reader is the appropriate boundary because the requested Spark schema alone cannot reveal a file annotation. An eager check also preserves rejection of an empty file when the incompatible field is requested. The check needs the same selection rules as the read itself: the requested schema for its scope, field-ID/name resolution for structs, and positional resolution for map children. Those changes address the findings without restricting legitimate projections or moving the error into per-row execution.
Abstraction & complexity
The option and structured error carrier fit the existing scan and error-conversion interfaces. The new recursive matcher duplicates struct-resolution logic already centralized in match_struct_fields. Reusing that helper for structs and handling map children explicitly would keep validation aligned with conversion, including ambiguity handling. The file-writing tests and probe factory have a useful purpose: they distinguish a real Parquet annotation reaching the adapter from an Arrow-schema hint that merely looks annotated. Their missing boundary cases are an omitted annotated root and a differently named map value, which the inline findings request.
There was a problem hiding this comment.
Correctness
[P2] Could this validation use the actual requested read schema rather than every field of logical_file_schema? CometNativeScan keeps an ordinary struct in nativeDataSchema even when it is unprojected, and init_datasource_exec passes that full schema to ParquetSource with a separate projection. DataFusion 55.1.0 consequently calls this factory with the full schema before rewriting the projection. For a file with id INT, v VARIANT(1), a read declared as id INT, v STRUCT<value BINARY, metadata BINARY> followed by .select("id") now fails on v, although no read of v was requested. Spark clips its Parquet schema to the requested columns before conversion. Please keep the eager check for requested fields, including empty files, and add a regression covering an omitted annotated root.
There was a problem hiding this comment.
Correctness
[P2] Could the map branch validate the key and value by position instead of recursing through the entries struct's name/field-ID matcher? The requested Arrow map uses key/value, but parquet-rs retains the file's child names. If the second child is named payload and carries VARIANT(1), the struct matcher finds no value field and skips its annotation. Both check_conversion and parquet_convert_map_to_map still read that second child positionally, so the incompatible plain-struct map value gets past this new guard. Spark's map reader also selects children 0 and 1 rather than requiring those names. Please use the same positional pairing here and cover an annotated map value whose Parquet field name differs from value.

Which issue does this PR close?
Closes #5741.
Rationale for this change
Reading a VARIANT-annotated Parquet field as an ordinary
struct<value binary, metadata binary>silently returned the storage bytes. Spark rejects that read inParquetToSparkSchemaConverter.convertGroupFieldwith_LEGACY_ERROR_TEMP_3071unlessspark.sql.parquet.ignoreVariantAnnotationis set.CometScanRulecannot catch it: it only sees the requested schema, and a hand-written struct carries none of theVariantMetadatathatisVariantStructlooks for. The annotation lives in the file, so the check has to happen in the native reader.What changes are included in this PR?
arrow-rs surfaces the annotation as the
arrow.parquet.variantArrow extension type, and Comet's serde marks a requestedVariantTypethe same way.check_variant_annotationcompares the two sides symmetrically inSparkPhysicalExprAdapterFactory::create, so a marked request stays a legitimate Variant read and only an unmarked request against an annotated file is rejected.Matching on
value/metadatachild names instead would misclassify ordinary structs, which #5741 rules out.spark.sql.parquet.ignoreVariantAnnotationplumbed throughNativeScanCommon, read by key since the conf is 4.1-only while this file compiles against 3.4 through 4.1.spark_parquet_convertdoes.SparkError::ParquetVariantAnnotationMismatchconverted by the 4.x shim into Spark'sAnalysisExceptionwrapped inFAILED_READ_FILE.dev/diffs/4.1.3.diffregenerated perspark-sql-tests.mdto drop theIgnoreCometexclusion.Known gap, documented on
is_variant_marked: the extension type carries no spec version, so withignoreVariantAnnotation=trueon a non-v1 annotation Comet reads a file Spark refuses. Both engines reject it when the conf is off, with different error classes.How are these changes tested?
ParquetVariantShreddingSuite / variant logical type annotation - ignore variant annotationis no longer excluded. I ran the suite locally against Spark 4.1.3 withENABLE_COMET=true: 7 tests pass, none ignored. With the check disabled it fails withExpected exception org.apache.spark.SparkException to be thrown, but no exception was thrown, the failure #5741reports, so the suite is exercising this change.
15 native tests in
schema_adapter.rscover rejection at each nesting shape, a realVARIANT(1)file written through the low-level writer,ignoreVariantAnnotation=true, a marked Variant request not being rejected, an empty file, nested field-id resolution, and probes asserting the annotation still reachescreateas an extension type.