comphead commented on code in PR #6726:
URL: https://github.com/apache/datafusion-comet/pull/6726#discussion_r4459279857


##########
spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala:
##########
@@ -4558,43 +4558,263 @@ class CometExecSuite extends CometTestBase {
     })
   }
 
-  test("SparkToColumnar admits only binary string arrays and maps through the 
collection gate") {
-    for (nullable <- Seq(false, true); containsNull <- Seq(false, true)) {
-      for (dataType <- Seq(
-          ArrayType(StringType, containsNull),
-          MapType(StringType, StringType, containsNull))) {
-        val field = StructField("tags", dataType, nullable)
-        Seq(
-          StructType(Seq(field)),
-          StructType(Seq(StructField("nested", StructType(Seq(field))))))
-          .foreach { schema =>
-            assert(CometSparkToColumnarExec.isSchemaSupported(schema, 
ListBuffer.empty))
-          }
-      }
-    }
-    val unsupported = Seq(
+  test("SparkToColumnar admits arrays and maps of every type it converts") {
+    val struct = StructType(Seq(StructField("i", IntegerType), 
StructField("s", StringType)))
+    val admitted = Seq(
+      ArrayType(StringType),
       ArrayType(IntegerType),
+      ArrayType(DecimalType(38, 10)),
+      ArrayType(TimestampType),
       ArrayType(BinaryType),
       ArrayType(ArrayType(StringType)),
-      ArrayType(StructType(Seq(StructField("s", StringType)))),
-      MapType(StringType, IntegerType),
-      MapType(IntegerType, StringType),
-      MapType(StringType, ArrayType(StringType)),
-      MapType(StringType, StructType(Seq(StructField("s", StringType))))) ++
-      (if (isSpark40Plus)
+      ArrayType(struct),
+      ArrayType(MapType(StringType, IntegerType)),
+      MapType(StringType, StringType),
+      MapType(IntegerType, IntegerType),
+      MapType(DateType, DecimalType(20, 2)),
+      MapType(StringType, ArrayType(IntegerType)),
+      MapType(IntegerType, struct),
+      MapType(struct, StringType),
+      MapType(ArrayType(IntegerType), IntegerType),
+      MapType(StringType, MapType(IntegerType, StringType)))
+    for (nullable <- Seq(false, true); containsNull <- Seq(false, true); 
dataType <- admitted) {
+      val collection = dataType match {
+        case a: ArrayType => a.copy(containsNull = containsNull)
+        case m: MapType => m.copy(valueContainsNull = containsNull)
+      }
+      val field = StructField("c", collection, nullable)
+      Seq(StructType(Seq(field)), StructType(Seq(StructField("nested", 
StructType(Seq(field))))))
+        .foreach { schema =>
+          val reasons = ListBuffer.empty[String]
+          assert(CometSparkToColumnarExec.isSchemaSupported(schema, reasons), 
s"$collection")
+          assert(reasons.isEmpty, reasons)
+        }
+    }
+
+    // The shared rule looks inside a collection and declines these for what 
it finds there, and
+    // so does the conversion for a calendar interval. A decline of the whole 
collection would
+    // record no reason, which is how the two differ.
+    val duplicate = StructType(Seq(StructField("a", LongType), 
StructField("a", LongType)))
+    val declined = Seq(
+      ArrayType(duplicate),
+      MapType(LongType, duplicate),
+      ArrayType(ArrayType(duplicate)),
+      ArrayType(NullType),
+      MapType(IntegerType, ArrayType(NullType)),
+      ArrayType(DayTimeIntervalType()),
+      MapType(StringType, YearMonthIntervalType()),
+      ArrayType(CalendarIntervalType),
+      MapType(StringType, CalendarIntervalType)) ++
+      (if (isSpark40Plus) {
          Seq(
-           DataType.fromDDL("ARRAY<STRING COLLATE UTF8_LCASE>"),
-           DataType.fromDDL("MAP<STRING COLLATE UTF8_LCASE,STRING>"),
-           DataType.fromDDL("MAP<STRING,STRING COLLATE UTF8_LCASE>"))
-       else Seq.empty)
-    unsupported.foreach { dataType =>
+           "ARRAY<STRING COLLATE UTF8_LCASE>",
+           "ARRAY<ARRAY<STRING COLLATE UTF8_LCASE>>",
+           "ARRAY<STRUCT<s: STRING COLLATE UTF8_LCASE>>",
+           "MAP<STRING COLLATE UTF8_LCASE,STRING>",
+           "MAP<STRING,STRING COLLATE UTF8_LCASE>",
+           "MAP<INT,ARRAY<STRING COLLATE UTF8_LCASE>>").map(DataType.fromDDL)
+       } else Seq.empty)
+    declined.foreach { dataType =>
       val schema = StructType(Seq(StructField("value", dataType)))
-      assert(!CometSparkToColumnarExec.isSchemaSupported(schema, 
ListBuffer.empty), dataType)
-      assert(
-        !CometSparkToColumnarExec.isSchemaSupported(
-          StructType(Seq(StructField("nested", schema))),
-          ListBuffer.empty),
-        dataType)
+      Seq(schema, StructType(Seq(StructField("nested", schema)))).foreach { 
declinedSchema =>
+        val reasons = ListBuffer.empty[String]
+        assert(!CometSparkToColumnarExec.isSchemaSupported(declinedSchema, 
reasons), dataType)
+        assert(reasons.nonEmpty, s"$dataType was declined without a reason")
+      }
+    }
+
+    // The shared rule admits a calendar interval, but Arrow holds the time 
part of one in
+    // nanoseconds, so the conversion declines it at the top level too (#5279).
+    val reasons = ListBuffer.empty[String]
+    val interval = StructType(Seq(StructField("i", CalendarIntervalType)))
+    assert(!CometSparkToColumnarExec.isSchemaSupported(interval, reasons))
+    assert(reasons.exists(_.contains("issues/5279")), reasons)
+  }
+
+  // A column of each array and map shape the conversion admits, as its type, 
an expression over
+  // `id` that builds it, and one that reads an element of it. The column has, 
in some rows, a
+  // null collection, an empty one, and elements that are null.
+  private val sparkToColumnarShapes: Seq[(String, String, String)] = {
+    val positions = "sequence(0, cast(id % 4 AS INT))"
+    def collection(ddl: String, empty: String, body: String): String =
+      "cast(if(id % 7 = 0, null, " +
+        s"if(id % 7 = 1, cast($empty AS $ddl), cast($body AS $ddl))) AS $ddl)"
+    def array(ddl: String, element: String): (String, String, String) =
+      (
+        ddl,
+        collection(
+          ddl,
+          "array()",
+          s"transform($positions, x -> if((x + id) % 5 = 4, null, $element))"),
+        "if(size(c) > 0, c[0], null)")
+    def map(ddl: String, key: String, value: String): (String, String, String) 
=
+      (
+        ddl,
+        collection(
+          ddl,
+          "map()",
+          s"map_from_arrays(transform($positions, x -> $key), " +
+            s"transform($positions, x -> if((x + id) % 5 = 4, null, 
$value)))"),
+        "if(size(c) > 0, element_at(map_values(c), 1), null)")
+    val struct = "named_struct('i', cast(id + x AS INT), 's', concat('s', 
cast(x AS STRING)))"
+    Seq(
+      array("ARRAY<INT>", "cast(id * 10 + x AS INT)"),
+      array(
+        "ARRAY<DECIMAL(38,10)>",
+        "cast(concat(if(x % 2 = 0, '-', ''), cast(id AS STRING), 
'12345678901234567.0123456789') " +
+          "AS DECIMAL(38,10))"),
+      array("ARRAY<TIMESTAMP>", "timestamp_micros(1700000000000000 + id * 
1000003 + x)"),
+      array(
+        "ARRAY<BINARY>",
+        "cast(concat('b', cast(id AS STRING), cast(x AS STRING)) AS BINARY)"),
+      array("ARRAY<DOUBLE>", "cast(id AS DOUBLE) / 3 + x"),
+      array("ARRAY<BOOLEAN>", "(id + x) % 3 = 0"),
+      array(
+        "ARRAY<ARRAY<INT>>",
+        "if((x + id) % 4 = 3, null, if((x + id) % 4 = 2, cast(array() AS 
ARRAY<INT>), " +
+          "transform(sequence(0, cast(x % 3 AS INT)), y -> cast(id + x + y AS 
INT))))"),
+      array("ARRAY<STRUCT<i: INT, s: STRING>>", struct),
+      array("ARRAY<MAP<STRING,INT>>", "map(concat('k', cast(x AS STRING)), 
cast(id AS INT))"),
+      map("MAP<INT,INT>", "x", "cast(id * 10 + x AS INT)"),
+      map(
+        "MAP<DATE,DECIMAL(20,2)>",
+        "date_add(DATE'2024-01-01', x)",
+        "cast((id * 1000003 + x) / 100 AS DECIMAL(20,2))"),
+      map(
+        "MAP<DECIMAL(20,2),STRING>",
+        "cast(concat(cast(x AS STRING), '.25') AS DECIMAL(20,2))",
+        "concat('v', cast(id AS STRING), '_', cast(x AS STRING))"),
+      map(
+        "MAP<STRING,ARRAY<INT>>",
+        "concat('k', cast(x AS STRING))",
+        "transform(sequence(0, x), y -> cast(id + y AS INT))"),
+      map("MAP<INT,STRUCT<i: INT, s: STRING>>", "x", struct),
+      map(
+        "MAP<STRUCT<i: INT, s: STRING>,STRING>",
+        "named_struct('i', x, 's', concat('k', cast(x AS STRING)))",
+        "concat('v', cast(id AS STRING))"),
+      map("MAP<ARRAY<INT>,INT>", "array(x, x + 1)", "cast(id + x AS INT)"),
+      map(
+        "MAP<STRING,MAP<INT,STRING>>",
+        "concat('k', cast(x AS STRING))",
+        "map_from_arrays(array(x, x + 1), array('a', concat('b', cast(id AS 
STRING))))"))
+  }
+
+  sparkToColumnarShapes.foreach { case (ddl, expression, element) =>

Review Comment:
   The body of this loop repeats the harness of `SparkToColumnar string maps 
cross RDD and Parquet native boundaries` further down (the `rdd`, `parquet-row` 
and `parquet-columnar` loop, the same confs, the `CometSparkToColumnarExec` 
count, `checkSparkSchema`, the native shuffle check, `limit(1)`). Could that 
harness become a helper that takes a schema, rows and a query, so the two 
string tests and the 17 shapes share it? The string-array and string-map tests 
would then differ only in their extra checks, and `ARRAY<STRING>` and 
`MAP<STRING,STRING>` would not be the only shapes with their own copy of the 
loop.



##########
spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala:
##########
@@ -4558,43 +4558,263 @@ class CometExecSuite extends CometTestBase {
     })
   }
 
