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

andygrove 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 e36a410a2f feat: support size() for MapType input (#4580)
e36a410a2f is described below

commit e36a410a2fc3eb7d7c64270c6f401be7d7a23cad
Author: Zaki <[email protected]>
AuthorDate: Fri Jul 17 22:19:57 2026 +0800

    feat: support size() for MapType input (#4580)
---
 docs/source/user-guide/latest/expressions.md       |   4 +-
 native/spark-expr/src/array_funcs/size.rs          | 141 ++++++++++++++++-----
 .../main/scala/org/apache/comet/serde/arrays.scala |   6 +-
 .../sql-tests/expressions/array/posexplode.sql     |   4 +-
 .../resources/sql-tests/expressions/array/size.sql |  18 ++-
 .../org/apache/comet/CometMapExpressionSuite.scala |  51 +++++---
 6 files changed, 158 insertions(+), 66 deletions(-)

diff --git a/docs/source/user-guide/latest/expressions.md 
b/docs/source/user-guide/latest/expressions.md
index 4ad4d9afe3..f45fa21444 100644
--- a/docs/source/user-guide/latest/expressions.md
+++ b/docs/source/user-guide/latest/expressions.md
@@ -186,10 +186,10 @@ The tables below list every Spark built-in expression 
with its current status.
 | Function | Status | Notes |
 | --- | --- | --- |
 | `array_size` | ✅ |  |
-| `cardinality` | ✅ | MapType input falls back |
+| `cardinality` | ✅ |  |
 | `concat` | ✅ | Binary/array children fall back |
 | `reverse` | ✅ | Binary-element arrays fall back (Incompatible) 
([details](compatibility/expressions/array.md)) |
-| `size` | ✅ | MapType input falls back |
+| `size` | ✅ |  |
 
 ---
 
diff --git a/native/spark-expr/src/array_funcs/size.rs 
b/native/spark-expr/src/array_funcs/size.rs
index 9bed27b33b..5ad4f1670a 100644
--- a/native/spark-expr/src/array_funcs/size.rs
+++ b/native/spark-expr/src/array_funcs/size.rs
@@ -195,6 +195,14 @@ fn spark_size_scalar(scalar: &ScalarValue) -> 
Result<ScalarValue, DataFusionErro
                 Ok(ScalarValue::Int32(Some(len)))
             }
         }
+        ScalarValue::Map(array) => {
+            if array.is_null(0) {
+                Ok(ScalarValue::Int32(Some(-1)))
+            } else {
+                let len = array.value_length(0);
+                Ok(ScalarValue::Int32(Some(len)))
+            }
+        }
         ScalarValue::Null => {
             Ok(ScalarValue::Int32(Some(-1))) // Spark behavior: return -1 for 
null
         }
@@ -273,38 +281,28 @@ mod tests {
         assert_eq!(result, ScalarValue::Int32(Some(-1)));
     }
 
