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


##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -963,8 +963,10 @@ case class CometScanRule(session: SparkSession)
   private def isSchemaSupported(scanExec: FileSourceScanExec, r: 
HadoopFsRelation): Boolean = {
     val fallbackReasons = new ListBuffer[String]()
     val typeChecker = CometScanTypeChecker()
-    val schemaSupported =
-      typeChecker.isSchemaSupported(scanExec.requiredSchema, fallbackReasons)
+    val schemaSupported = scanExec.requiredSchema.fields.forall { field =>
+      isVariantType(field.dataType) ||
+      typeChecker.isTypeSupported(field.dataType, field.name, fallbackReasons)

Review Comment:
   [P2] Reject unsupported Variant defaults before admitting the scan
   
   Could we keep this scan on Spark when a required Variant field has a 
non-null existence default? `CometNativeScan` drops failed default 
serializations with `flatMap`, but retains every default index, and 
`CometLiteral` still rejects Variant. I reproduced this on Spark 4.0.4:
   
   ```sql
   CREATE TABLE t(v VARIANT DEFAULT parse_json('1')) USING parquet;
   INSERT INTO t VALUES (parse_json('42'));
   ALTER TABLE t ADD COLUMNS(n INT DEFAULT 7);
   SELECT v, n FROM t;
   ```
   
   Spark returns `(42, 7)`, while this head's native scan returns `(42, NULL)`. 
The remaining default `7` is zipped to index 0 (`v`), where it is ignored 
because that column exists physically, leaving `n` without its default. Please 
reject an unserializable default or preserve and validate each value/index pair 
before enabling the scan.



##########
native/core/src/parquet/cast_column.rs:
##########
@@ -176,6 +180,42 @@ fn cast_timestamp_micros_to_millis_scalar(
     ScalarValue::TimestampMillisecond(new_val, target_tz)
 }
 
+fn normalize_variant_array(
+    array: &ArrayRef,
+    target_field: &FieldRef,
+) -> DataFusionResult<ArrayRef> {
+    let DataType::Struct(fields) = target_field.data_type() else {
+        return Err(DataFusionError::Execution(
+            "Variant extension field must use Struct storage".to_string(),
+        ));
+    };
+    if fields.len() != 2
+        || fields[0].name() != "value"
+        || fields[1].name() != "metadata"
+        || fields
+            .iter()
+            .any(|field| field.data_type() != &DataType::Binary)
+    {
+        return Err(DataFusionError::Execution(
+            "Variant output must contain Binary children [value, 
metadata]".to_string(),
+        ));
+    }
+
+    let variant = VariantArray::try_new(array.as_ref())?;
+    let unshredded = unshred_variant(&variant)?;

Review Comment:
   [P2] Preserve Spark lookup compatibility when rebuilding Unicode objects
   
   Could we account for Spark's object-key ordering before returning these 
reconstructed bytes? Arrow sorts object keys in UTF-8 order, but the supported 
Spark versions use Java `String.compareTo` and switch to binary search at 32 
fields. I wrote a shredded Parquet object with Spark containing `k00` through 
`k29`, `U+E000`, and `😀`. With `pushVariantIntoScan=false` and 
`allowReadingShredded=true`, `variant_get(v, '$.😀', 'int')` returns `531` on 
Spark but `NULL` with this native scan. The expression itself correctly falls 
back to Spark, but it consumes the incompatible reconstructed ordering. The 
byte-level mismatch also reproduces on Spark 4.1.3. Please normalize for the 
Spark consumer or retain fallback for affected values, with a 32-key Unicode 
regression.



##########
native/core/src/execution/utils.rs:
##########
@@ -56,7 +57,7 @@ impl SparkArrowConvert for ArrayData {
             );
             unsafe {
                 std::ptr::write(array_ptr, FFI_ArrowArray::new(self));
-                std::ptr::write(schema_ptr, 
FFI_ArrowSchema::try_from(self.data_type())?);
+                std::ptr::write(schema_ptr, FFI_ArrowSchema::try_from(field)?);

Review Comment:
   [P2] Handle NUL-containing field names before C schema export
   
   Could we preserve the Field metadata without passing an embedded-NUL name to 
Arrow's C-string exporter? Spark accepts a top-level Parquet column named 
`v\u0000suffix`. I wrote and read that ordinary BIGINT column successfully with 
Spark 4.0.4, but the native scan at this head fails with `NulError`. Arrow 
58.4.0's `FFI_ArrowSchema::try_from(field)` calls 
`CString::new(field.name()).unwrap()`, whereas the previous datatype-only 
export did not serialize the parent name. This affects non-Variant columns too, 
and the unaligned branch has the same issue. Please use a safe exported name or 
an explicit pre-execution fallback while retaining the logical metadata.



##########
native/core/src/parquet/cast_column.rs:
##########
@@ -176,6 +180,42 @@ fn cast_timestamp_micros_to_millis_scalar(
     ScalarValue::TimestampMillisecond(new_val, target_tz)
 }
 
+fn normalize_variant_array(
+    array: &ArrayRef,
+    target_field: &FieldRef,
+) -> DataFusionResult<ArrayRef> {
+    let DataType::Struct(fields) = target_field.data_type() else {
+        return Err(DataFusionError::Execution(
+            "Variant extension field must use Struct storage".to_string(),
+        ));
+    };
+    if fields.len() != 2
+        || fields[0].name() != "value"
+        || fields[1].name() != "metadata"
+        || fields
+            .iter()
+            .any(|field| field.data_type() != &DataType::Binary)
+    {
+        return Err(DataFusionError::Execution(
+            "Variant output must contain Binary children [value, 
metadata]".to_string(),
+        ));
+    }
+
+    let variant = VariantArray::try_new(array.as_ref())?;

Review Comment:
   [P2] Decode dictionary metadata before constructing VariantArray
   
   Could we decode dictionary-encoded metadata before this call? The [canonical 
Arrow Variant 
representation](https://arrow.apache.org/docs/format/CanonicalExtensions.html#parquet-variant)
 permits it, and an Arrow-written Parquet file can retain `metadata: 
Dictionary(Int32, Binary)` in its embedded `ARROW:schema` while storing 
ordinary required BINARY children physically. I reproduced a file containing 
`42, 43, 44`: Spark 4.0.4 reads it successfully, but this head's native scan 
throws `Illegal shredded value type: Dictionary(Int32, Binary)`. Arrow/Parquet 
58.4.0 restores the nested dictionary, which `VariantArray::try_new` rejects, 
so the Binary cast below is never reached. Decoding the metadata child first 
makes the same values readable.



##########
spark/src/test/resources/sql-tests/expressions/misc/variant.sql:
##########
@@ -43,8 +45,21 @@ SELECT tail FROM test_variant ORDER BY id
 query
 SELECT id, tail FROM test_variant WHERE tail IS NOT NULL ORDER BY id
 
+-- Full-value projection is scan-only: no native expression or pass-through 
operator carries v.
+query
+SELECT v FROM test_variant

Review Comment:
   [P2] Pin Variant pushdown for the native projection SQL assertions
   
   Could we set `spark.sql.variant.pushVariantIntoScan=false` for these 
native-projection cases, as the new Scala vector test does? Spark 4.1 and 4.2 
enable that optimizer rule by default, so even `SELECT v` becomes the annotated 
VariantStruct representation that this PR deliberately keeps on Spark. The 
plain `query` assertion then requires a native plan that cannot be produced. 
This is the actual failure in both [Spark 4.1 
CI](https://github.com/apache/datafusion-comet/actions/runs/32476998515/job/96761947281)
 and [Spark 4.2 
CI](https://github.com/apache/datafusion-comet/actions/runs/32476998515/job/96761947274):
 `variant.sql:50` fails with `Expected only Comet native operators, but found 
Project`. Please configure the whole-value test path explicitly and keep a 
separate fallback assertion for pushed VariantStruct.



##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -731,6 +732,15 @@ case class CometExecRule(session: SparkSession)
   private def tryConvertToComet(
       op: SparkPlan,
       handler: CometOperatorSerde[_]): Option[SparkPlan] = {
+    if (!op.isInstanceOf[CometScanExec] &&
+      (op.output ++ op.children.flatMap(_.output)).exists(attr =>
+        containsVariantType(attr.dataType))) {

Review Comment:
   [P2] Check the write input beneath WriteFilesExec
   
   Could this guard inspect the same unwrapped data-producing children used by 
`requiresNativeChildren` below? For 
`DataWritingCommandExec(WriteFilesExec(CometNativeScan[Variant]))`, both the 
command output and `WriteFilesExec.output` are empty, so the Variant check 
misses the input. With `spark.comet.parquet.write.enabled=true` and 
`spark.comet.operator.DataWritingCommandExec.allowIncompatible=true`, copying a 
Spark-written Variant Parquet column now selects `CometNativeWriteExec` and 
fails in `CometArrowStream.inputObjects -> Utils.toArrowSchema` with 
`Unsupported data type: ... VariantType ... variant`. The intended Spark write 
fallback succeeds. Please apply the Variant check after unwrapping 
`WriteFilesExec` so this scan-only change does not enable the unsupported 
writer.



##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -731,6 +732,15 @@ case class CometExecRule(session: SparkSession)
   private def tryConvertToComet(
       op: SparkPlan,
       handler: CometOperatorSerde[_]): Option[SparkPlan] = {
+    if (!op.isInstanceOf[CometScanExec] &&
+      (op.output ++ op.children.flatMap(_.output)).exists(attr =>
+        containsVariantType(attr.dataType))) {
+      withFallbackReason(
+        op,
+        "Native operators do not support schemas containing type VariantType")
+      return None

Review Comment:
   [P2] Apply the Variant fallback to the later Python rewrite too
   
   Could we apply this boundary to 
`EliminateRedundantTransitions.EligibleMapInBatch` as well? That later rule 
creates `CometMapInBatchExec` without passing through this guard. I reproduced 
a native Variant scan followed by `df.mapInPandas(lambda batches: batches, 
df.schema)`: it succeeds with `spark.comet.exec.pyarrowUDF.enabled=false`, but 
fails with the flag enabled. The accelerated runner forwards the new Arrow 
schema, which lacks Spark's `variant=true` metadata on the `metadata` child. 
Spark's Pandas serializer therefore supplies a `dict` rather than `VariantVal`, 
and the identity result fails `assert isinstance(variant, VariantVal)` during 
output conversion. Keeping Variant-bearing inputs on the ordinary Spark Python 
path would preserve the intended fallback.



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