-  test("SparkToColumnar admits only binary string arrays and maps through the 
collection gate") {
-    for (nullable <- Seq(false, true); containsNull <- Seq(false, true)) {
-      for (dataType <- Seq(
-          ArrayType(StringType, containsNull),
-          MapType(StringType, StringType, containsNull))) {
-        val field = StructField("tags", dataType, nullable)
-        Seq(
-          StructType(Seq(field)),
-          StructType(Seq(StructField("nested", StructType(Seq(field))))))
-          .foreach { schema =>
-            assert(CometSparkToColumnarExec.isSchemaSupported(schema, 
ListBuffer.empty))
-          }
-      }
-    }
-    val unsupported = Seq(
+  test("SparkToColumnar admits arrays and maps of every type it converts") {
+    val struct = StructType(Seq(StructField("i", IntegerType), 
StructField("s", StringType)))
+    val admitted = Seq(
+      ArrayType(StringType),
       ArrayType(IntegerType),
+      ArrayType(DecimalType(38, 10)),
+      ArrayType(TimestampType),
       ArrayType(BinaryType),
       ArrayType(ArrayType(StringType)),
-      ArrayType(StructType(Seq(StructField("s", StringType)))),
-      MapType(StringType, IntegerType),
-      MapType(IntegerType, StringType),
-      MapType(StringType, ArrayType(StringType)),
-      MapType(StringType, StructType(Seq(StructField("s", StringType))))) ++
-      (if (isSpark40Plus)
+      ArrayType(struct),
+      ArrayType(MapType(StringType, IntegerType)),
+      MapType(StringType, StringType),
+      MapType(IntegerType, IntegerType),
+      MapType(DateType, DecimalType(20, 2)),
+      MapType(StringType, ArrayType(IntegerType)),
+      MapType(IntegerType, struct),
+      MapType(struct, StringType),
+      MapType(ArrayType(IntegerType), IntegerType),
+      MapType(StringType, MapType(IntegerType, StringType)))
+    for (nullable <- Seq(false, true); containsNull <- Seq(false, true); 
dataType <- admitted) {
+      val collection = dataType match {
+        case a: ArrayType => a.copy(containsNull = containsNull)
+        case m: MapType => m.copy(valueContainsNull = containsNull)
+      }
+      val field = StructField("c", collection, nullable)
+      Seq(StructType(Seq(field)), StructType(Seq(StructField("nested", 
StructType(Seq(field))))))
+        .foreach { schema =>
+          val reasons = ListBuffer.empty[String]
+          assert(CometSparkToColumnarExec.isSchemaSupported(schema, reasons), 
s"$collection")
+          assert(reasons.isEmpty, reasons)
+        }
+    }
+
+    // The shared rule looks inside a collection and declines these for what 
it finds there, and
+    // so does the conversion for a calendar interval. A decline of the whole 
collection would
+    // record no reason, which is how the two differ.
+    val duplicate = StructType(Seq(StructField("a", LongType), 
StructField("a", LongType)))

Review Comment:
   The three entries here that nest `duplicate` (`ArrayType(duplicate)`, 
`MapType(LongType, duplicate)`, `ArrayType(ArrayType(duplicate))`) overlap with 
the `CometNativeShuffleSuite` change in this PR, which now asserts the same two 
shapes against `CometSparkToColumnarExec` and also checks the `struct with 
duplicate field names` reason. This test only checks that some reason was 
recorded. Could the duplicate-struct entries live in one of the two places?



##########
spark/src/test/scala/org/apache/comet/exec/CometShuffleInputConversionSuite.scala:
##########
@@ -313,32 +329,37 @@ class CometShuffleInputConversionSuite extends 
CometTestBase {
         val (_, placed) = checkSparkAnswer(
           keys.repartition(10, col("k")).select(col("k"), col("v"), 
spark_partition_id()))
         assert(convertedShuffles(placed).length == 1, placed)
-        // The left input converts and the right one, with its array<bigint> 
column, does not, so
+        // The left input converts and the right one, with its NullType 
column, does not, so
         // matching keys meet only if both shuffles put them in the same 
partition.
         val left = keys.select(col("k").as("lk"), col("v"))
-        val right = keys.select(col("k").as("rk"), array(col("v")).as("xs"))
+        val right = keys.select(col("k").as("rk"), col("n"))
         val (_, joined) = checkSparkAnswer(left.join(right, col("lk") === 
col("rk")))
         assert(convertedShuffles(joined).length == 1, joined)
         assert(cometShuffles(joined).count(_.shuffleType == 
CometColumnarShuffle) == 1, joined)
       }
     }
   }
 
-  convertTest("a converted wide-decimal shuffle joins the JVM columnar 
shuffle") {
-    withShuffledJoins {
-      Seq("true", "false").foreach { aqe =>
-        withSQLConf(
-          SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe,
-          SQLConf.COALESCE_PARTITIONS_ENABLED.key -> "false",
-          SQLConf.SKEW_JOIN_ENABLED.key -> "false") {
-          val left = rowsDf(100).select(col("k").cast(DecimalType(38, 
0)).as("lk"), col("l"))
-          // The array column keeps the right input on the JVM columnar 
shuffle.
-          val right = arraysDf().select(col("k").cast(DecimalType(38, 
0)).as("rk"), col("xs"))
-          val (_, plan) = checkSparkAnswer(left.join(right, col("lk") === 
col("rk")))
-          assert(convertedShuffles(plan).length == 1, s"AQE $aqe:\n$plan")
-          assert(
-            cometShuffles(plan).count(_.shuffleType == CometColumnarShuffle) 
== 1,
-            s"AQE $aqe:\n$plan")
+  Seq("xs", "m").foreach { column =>
+    convertTest(s"a converted wide-decimal shuffle joins the JVM columnar 
shuffle with $column") {

Review Comment:
   The `xs` and `m` variants differ only in the payload column of the converted 
side. The join key and both shuffles are the same, and the hash is over `lk`, 
so I do not expect the payload type to change what is being checked. Would one 
test that selects both `xs` and `m` from `collectionsDf()` cover the same 
ground with half of the joins (each runs with AQE on and off)?



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