-    // TODO: Add map array test once Arrow MapArray API constraints are 
resolved
-    // Currently MapArray doesn't allow nulls in entries which makes testing 
complex
-    // The core size() implementation supports maps correctly
-    #[ignore]
     #[test]
     fn test_spark_size_map_array() {
-        use arrow::array::{MapArray, StringArray};
+        use arrow::array::{Int32Array, MapArray, StringArray};
 
-        // Create a simpler test with maps:
-        // [{"key1": "value1", "key2": "value2"}, {"key3": "value3"}, {}, null]
+        // Create test data: [{"key1": 1, "key2": 2}, {"key3": 3}, {}, null]
+        let keys = StringArray::from(vec![Some("key1"), Some("key2"), 
Some("key3")]);
+        let values = Int32Array::from(vec![Some(1), Some(2), Some(3)]);
 
-        // Create keys array for all entries (no nulls)
-        let keys = StringArray::from(vec!["key1", "key2", "key3"]);
-
-        // Create values array for all entries (no nulls)
-        let values = StringArray::from(vec!["value1", "value2", "value3"]);
-
-        // Create entry offsets: [0, 2, 3, 3] representing:
+        // Create entry offsets: [0, 2, 3, 3, 3] representing:
         // - Map 1: entries 0-1 (2 key-value pairs)
-        // - Map 2: entries 2-2 (1 key-value pair)
+        // - Map 2: entry 2 (1 key-value pair)
         // - Map 3: entries 3-2 (0 key-value pairs, empty map)
         // - Map 4: null (handled by null buffer)
-        let entry_offsets = arrow::buffer::OffsetBuffer::new(vec![0, 2, 3, 3, 
3].into());
+        let entry_offsets = arrow::buffer::OffsetBuffer::new(vec![0i32, 2, 3, 
3, 3].into());
 
         let key_field = Arc::new(Field::new("key", DataType::Utf8, false));
-        let value_field = Arc::new(Field::new("value", DataType::Utf8, 
false)); // Make values non-nullable too
+        let value_field = Arc::new(Field::new("value", DataType::Int32, true));
 
-        // Create the entries struct array
         let entries = arrow::array::StructArray::new(
             arrow::datatypes::Fields::from(vec![key_field, value_field]),
             vec![Arc::new(keys), Arc::new(values)],
-            None, // No nulls in the entries struct array itself
+            None,
         );
 
         // Create null buffer for the map array (fourth map is null)
@@ -314,27 +312,23 @@ mod tests {
         null_buffer.append(true); // Empty map - not null
         null_buffer.append(false); // null map
 
-        let map_data_type = DataType::Map(
-            Arc::new(Field::new(
-                "entries",
-                DataType::Struct(arrow::datatypes::Fields::from(vec![
-                    Field::new("key", DataType::Utf8, false),
-                    Field::new("value", DataType::Utf8, false), // Make values 
non-nullable too
-                ])),
-                false,
-            )),
-            false, // keys are not sorted
-        );
-
-        let map_field = Arc::new(Field::new("map", map_data_type, true));
+        let map_field = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(arrow::datatypes::Fields::from(vec![
+                Field::new("key", DataType::Utf8, false),
+                Field::new("value", DataType::Int32, true),
+            ])),
+            false,
+        ));
 
-        let map_array = MapArray::new(
+        let map_array = MapArray::try_new(
             map_field,
             entry_offsets,
             entries,
             null_buffer.finish(),
-            false, // keys are not sorted
-        );
+            false,
+        )
+        .unwrap();
 
         let array_ref: ArrayRef = Arc::new(map_array);
         let result = spark_size_array(&array_ref).unwrap();
@@ -347,6 +341,83 @@ mod tests {
         assert_eq!(result.value(3), -1); // null map returns -1
     }
 
