This is an automated email from the ASF dual-hosted git repository.

agrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new 5b5142b1 feat: Add GetStructField expression (#731)
5b5142b1 is described below

commit 5b5142b1a248e838f70de351baff4509a4f60434
Author: Adam Binford <[email protected]>
AuthorDate: Fri Aug 2 20:35:03 2024 -0400

    feat: Add GetStructField expression (#731)
    
    * Add GetStructField support
    
    * Add custom types to CometBatchScanExec
    
    * Remove test explain
    
    * Rust fmt
    
    * Fix struct type support checks
    
    * Support converting StructArray to native
    
    * fix style
    
    * Attempt to fix scalar subquery issue
    
    * Fix other unit test
    
    * Cleanup
    
    * Default query plan supporting complex type to false
    
    * Migrate struct expressions to spark-expr
    
    * Update shouldApplyRowToColumnar comment
    
    * Add nulls to test
    
    * Rename to allowStruct
    
    * Add DataTypeSupport trait
    
    * Fix parquet datatype test
---
 .../org/apache/spark/sql/comet/util/Utils.scala    |   3 +-
 .../src/execution/datafusion/expressions/mod.rs    |   1 -
 native/core/src/execution/datafusion/planner.rs    |   9 +-
 native/proto/src/proto/expr.proto                  |   6 ++
 native/spark-expr/src/lib.rs                       |   2 +
 .../src/structs.rs}                                | 106 ++++++++++++++++++++-
 .../apache/comet/CometSparkSessionExtensions.scala |  44 ++++-----
 .../scala/org/apache/comet/DataTypeSupport.scala   |  72 ++++++++++++++
 .../org/apache/comet/serde/QueryPlanSerde.scala    |  20 +++-
 .../spark/sql/comet/CometBatchScanExec.scala       |   4 +-
 .../spark/sql/comet/CometRowToColumnarExec.scala   |  25 ++++-
 .../org/apache/spark/sql/comet/CometScanExec.scala |   4 +-
 .../org/apache/comet/CometExpressionSuite.scala    |  21 +++-
 .../comet/CometSparkSessionExtensionsSuite.scala   |  37 -------
 .../apache/comet/parquet/ParquetReadSuite.scala    |  38 ++++++++
 15 files changed, 313 insertions(+), 79 deletions(-)

diff --git a/common/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala 
b/common/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala
index 2300e109..8d6a6334 100644
--- a/common/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala
+++ b/common/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala
@@ -28,6 +28,7 @@ import scala.collection.JavaConverters._
 import org.apache.arrow.c.CDataDictionaryProvider
 import org.apache.arrow.vector.{BigIntVector, BitVector, DateDayVector, 
DecimalVector, FieldVector, FixedSizeBinaryVector, Float4Vector, Float8Vector, 
IntVector, SmallIntVector, TimeStampMicroTZVector, TimeStampMicroVector, 
TinyIntVector, ValueVector, VarBinaryVector, VarCharVector, VectorSchemaRoot}
 import org.apache.arrow.vector.complex.MapVector
+import org.apache.arrow.vector.complex.StructVector
 import org.apache.arrow.vector.dictionary.DictionaryProvider
 import org.apache.arrow.vector.ipc.ArrowStreamWriter
 import org.apache.arrow.vector.types._
@@ -258,7 +259,7 @@ object Utils {
       case v @ (_: BitVector | _: TinyIntVector | _: SmallIntVector | _: 
IntVector |
           _: BigIntVector | _: Float4Vector | _: Float8Vector | _: 
VarCharVector |
           _: DecimalVector | _: DateDayVector | _: TimeStampMicroTZVector | _: 
VarBinaryVector |
-          _: FixedSizeBinaryVector | _: TimeStampMicroVector) =>
+          _: FixedSizeBinaryVector | _: TimeStampMicroVector | _: 
StructVector) =>
         v.asInstanceOf[FieldVector]
       case _ =>
         throw new SparkException(s"Unsupported Arrow Vector for $reason: 
${valueVector.getClass}")
diff --git a/native/core/src/execution/datafusion/expressions/mod.rs 
b/native/core/src/execution/datafusion/expressions/mod.rs
index ecd04956..4435f6b6 100644
--- a/native/core/src/execution/datafusion/expressions/mod.rs
+++ b/native/core/src/execution/datafusion/expressions/mod.rs
@@ -29,7 +29,6 @@ pub mod bloom_filter_might_contain;
 pub mod comet_scalar_funcs;
 pub mod correlation;
 pub mod covariance;
-pub mod create_named_struct;
 pub mod negative;
 pub mod stats;
 pub mod stddev;
diff --git a/native/core/src/execution/datafusion/planner.rs 
b/native/core/src/execution/datafusion/planner.rs
index 8038e0fb..b4d723eb 100644
--- a/native/core/src/execution/datafusion/planner.rs
+++ b/native/core/src/execution/datafusion/planner.rs
@@ -94,7 +94,7 @@ use crate::{
     },
 };
 
-use super::expressions::{create_named_struct::CreateNamedStruct, EvalMode};
+use super::expressions::EvalMode;
 use 
crate::execution::datafusion::expressions::comet_scalar_funcs::create_comet_physical_fun;
 use datafusion_comet_proto::{
     spark_expression::{
@@ -109,7 +109,8 @@ use datafusion_comet_proto::{
     spark_partitioning::{partitioning::PartitioningStruct, Partitioning as 
SparkPartitioning},
 };
 use datafusion_comet_spark_expr::{
-    Cast, DateTruncExpr, HourExpr, IfExpr, MinuteExpr, RLike, SecondExpr, 
TimestampTruncExpr,
+    Cast, CreateNamedStruct, DateTruncExpr, GetStructField, HourExpr, IfExpr, 
MinuteExpr, RLike,
+    SecondExpr, TimestampTruncExpr,
 };
 
 // For clippy error on type_complexity.
@@ -619,6 +620,10 @@ impl PhysicalPlanner {
                 let data_type = 
to_arrow_datatype(expr.datatype.as_ref().unwrap());
                 Ok(Arc::new(CreateNamedStruct::new(values, data_type)))
             }
+            ExprStruct::GetStructField(expr) => {
+                let child = self.create_expr(expr.child.as_ref().unwrap(), 
input_schema.clone())?;
+                Ok(Arc::new(GetStructField::new(child, expr.ordinal as usize)))
+            }
             expr => Err(ExecutionError::GeneralError(format!(
                 "Not implemented: {:?}",
                 expr
diff --git a/native/proto/src/proto/expr.proto 
b/native/proto/src/proto/expr.proto
index 158356df..485fdcfd 100644
--- a/native/proto/src/proto/expr.proto
+++ b/native/proto/src/proto/expr.proto
@@ -78,6 +78,7 @@ message Expr {
     UnboundReference unbound = 51;
     BloomFilterMightContain bloom_filter_might_contain = 52;
     CreateNamedStruct create_named_struct = 53;
+    GetStructField get_struct_field = 54;
   }
 }
 
@@ -492,6 +493,11 @@ message CreateNamedStruct {
   DataType datatype = 2;
 }
 
+message GetStructField {
+  Expr child = 1;
+  int32 ordinal = 2;
+}
+
 enum SortDirection {
   Ascending = 0;
   Descending = 1;
diff --git a/native/spark-expr/src/lib.rs b/native/spark-expr/src/lib.rs
index c9606233..cf7fc872 100644
--- a/native/spark-expr/src/lib.rs
+++ b/native/spark-expr/src/lib.rs
@@ -23,6 +23,7 @@ mod kernels;
 mod regexp;
 pub mod scalar_funcs;
 pub mod spark_hash;
+mod structs;
 mod temporal;
 pub mod timezone;
 pub mod utils;
@@ -32,6 +33,7 @@ pub use cast::{spark_cast, Cast};
 pub use error::{SparkError, SparkResult};
 pub use if_expr::IfExpr;
 pub use regexp::RLike;
+pub use structs::{CreateNamedStruct, GetStructField};
 pub use temporal::{DateTruncExpr, HourExpr, MinuteExpr, SecondExpr, 
TimestampTruncExpr};
 
 /// Spark supports three evaluation modes when evaluating expressions, which 
affect
diff --git 
a/native/core/src/execution/datafusion/expressions/create_named_struct.rs 
b/native/spark-expr/src/structs.rs
similarity index 68%
rename from 
native/core/src/execution/datafusion/expressions/create_named_struct.rs
rename to native/spark-expr/src/structs.rs
index c06aacf1..d6844ea8 100644
--- a/native/core/src/execution/datafusion/expressions/create_named_struct.rs
+++ b/native/spark-expr/src/structs.rs
@@ -19,9 +19,9 @@ use arrow::compute::take;
 use arrow::record_batch::RecordBatch;
 use arrow_array::types::Int32Type;
 use arrow_array::{Array, DictionaryArray, StructArray};
-use arrow_schema::{DataType, Schema};
+use arrow_schema::{DataType, Field, Schema};
 use datafusion::logical_expr::ColumnarValue;
-use datafusion_common::{DataFusionError, Result as DataFusionResult};
+use datafusion_common::{DataFusionError, Result as DataFusionResult, 
ScalarValue};
 use datafusion_physical_expr::PhysicalExpr;
 use std::{
     any::Any,
@@ -30,7 +30,7 @@ use std::{
     sync::Arc,
 };
 
-use crate::execution::datafusion::expressions::utils::down_cast_any_ref;
+use crate::utils::down_cast_any_ref;
 
 #[derive(Debug, Hash)]
 pub struct CreateNamedStruct {
@@ -142,6 +142,106 @@ impl PartialEq<dyn Any> for CreateNamedStruct {
     }
 }
 
+#[derive(Debug, Hash)]
+pub struct GetStructField {
+    child: Arc<dyn PhysicalExpr>,
+    ordinal: usize,
+}
+
+impl GetStructField {
+    pub fn new(child: Arc<dyn PhysicalExpr>, ordinal: usize) -> Self {
+        Self { child, ordinal }
+    }
+
+    fn child_field(&self, input_schema: &Schema) -> 
DataFusionResult<Arc<Field>> {
+        match self.child.data_type(input_schema)? {
+            DataType::Struct(fields) => Ok(fields[self.ordinal].clone()),
+            data_type => Err(DataFusionError::Plan(format!(
+                "Expect struct field, got {:?}",
+                data_type
+            ))),
+        }
+    }
+}
+
+impl PhysicalExpr for GetStructField {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, input_schema: &Schema) -> DataFusionResult<DataType> {
+        Ok(self.child_field(input_schema)?.data_type().clone())
+    }
+
+    fn nullable(&self, input_schema: &Schema) -> DataFusionResult<bool> {
+        Ok(self.child_field(input_schema)?.is_nullable())
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> DataFusionResult<ColumnarValue> 
{
+        let child_value = self.child.evaluate(batch)?;
+
+        match child_value {
+            ColumnarValue::Array(array) => {
+                let struct_array = array
+                    .as_any()
+                    .downcast_ref::<StructArray>()
+                    .expect("A struct is expected");
+
+                Ok(ColumnarValue::Array(
+                    struct_array.column(self.ordinal).clone(),
+                ))
+            }
+            ColumnarValue::Scalar(ScalarValue::Struct(struct_array)) => 
Ok(ColumnarValue::Array(
+                struct_array.column(self.ordinal).clone(),
+            )),
+            value => Err(DataFusionError::Execution(format!(
+                "Expected a struct array, got {:?}",
+                value
+            ))),
+        }
+    }
+
+    fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
+        vec![&self.child]
+    }
+
+    fn with_new_children(
+        self: Arc<Self>,
+        children: Vec<Arc<dyn PhysicalExpr>>,
+    ) -> datafusion_common::Result<Arc<dyn PhysicalExpr>> {
+        Ok(Arc::new(GetStructField::new(
+            children[0].clone(),
+            self.ordinal,
+        )))
+    }
+
+    fn dyn_hash(&self, state: &mut dyn Hasher) {
+        let mut s = state;
+        self.child.hash(&mut s);
+        self.ordinal.hash(&mut s);
+        self.hash(&mut s);
+    }
+}
+
+impl Display for GetStructField {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "GetStructField [child: {:?}, ordinal: {:?}]",
+            self.child, self.ordinal
+        )
+    }
+}
+
+impl PartialEq<dyn Any> for GetStructField {
+    fn eq(&self, other: &dyn Any) -> bool {
+        down_cast_any_ref(other)
+            .downcast_ref::<Self>()
+            .map(|x| self.child.eq(&x.child) && self.ordinal.eq(&x.ordinal))
+            .unwrap_or(false)
+    }
+}
+
 #[cfg(test)]
 mod test {
     use super::CreateNamedStruct;
diff --git 
a/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala 
b/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala
index 96ec23fa..87697309 100644
--- a/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala
+++ b/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala
@@ -42,11 +42,10 @@ import 
org.apache.spark.sql.execution.exchange.{BroadcastExchangeExec, ReusedExc
 import org.apache.spark.sql.execution.joins.{BroadcastHashJoinExec, 
ShuffledHashJoinExec, SortMergeJoinExec}
 import org.apache.spark.sql.execution.window.WindowExec
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types._
 
 import org.apache.comet.CometConf._
 import org.apache.comet.CometExplainInfo.getActualPlan
-import org.apache.comet.CometSparkSessionExtensions.{createMessage, 
getCometBroadcastNotEnabledReason, getCometShuffleNotEnabledReason, 
isANSIEnabled, isCometBroadCastForceEnabled, isCometEnabled, 
isCometExecEnabled, isCometJVMShuffleMode, isCometNativeShuffleMode, 
isCometOperatorEnabled, isCometScan, isCometScanEnabled, isCometShuffleEnabled, 
isSchemaSupported, isSpark34Plus, isSpark40Plus, shouldApplyRowToColumnar, 
withInfo, withInfos}
+import org.apache.comet.CometSparkSessionExtensions.{createMessage, 
getCometBroadcastNotEnabledReason, getCometShuffleNotEnabledReason, 
isANSIEnabled, isCometBroadCastForceEnabled, isCometEnabled, 
isCometExecEnabled, isCometJVMShuffleMode, isCometNativeShuffleMode, 
isCometOperatorEnabled, isCometScan, isCometScanEnabled, isCometShuffleEnabled, 
isSpark34Plus, isSpark40Plus, shouldApplyRowToColumnar, withInfo, withInfos}
 import org.apache.comet.parquet.{CometParquetScan, SupportsComet}
 import org.apache.comet.serde.OperatorOuterClass.Operator
 import org.apache.comet.serde.QueryPlanSerde
@@ -95,8 +94,10 @@ class CometSparkSessionExtensions
           // data source V2
           case scanExec: BatchScanExec
               if scanExec.scan.isInstanceOf[ParquetScan] &&
-                
isSchemaSupported(scanExec.scan.asInstanceOf[ParquetScan].readDataSchema) &&
-                
isSchemaSupported(scanExec.scan.asInstanceOf[ParquetScan].readPartitionSchema) 
&&
+                CometBatchScanExec.isSchemaSupported(
+                  scanExec.scan.asInstanceOf[ParquetScan].readDataSchema) &&
+                CometBatchScanExec.isSchemaSupported(
+                  scanExec.scan.asInstanceOf[ParquetScan].readPartitionSchema) 
&&
                 // Comet does not support pushedAggregate
                 
scanExec.scan.asInstanceOf[ParquetScan].pushedAggregate.isEmpty =>
             val cometScan = 
CometParquetScan(scanExec.scan.asInstanceOf[ParquetScan])
@@ -110,11 +111,11 @@ class CometSparkSessionExtensions
           case scanExec: BatchScanExec if 
scanExec.scan.isInstanceOf[ParquetScan] =>
             val requiredSchema = 
scanExec.scan.asInstanceOf[ParquetScan].readDataSchema
             val info1 = createMessage(
-              !isSchemaSupported(requiredSchema),
+              !CometBatchScanExec.isSchemaSupported(requiredSchema),
               s"Schema $requiredSchema is not supported")
             val readPartitionSchema = 
scanExec.scan.asInstanceOf[ParquetScan].readPartitionSchema
             val info2 = createMessage(
-              !isSchemaSupported(readPartitionSchema),
+              !CometBatchScanExec.isSchemaSupported(readPartitionSchema),
               s"Partition schema $readPartitionSchema is not supported")
             // Comet does not support pushedAggregate
             val info3 = createMessage(
@@ -129,7 +130,7 @@ class CometSparkSessionExtensions
               // Iceberg scan, supported cases
               case s: SupportsComet
                   if s.isCometEnabled &&
-                    isSchemaSupported(scanExec.scan.readSchema()) =>
+                    
CometBatchScanExec.isSchemaSupported(scanExec.scan.readSchema()) =>
                 logInfo(s"Comet extension enabled for 
${scanExec.scan.getClass.getSimpleName}")
                 // When reading from Iceberg, we automatically enable type 
promotion
                 SQLConf.get.setConfString(COMET_SCHEMA_EVOLUTION_ENABLED.key, 
"true")
@@ -144,7 +145,7 @@ class CometSparkSessionExtensions
                   "Comet extension is not enabled for " +
                     s"${scanExec.scan.getClass.getSimpleName}: not enabled on 
data source side")
                 val info2 = createMessage(
-                  !isSchemaSupported(scanExec.scan.readSchema()),
+                  
!CometBatchScanExec.isSchemaSupported(scanExec.scan.readSchema()),
                   "Comet extension is not enabled for " +
                     s"${scanExec.scan.getClass.getSimpleName}: Schema not 
supported")
                 withInfos(scanExec, Seq(info1, info2).flatten.toSet)
@@ -166,7 +167,9 @@ class CometSparkSessionExtensions
                 _,
                 _,
                 _,
-                _) if isSchemaSupported(requiredSchema) && 
isSchemaSupported(partitionSchema) =>
+                _)
+              if CometScanExec.isSchemaSupported(requiredSchema)
+                && CometScanExec.isSchemaSupported(partitionSchema) =>
             logInfo("Comet extension enabled for v1 Scan")
             CometScanExec(scanExec, session)
 
@@ -182,10 +185,10 @@ class CometSparkSessionExtensions
                 _,
                 _) =>
             val info1 = createMessage(
-              !isSchemaSupported(requiredSchema),
+              !CometScanExec.isSchemaSupported(requiredSchema),
               s"Schema $requiredSchema is not supported")
             val info2 = createMessage(
-              !isSchemaSupported(partitionSchema),
+              !CometScanExec.isSchemaSupported(partitionSchema),
               s"Partition schema $partitionSchema is not supported")
             withInfo(scanExec, Seq(info1, info2).flatten.mkString(","))
             scanExec
@@ -1109,28 +1112,17 @@ object CometSparkSessionExtensions extends Logging {
     COMET_EXEC_ALL_OPERATOR_ENABLED.get(conf)
   }
 
-  private[comet] def isSchemaSupported(schema: StructType): Boolean =
-    schema.map(_.dataType).forall(isTypeSupported)
-
-  private[comet] def isTypeSupported(dt: DataType): Boolean = dt match {
-    case BooleanType | ByteType | ShortType | IntegerType | LongType | 
FloatType | DoubleType |
-        BinaryType | StringType | _: DecimalType | DateType | TimestampType =>
-      true
-    case t: DataType if t.typeName == "timestamp_ntz" => true
-    case dt =>
-      logInfo(s"Comet extension is disabled because data type $dt is not 
supported")
-      false
-  }
-
   def isCometScan(op: SparkPlan): Boolean = {
     op.isInstanceOf[CometBatchScanExec] || op.isInstanceOf[CometScanExec]
   }
 
   private def shouldApplyRowToColumnar(conf: SQLConf, op: SparkPlan): Boolean 
= {
     // Only consider converting leaf nodes to columnar currently, so that all 
the following
-    // operators can have a chance to be converted to columnar.
+    // operators can have a chance to be converted to columnar. Leaf operators 
that output
+    // columnar batches, such as Spark's vectorized readers, will also be 
converted to native
+    // comet batches.
     // TODO: consider converting other intermediate operators to columnar.
-    op.isInstanceOf[LeafExecNode] && !op.supportsColumnar && 
isSchemaSupported(op.schema) &&
+    op.isInstanceOf[LeafExecNode] && 
CometRowToColumnarExec.isSchemaSupported(op.schema) &&
     COMET_ROW_TO_COLUMNAR_ENABLED.get(conf) && {
       val simpleClassName = Utils.getSimpleName(op.getClass)
       val nodeName = simpleClassName.replaceAll("Exec$", "")
diff --git a/spark/src/main/scala/org/apache/comet/DataTypeSupport.scala 
b/spark/src/main/scala/org/apache/comet/DataTypeSupport.scala
new file mode 100644
index 00000000..c49a2c46
--- /dev/null
+++ b/spark/src/main/scala/org/apache/comet/DataTypeSupport.scala
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package org.apache.comet
+
+import org.apache.spark.sql.types._
+
+trait DataTypeSupport {
+
+  /**
+   * This can be overridden to provide support for specific data types that 
aren't universally
+   * supported. This will only be called for non-universally supported types.
+   *
+   * @param dt
+   *   the data type to check
+   * @return
+   *   true if the datatype is supported
+   */
+  def isAdditionallySupported(dt: DataType): Boolean = false
+
+  private def isGloballySupported(dt: DataType): Boolean = dt match {
+    case BooleanType | ByteType | ShortType | IntegerType | LongType | 
FloatType | DoubleType |
+        BinaryType | StringType | _: DecimalType | DateType | TimestampType =>
+      true
+    case t: DataType if t.typeName == "timestamp_ntz" => true
+    case _ => false
+  }
+
+  /**
+   * Checks if this schema is supported by checking if each field in the 
struct is supported.
+   *
+   * @param struct
+   *   the struct to check the fields of
+   * @return
+   *   true if all fields in the struct are supported
+   */
+  def isSchemaSupported(struct: StructType): Boolean = {
+    struct.fields.map(_.dataType).forall(isTypeSupported)
+  }
+
+  def isTypeSupported(dt: DataType): Boolean = {
+    if (isGloballySupported(dt) || isAdditionallySupported(dt)) {
+      // If complex types are supported, we additionally want to recurse into 
their children
+      dt match {
+        case StructType(fields) => 
fields.map(_.dataType).forall(isTypeSupported)
+        case ArrayType(elementType, _) => isTypeSupported(elementType)
+        case MapType(keyType, valueType, _) =>
+          isTypeSupported(keyType) && isTypeSupported(valueType)
+        // Not a complex type
+        case _ => true
+      }
+    } else {
+      false
+    }
+  }
+}
diff --git a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala 
b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
index 3fbe6d74..98ca1404 100644
--- a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
@@ -59,12 +59,13 @@ object QueryPlanSerde extends Logging with 
ShimQueryPlanSerde with CometExprShim
     logWarning(s"Comet native execution is disabled due to: $reason")
   }
 
-  def supportedDataType(dt: DataType): Boolean = dt match {
+  def supportedDataType(dt: DataType, allowStruct: Boolean = false): Boolean = 
dt match {
     case _: ByteType | _: ShortType | _: IntegerType | _: LongType | _: 
FloatType |
         _: DoubleType | _: StringType | _: BinaryType | _: TimestampType | _: 
DecimalType |
         _: DateType | _: BooleanType | _: NullType =>
       true
     case dt if isTimestampNTZType(dt) => true
+    case s: StructType if allowStruct => 
s.fields.map(_.dataType).forall(supportedDataType(_))
     case dt =>
       emitWarning(s"unsupported Spark data type: $dt")
       false
@@ -2199,7 +2200,7 @@ object QueryPlanSerde extends Logging with 
ShimQueryPlanSerde with CometExprShim
             
ExprOuterClass.Expr.newBuilder().setNormalizeNanAndZero(builder).build()
           }
 
-        case s @ execution.ScalarSubquery(_, _) =>
+        case s @ execution.ScalarSubquery(_, _) if 
supportedDataType(s.dataType) =>
           val dataType = serializeDataType(s.dataType)
           if (dataType.isEmpty) {
             withInfo(s, s"Scalar subquery returns unsupported datatype 
${s.dataType}")
@@ -2316,6 +2317,19 @@ object QueryPlanSerde extends Logging with 
ShimQueryPlanSerde with CometExprShim
             None
           }
 
+        case GetStructField(child, ordinal, _) =>
+          exprToProto(child, inputs, binding).map { childExpr =>
+            val getStructFieldBuilder = ExprOuterClass.GetStructField
+              .newBuilder()
+              .setChild(childExpr)
+              .setOrdinal(ordinal)
+
+            ExprOuterClass.Expr
+              .newBuilder()
+              .setGetStructField(getStructFieldBuilder)
+              .build()
+          }
+
         case _ =>
           withInfo(expr, s"${expr.prettyName} is not supported", 
expr.children: _*)
           None
@@ -2819,7 +2833,7 @@ object QueryPlanSerde extends Logging with 
ShimQueryPlanSerde with CometExprShim
         withInfo(join, "SortMergeJoin is not enabled")
         None
 
-      case op if isCometSink(op) && op.output.forall(a => 
supportedDataType(a.dataType)) =>
+      case op if isCometSink(op) && op.output.forall(a => 
supportedDataType(a.dataType, true)) =>
         // These operators are source of Comet native execution chain
         val scanBuilder = OperatorOuterClass.Scan.newBuilder()
 
diff --git 
a/spark/src/main/scala/org/apache/spark/sql/comet/CometBatchScanExec.scala 
b/spark/src/main/scala/org/apache/spark/sql/comet/CometBatchScanExec.scala
index 82ebed95..6c21b93e 100644
--- a/spark/src/main/scala/org/apache/spark/sql/comet/CometBatchScanExec.scala
+++ b/spark/src/main/scala/org/apache/spark/sql/comet/CometBatchScanExec.scala
@@ -34,7 +34,7 @@ import org.apache.spark.sql.vectorized._
 
 import com.google.common.base.Objects
 
-import org.apache.comet.MetricsSupport
+import org.apache.comet.{DataTypeSupport, MetricsSupport}
 import org.apache.comet.shims.ShimCometBatchScanExec
 
 case class CometBatchScanExec(wrapped: BatchScanExec, runtimeFilters: 
Seq[Expression])
@@ -152,3 +152,5 @@ case class CometBatchScanExec(wrapped: BatchScanExec, 
runtimeFilters: Seq[Expres
 
   override def supportsColumnar: Boolean = true
 }
+
+object CometBatchScanExec extends DataTypeSupport
diff --git 
a/spark/src/main/scala/org/apache/spark/sql/comet/CometRowToColumnarExec.scala 
b/spark/src/main/scala/org/apache/spark/sql/comet/CometRowToColumnarExec.scala
index 5679e865..b8a5329c 100644
--- 
a/spark/src/main/scala/org/apache/spark/sql/comet/CometRowToColumnarExec.scala
+++ 
b/spark/src/main/scala/org/apache/spark/sql/comet/CometRowToColumnarExec.scala
@@ -19,6 +19,8 @@
 
 package org.apache.spark.sql.comet
 
+import scala.collection.JavaConverters._
+
 import org.apache.spark.TaskContext
 import org.apache.spark.broadcast.Broadcast
 import org.apache.spark.rdd.RDD
@@ -28,8 +30,11 @@ import 
org.apache.spark.sql.catalyst.plans.physical.Partitioning
 import org.apache.spark.sql.comet.execution.arrow.CometArrowConverters
 import org.apache.spark.sql.execution.{RowToColumnarTransition, SparkPlan}
 import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
+import org.apache.spark.sql.types._
 import org.apache.spark.sql.vectorized.ColumnarBatch
 
+import org.apache.comet.DataTypeSupport
+
 case class CometRowToColumnarExec(child: SparkPlan)
     extends RowToColumnarTransition
     with CometPlan {
@@ -60,8 +65,17 @@ case class CometRowToColumnarExec(child: SparkPlan)
     val timeZoneId = conf.sessionLocalTimeZone
     val schema = child.schema
 
-    child
-      .execute()
+    val rdd: RDD[InternalRow] = if (child.supportsColumnar) {
+      child
+        .executeColumnar()
+        .mapPartitionsInternal { iter =>
+          iter.flatMap(_.rowIterator().asScala)
+        }
+    } else {
+      child.execute()
+    }
+
+    rdd
       .mapPartitionsInternal { iter =>
         val context = TaskContext.get()
         CometArrowConverters.toArrowBatchIterator(
@@ -82,3 +96,10 @@ case class CometRowToColumnarExec(child: SparkPlan)
     copy(child = newChild)
 
 }
+
+object CometRowToColumnarExec extends DataTypeSupport {
+  override def isAdditionallySupported(dt: DataType): Boolean = dt match {
+    case _: StructType => true
+    case _ => false
+  }
+}
diff --git 
a/spark/src/main/scala/org/apache/spark/sql/comet/CometScanExec.scala 
b/spark/src/main/scala/org/apache/spark/sql/comet/CometScanExec.scala
index adaefb23..ce040f9f 100644
--- a/spark/src/main/scala/org/apache/spark/sql/comet/CometScanExec.scala
+++ b/spark/src/main/scala/org/apache/spark/sql/comet/CometScanExec.scala
@@ -43,7 +43,7 @@ import org.apache.spark.sql.vectorized.ColumnarBatch
 import org.apache.spark.util.SerializableConfiguration
 import org.apache.spark.util.collection._
 
-import org.apache.comet.{CometConf, MetricsSupport}
+import org.apache.comet.{CometConf, DataTypeSupport, MetricsSupport}
 import org.apache.comet.parquet.{CometParquetFileFormat, 
CometParquetPartitionReaderFactory}
 
 /**
@@ -439,7 +439,7 @@ case class CometScanExec(
   }
 }
 
-object CometScanExec {
+object CometScanExec extends DataTypeSupport {
   def apply(scanExec: FileSourceScanExec, session: SparkSession): 
CometScanExec = {
     // TreeNode.mapProductIterator is protected method.
     def mapProductIterator[B: ClassTag](product: Product, f: Any => B): 
Array[B] = {
diff --git a/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala 
b/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala
index f169538a..27c046d7 100644
--- a/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala
@@ -28,7 +28,7 @@ import scala.util.Random
 import org.apache.hadoop.fs.Path
 import org.apache.spark.sql.{CometTestBase, DataFrame, Row}
 import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
-import org.apache.spark.sql.functions.expr
+import org.apache.spark.sql.functions._
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.internal.SQLConf.SESSION_LOCAL_TIMEZONE
 import org.apache.spark.sql.types.{Decimal, DecimalType}
@@ -1903,4 +1903,23 @@ class CometExpressionSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
       }
     }
   }
+
+  test("get_struct_field") {
+    withSQLConf(
+      CometConf.COMET_ROW_TO_COLUMNAR_ENABLED.key -> "true",
+      CometConf.COMET_ROW_TO_COLUMNAR_SUPPORTED_OPERATOR_LIST.key -> 
"FileSourceScan") {
+      withTempPath { dir =>
+        var df = spark
+          .range(5)
+          // Add both a null struct and null inner value
+          .select(when(col("id") > 1, struct(when(col("id") > 2, 
col("id")).alias("id")))
+            .alias("nested"))
+
+        df.write.parquet(dir.toString())
+
+        df = spark.read.parquet(dir.toString())
+        checkSparkAnswerAndOperator(df.select("nested.id"))
+      }
+    }
+  }
 }
diff --git 
a/spark/src/test/scala/org/apache/comet/CometSparkSessionExtensionsSuite.scala 
b/spark/src/test/scala/org/apache/comet/CometSparkSessionExtensionsSuite.scala
index fdcd307b..516fb09f 100644
--- 
a/spark/src/test/scala/org/apache/comet/CometSparkSessionExtensionsSuite.scala
+++ 
b/spark/src/test/scala/org/apache/comet/CometSparkSessionExtensionsSuite.scala
@@ -22,45 +22,8 @@ package org.apache.comet
 import org.apache.spark.SparkConf
 import org.apache.spark.sql._
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types._
 
 class CometSparkSessionExtensionsSuite extends CometTestBase {
-  test("unsupported Spark types") {
-    Seq(
-      NullType -> false,
-      BooleanType -> true,
-      ByteType -> true,
-      ShortType -> true,
-      IntegerType -> true,
-      LongType -> true,
-      FloatType -> true,
-      DoubleType -> true,
-      BinaryType -> true,
-      StringType -> true,
-      ArrayType(TimestampType) -> false,
-      StructType(
-        Seq(
-          StructField("f1", DecimalType.SYSTEM_DEFAULT),
-          StructField("f2", StringType))) -> false,
-      MapType(keyType = LongType, valueType = DateType) -> false,
-      StructType(Seq(StructField("f1", ByteType), StructField("f2", 
StringType))) -> false,
-      MapType(keyType = IntegerType, valueType = BinaryType) -> false).foreach 
{
-      case (dt, expected) =>
-        assert(CometSparkSessionExtensions.isTypeSupported(dt) == expected)
-    }
-  }
-
-  test("unsupported Spark schema") {
-    Seq(
-      Seq(StructField("f1", IntegerType), StructField("f2", BooleanType)) -> 
true,
-      Seq(StructField("f1", IntegerType), StructField("f2", 
ArrayType(IntegerType))) -> false,
-      Seq(
-        StructField("f1", MapType(keyType = LongType, valueType = StringType)),
-        StructField("f2", ArrayType(DoubleType))) -> false).foreach { case 
(schema, expected) =>
-      assert(CometSparkSessionExtensions.isSchemaSupported(StructType(schema)) 
== expected)
-    }
-  }
-
   test("isCometEnabled") {
     val conf = new SQLConf
 
diff --git 
a/spark/src/test/scala/org/apache/comet/parquet/ParquetReadSuite.scala 
b/spark/src/test/scala/org/apache/comet/parquet/ParquetReadSuite.scala
index 9e6cbc0a..3c5ed16c 100644
--- a/spark/src/test/scala/org/apache/comet/parquet/ParquetReadSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/parquet/ParquetReadSuite.scala
@@ -82,6 +82,44 @@ abstract class ParquetReadSuite extends CometTestBase {
     }
   }
 
+  test("unsupported Spark types") {
+    Seq(
+      NullType -> false,
+      BooleanType -> true,
+      ByteType -> true,
+      ShortType -> true,
+      IntegerType -> true,
+      LongType -> true,
+      FloatType -> true,
+      DoubleType -> true,
+      BinaryType -> true,
+      StringType -> true,
+      ArrayType(TimestampType) -> false,
+      StructType(
+        Seq(
+          StructField("f1", DecimalType.SYSTEM_DEFAULT),
+          StructField("f2", StringType))) -> false,
+      MapType(keyType = LongType, valueType = DateType) -> false,
+      StructType(Seq(StructField("f1", ByteType), StructField("f2", 
StringType))) -> false,
+      MapType(keyType = IntegerType, valueType = BinaryType) -> false).foreach 
{
+      case (dt, expected) =>
+        assert(CometScanExec.isTypeSupported(dt) == expected)
+        assert(CometBatchScanExec.isTypeSupported(dt) == expected)
+    }
+  }
+
+  test("unsupported Spark schema") {
+    Seq(
+      Seq(StructField("f1", IntegerType), StructField("f2", BooleanType)) -> 
true,
+      Seq(StructField("f1", IntegerType), StructField("f2", 
ArrayType(IntegerType))) -> false,
+      Seq(
+        StructField("f1", MapType(keyType = LongType, valueType = StringType)),
+        StructField("f2", ArrayType(DoubleType))) -> false).foreach { case 
(schema, expected) =>
+      assert(CometScanExec.isSchemaSupported(StructType(schema)) == expected)
+      assert(CometBatchScanExec.isSchemaSupported(StructType(schema)) == 
expected)
+    }
+  }
+
   test("simple count") {
     withParquetTable((0 until 10).map(i => (i, i.toString)), "tbl") {
       assert(sql("SELECT * FROM tbl WHERE _1 % 2 == 0").count() == 5)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to