Skip to content

Commit 58d254f

Browse files
committed
feat: support size() for MapType input (#4472)
1 parent 763691f commit 58d254f

5 files changed

Lines changed: 41 additions & 72 deletions

File tree

native/spark-expr/src/array_funcs/size.rs

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ fn spark_size_scalar(scalar: &ScalarValue) -> Result<ScalarValue, DataFusionErro
198198
Ok(ScalarValue::Int32(Some(len)))
199199
}
200200
}
201+
ScalarValue::Map(array) => {
202+
if array.is_null(0) {
203+
Ok(ScalarValue::Int32(Some(-1)))
204+
} else {
205+
let len = array.value_length(0) as i32;
206+
Ok(ScalarValue::Int32(Some(len)))
207+
}
208+
}
201209
ScalarValue::Null => {
202210
Ok(ScalarValue::Int32(Some(-1))) // Spark behavior: return -1 for null
203211
}
@@ -279,75 +287,56 @@ mod tests {
279287
// TODO: Add map array test once Arrow MapArray API constraints are resolved
280288
// Currently MapArray doesn't allow nulls in entries which makes testing complex
281289
// The core size() implementation supports maps correctly
282-
#[ignore]
283290
#[test]
284291
fn test_spark_size_map_array() {
285-
use arrow::array::{MapArray, StringArray};
286-
287-
// Create a simpler test with maps:
288-
// [{"key1": "value1", "key2": "value2"}, {"key3": "value3"}, {}, null]
292+
use arrow::array::{Int32Array, MapArray, StringArray};
289293

290-
// Create keys array for all entries (no nulls)
291-
let keys = StringArray::from(vec!["key1", "key2", "key3"]);
294+
let keys = StringArray::from(vec![Some("key1"), Some("key2"), Some("key3")]);
295+
let values = Int32Array::from(vec![Some(1), Some(2), Some(3)]);
292296

293-
// Create values array for all entries (no nulls)
294-
let values = StringArray::from(vec!["value1", "value2", "value3"]);
295-
296-
// Create entry offsets: [0, 2, 3, 3] representing:
297-
// - Map 1: entries 0-1 (2 key-value pairs)
298-
// - Map 2: entries 2-2 (1 key-value pair)
299-
// - Map 3: entries 3-2 (0 key-value pairs, empty map)
300-
// - Map 4: null (handled by null buffer)
301-
let entry_offsets = arrow::buffer::OffsetBuffer::new(vec![0, 2, 3, 3, 3].into());
297+
let entry_offsets = arrow::buffer::OffsetBuffer::new(vec![0i32, 2, 3, 3, 3].into());
302298

303299
let key_field = Arc::new(Field::new("key", DataType::Utf8, false));
304-
let value_field = Arc::new(Field::new("value", DataType::Utf8, false)); // Make values non-nullable too
300+
let value_field = Arc::new(Field::new("value", DataType::Int32, true));
305301

306-
// Create the entries struct array
307302
let entries = arrow::array::StructArray::new(
308303
arrow::datatypes::Fields::from(vec![key_field, value_field]),
309304
vec![Arc::new(keys), Arc::new(values)],
310-
None, // No nulls in the entries struct array itself
305+
None,
311306
);
312307

313-
// Create null buffer for the map array (fourth map is null)
314308
let mut null_buffer = NullBufferBuilder::new(4);
315-
null_buffer.append(true); // Map with 2 entries - not null
316-
null_buffer.append(true); // Map with 1 entry - not null
317-
null_buffer.append(true); // Empty map - not null
318-
null_buffer.append(false); // null map
319-
320-
let map_data_type = DataType::Map(
321-
Arc::new(Field::new(
322-
"entries",
323-
DataType::Struct(arrow::datatypes::Fields::from(vec![
324-
Field::new("key", DataType::Utf8, false),
325-
Field::new("value", DataType::Utf8, false), // Make values non-nullable too
326-
])),
327-
false,
328-
)),
329-
false, // keys are not sorted
330-
);
331-
332-
let map_field = Arc::new(Field::new("map", map_data_type, true));
333-
334-
let map_array = MapArray::new(
309+
null_buffer.append(true);
310+
null_buffer.append(true);
311+
null_buffer.append(true);
312+
null_buffer.append(false);
313+
314+
let map_field = Arc::new(Field::new(
315+
"entries",
316+
DataType::Struct(arrow::datatypes::Fields::from(vec![
317+
Field::new("key", DataType::Utf8, false),
318+
Field::new("value", DataType::Int32, true),
319+
])),
320+
false,
321+
));
322+
323+
let map_array = MapArray::try_new(
335324
map_field,
336325
entry_offsets,
337326
entries,
338327
null_buffer.finish(),
339-
false, // keys are not sorted
340-
);
328+
false,
329+
)
330+
.unwrap();
341331

342332
let array_ref: ArrayRef = Arc::new(map_array);
343333
let result = spark_size_array(&array_ref).unwrap();
344334
let result = result.as_any().downcast_ref::<Int32Array>().unwrap();
345335

346-
// Expected: [2, 1, 0, -1]
347-
assert_eq!(result.value(0), 2); // Map with 2 key-value pairs
348-
assert_eq!(result.value(1), 1); // Map with 1 key-value pair
349-
assert_eq!(result.value(2), 0); // empty map has 0 pairs
350-
assert_eq!(result.value(3), -1); // null map returns -1
336+
assert_eq!(result.value(0), 2);
337+
assert_eq!(result.value(1), 1);
338+
assert_eq!(result.value(2), 0);
339+
assert_eq!(result.value(3), -1);
351340
}
352341

353342
#[test]

spark/src/main/scala/org/apache/comet/serde/arrays.scala

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -667,15 +667,11 @@ object CometArrayFilter extends CometExpressionSerde[ArrayFilter] {
667667

668668
object CometSize extends CometExpressionSerde[Size] {
669669

670-
override def getUnsupportedReasons(): Seq[String] = Seq(
671-
"Only supports `ArrayType` input; `MapType` input is not supported")
672-
673670
override def getSupportLevel(expr: Size): SupportLevel = {
674671
expr.child.dataType match {
675672
case _: ArrayType => Compatible()
676-
case _: MapType => Unsupported(Some("size does not support map inputs"))
673+
case _: MapType => Compatible()
677674
case other =>
678-
// this should be unreachable because Spark only supports map and array inputs
679675
Unsupported(Some(s"Unsupported child data type: $other"))
680676
}
681677
}

spark/src/test/resources/sql-tests/expressions/array/posexplode.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,6 @@ INSERT INTO test_posexplode_map VALUES
9595
(1, map('a', 1, 'b', 2)),
9696
(2, map('c', 3))
9797

98-
-- posexplode over a map falls back to Spark (Comet only supports array inputs)
99-
query expect_fallback(size does not support map inputs)
98+
-- posexplode over a map falls back to Spark (Comet only supports array inputs, not maps)
99+
query expect_fallback(Comet only supports explode/explode_outer for arrays, not maps)
100100
SELECT id, posexplode(m) FROM test_posexplode_map

spark/src/test/resources/sql-tests/expressions/array/size.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ CREATE TABLE test_size(arr array<int>, m map<string, int>) USING parquet
2121
statement
2222
INSERT INTO test_size VALUES (array(1, 2, 3), map('a', 1, 'b', 2)), (array(), map()), (NULL, NULL)
2323

24-
query spark_answer_only
24+
query
2525
SELECT size(arr), size(m) FROM test_size
2626

2727
-- literal arguments

spark/src/test/scala/org/apache/comet/CometMapExpressionSuite.scala

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -126,23 +126,7 @@ class CometMapExpressionSuite extends CometTestBase {
126126
}
127127
}
128128

129-
test("fallback for size with map input") {
130-
withTempDir { dir =>
131-
withTempView("t1") {
132-
val path = new Path(dir.toURI.toString, "test.parquet")
133-
makeParquetFileAllPrimitiveTypes(path, dictionaryEnabled = true, 100)
134-
spark.read.parquet(path.toString).createOrReplaceTempView("t1")
135-
136-
// Use column references in maps to avoid constant folding
137-
checkSparkAnswerAndFallbackReason(
138-
sql("SELECT size(case when _2 < 0 then map(_8, _9) else map() end) from t1"),
139-
"size does not support map inputs")
140-
}
141-
}
142-
}
143-
144-
// fails with "map is not supported"
145-
ignore("size with map input") {
129+
test("size with map input") {
146130
withTempDir { dir =>
147131
withTempView("t1") {
148132
val path = new Path(dir.toURI.toString, "test.parquet")

0 commit comments

Comments
 (0)