+    #[test]
+    fn test_spark_size_scalar_map() {
+        use arrow::array::{Int32Array, MapArray, StringArray};
+
+        // Test non-null map with 2 entries
+        let keys = StringArray::from(vec![Some("a"), Some("b")]);
+        let values = Int32Array::from(vec![Some(1), Some(2)]);
+        let entry_offsets = arrow::buffer::OffsetBuffer::new(vec![0i32, 
2].into());
+
+        let key_field = Arc::new(Field::new("key", DataType::Utf8, false));
+        let value_field = Arc::new(Field::new("value", DataType::Int32, true));
+
+        let entries = arrow::array::StructArray::new(
+            arrow::datatypes::Fields::from(vec![key_field, value_field]),
+            vec![Arc::new(keys), Arc::new(values)],
+            None,
+        );
+
+        let map_field = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(arrow::datatypes::Fields::from(vec![
+                Field::new("key", DataType::Utf8, false),
+                Field::new("value", DataType::Int32, true),
+            ])),
+            false,
+        ));
+
+        let map_array = MapArray::try_new(map_field, entry_offsets, entries, 
None, false).unwrap();
+        let scalar = ScalarValue::Map(Arc::new(map_array));
+        let result = spark_size_scalar(&scalar).unwrap();
+        assert_eq!(result, ScalarValue::Int32(Some(2))); // Map with 2 entries
+    }
+
+    #[test]
+    fn test_spark_size_scalar_null_map() {
+        use arrow::array::{Int32Array, MapArray, StringArray};
+
+        // Test null map - Spark returns -1 for null map input
+        let keys = StringArray::from(vec![Some("a")]);
+        let values = Int32Array::from(vec![Some(1)]);
+        let entry_offsets = arrow::buffer::OffsetBuffer::new(vec![0i32, 
1].into());
+
+        let key_field = Arc::new(Field::new("key", DataType::Utf8, false));
+        let value_field = Arc::new(Field::new("value", DataType::Int32, true));
+
+        let entries = arrow::array::StructArray::new(
+            arrow::datatypes::Fields::from(vec![key_field, value_field]),
+            vec![Arc::new(keys), Arc::new(values)],
+            None,
+        );
+
+        let map_field = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(arrow::datatypes::Fields::from(vec![
+                Field::new("key", DataType::Utf8, false),
+                Field::new("value", DataType::Int32, true),
+            ])),
+            false,
+        ));
+
+        // Mark the single map entry as null
+        let mut null_buffer = NullBufferBuilder::new(1);
+        null_buffer.append(false); // null map
+
+        let map_array = MapArray::try_new(
+            map_field,
+            entry_offsets,
+            entries,
+            null_buffer.finish(),
+            false,
+        )
+        .unwrap();
+        let scalar = ScalarValue::Map(Arc::new(map_array));
+        let result = spark_size_scalar(&scalar).unwrap();
+        assert_eq!(result, ScalarValue::Int32(Some(-1))); // null map returns 
-1
+    }
+
     #[test]
     fn test_spark_size_fixed_size_list_array() {
         use arrow::array::FixedSizeListArray;
diff --git a/spark/src/main/scala/org/apache/comet/serde/arrays.scala 
b/spark/src/main/scala/org/apache/comet/serde/arrays.scala
index f92827d3b1..8bc639021c 100644
--- a/spark/src/main/scala/org/apache/comet/serde/arrays.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/arrays.scala
@@ -739,15 +739,11 @@ object CometArrayFilter extends 
CometExpressionSerde[ArrayFilter] {
 
 object CometSize extends CometExpressionSerde[Size] {
 
-  override def getUnsupportedReasons(): Seq[String] = Seq(
-    "Only supports `ArrayType` input; `MapType` input is not supported")
-
   override def getSupportLevel(expr: Size): SupportLevel = {
     expr.child.dataType match {
       case _: ArrayType => Compatible()
-      case _: MapType => Unsupported(Some("size does not support map inputs"))
+      case _: MapType => Compatible()
       case other =>
-        // this should be unreachable because Spark only supports map and 
array inputs
         Unsupported(Some(s"Unsupported child data type: $other"))
     }
   }
diff --git 
a/spark/src/test/resources/sql-tests/expressions/array/posexplode.sql 
b/spark/src/test/resources/sql-tests/expressions/array/posexplode.sql
index 20b0547ebe..b7ba70d45a 100644
--- a/spark/src/test/resources/sql-tests/expressions/array/posexplode.sql
+++ b/spark/src/test/resources/sql-tests/expressions/array/posexplode.sql
@@ -95,6 +95,6 @@ INSERT INTO test_posexplode_map VALUES
   (1, map('a', 1, 'b', 2)),
   (2, map('c', 3))
 
--- posexplode over a map falls back to Spark (Comet only supports array inputs)
-query expect_fallback(size does not support map inputs)
+-- posexplode over a map falls back to Spark (Comet only supports array 
inputs, not maps)
+query expect_fallback(Comet only supports explode/explode_outer for arrays, 
not maps)
 SELECT id, posexplode(m) FROM test_posexplode_map
diff --git a/spark/src/test/resources/sql-tests/expressions/array/size.sql 
b/spark/src/test/resources/sql-tests/expressions/array/size.sql
index b006a4da0d..fb2bedef1e 100644
--- a/spark/src/test/resources/sql-tests/expressions/array/size.sql
+++ b/spark/src/test/resources/sql-tests/expressions/array/size.sql
@@ -15,15 +15,29 @@
 -- specific language governing permissions and limitations
 -- under the License.
 
+-- ConfigMatrix: spark.sql.legacy.sizeOfNull=true,false
+
 statement
 CREATE TABLE test_size(arr array<int>, m map<string, int>) USING parquet
 
 statement
 INSERT INTO test_size VALUES (array(1, 2, 3), map('a', 1, 'b', 2)), (array(), 
map()), (NULL, NULL)
 
-query spark_answer_only
+query
 SELECT size(arr), size(m) FROM test_size
 
--- literal arguments
+-- literal array arguments
 query
 SELECT size(array(1, 2, 3)), size(array()), size(cast(NULL as array<int>))
+
+-- literal map via CreateMap (falls back: Comet has no CreateMap serde;
+-- cast(NULL as map) avoids CreateMap and goes through CometLiteral instead)
+query spark_answer_only
+SELECT size(map('a', 1, 'b', 2)), size(map())
+
+query
+SELECT size(cast(NULL as map<string,int>))
+
+-- cardinality is a SQL alias for size
+query
+SELECT cardinality(arr), cardinality(m) FROM test_size
diff --git 
a/spark/src/test/scala/org/apache/comet/CometMapExpressionSuite.scala 
b/spark/src/test/scala/org/apache/comet/CometMapExpressionSuite.scala
index 9904919b74..2581ef5b0d 100644
--- a/spark/src/test/scala/org/apache/comet/CometMapExpressionSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/CometMapExpressionSuite.scala
@@ -125,34 +125,47 @@ class CometMapExpressionSuite extends CometTestBase {
     }
   }
 
-  test("fallback for size with map input") {
+  test("size with map input") {
     withTempDir { dir =>
       withTempView("t1") {
         val path = new Path(dir.toURI.toString, "test.parquet")
         makeParquetFileAllPrimitiveTypes(path, dictionaryEnabled = true, 100)
         spark.read.parquet(path.toString).createOrReplaceTempView("t1")
 
-        // Use column references in maps to avoid constant folding
-        checkSparkAnswerAndFallbackReason(
-          sql("SELECT size(case when _2 < 0 then map(_8, _9) else map() end) 
from t1"),
-          "size does not support map inputs")
+        checkSparkAnswer(
+          sql("SELECT size(case when _2 < 0 then map(_8, _9) else map() end) 
from t1"))
       }
     }
   }
 
-  // fails with "map is not supported"
-  ignore("size with map input") {
-    withTempDir { dir =>
-      withTempView("t1") {
-        val path = new Path(dir.toURI.toString, "test.parquet")
-        makeParquetFileAllPrimitiveTypes(path, dictionaryEnabled = true, 100)
-        spark.read.parquet(path.toString).createOrReplaceTempView("t1")
+  test("size with map input - v2 reader") {
+    withTempPath { dir =>
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        val df = spark
+          .range(100)
+          .select(
+            col("id"),
+            when(col("id") > 1, map(col("id"), col("id"))).alias("map1"),
+            when(col("id") > 5, map(col("id"), col("id"))).alias("map2"))
+        df.write.parquet(dir.toString())
+      }
 
-        // Use column references in maps to avoid constant folding
-        checkSparkAnswerAndOperator(
-          sql("SELECT size(map(_8, _9, _10, _11)) from t1 where _8 is not 
null"))
-        checkSparkAnswerAndOperator(
-          sql("SELECT size(case when _2 < 0 then map(_8, _9) else map() end) 
from t1"))
+      Seq("", "parquet").foreach { v1List =>
+        withSQLConf(SQLConf.USE_V1_SOURCE_LIST.key -> v1List) {
+          val df = spark.read.parquet(dir.toString())
+          df.createOrReplaceTempView("t1")
+          if (v1List.isEmpty) {
+            checkSparkAnswer(df.select(size(col("map1"))))
+            checkSparkAnswer(df.select(size(col("map2"))))
+            checkSparkAnswer(
+              sql("SELECT size(CASE WHEN id < 50 THEN map1 ELSE map2 END) FROM 
t1"))
+          } else {
+            checkSparkAnswerAndOperator(df.select(size(col("map1"))))
+            checkSparkAnswerAndOperator(df.select(size(col("map2"))))
+            checkSparkAnswerAndOperator(
+              sql("SELECT size(CASE WHEN id < 50 THEN map1 ELSE map2 END) FROM 
t1"))
+          }
+        }
       }
     }
   }
@@ -233,13 +246,11 @@ class CometMapExpressionSuite extends CometTestBase {
     }
   }
 
-  test("map_from_entries - binary type") {
+  test("map_from_entries - binary type routes through codegen dispatcher") {
     val table = "t2"
     withTable(table) {
       sql(
         s"create table $table using parquet as select cast(array() as 
array<binary>) as c1 from range(10)")
-      // The native path is Incompatible for binary keys/values, so Comet 
routes these through
-      // the codegen dispatcher and still executes natively.
       checkSparkAnswerAndOperator(
         sql(s"select map_from_entries(array(struct(c1, 0))) from $table"))
       checkSparkAnswerAndOperator(


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

Reply via email to