This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new d8d980bdcf fix(arrow-array): disallow creating `MapArray` with
nullable key field (#10272)
d8d980bdcf is described below
commit d8d980bdcffd69f42888d7f1f94490ac1668376f
Author: Raz Luvaton <[email protected]>
AuthorDate: Mon Jul 6 06:01:27 2026 +0300
fix(arrow-array): disallow creating `MapArray` with nullable key field
(#10272)
# Which issue does this PR close?
- Closes #10268
# Rationale for this change
we should be unable to create invalid MapArray according to the spec
# What changes are included in this PR?
added validation that key field is not nullable
# Are these changes tested?
yes
# Are there any user-facing changes?
yes, it will now fail for nullable key field
---
arrow-array/src/array/map_array.rs | 32 ++++
arrow-data/src/data.rs | 357 ++++++++++++++++++++++++++++++++++++-
arrow-ipc/src/reader.rs | 8 +-
arrow-row/src/lib.rs | 28 ---
4 files changed, 392 insertions(+), 33 deletions(-)
diff --git a/arrow-array/src/array/map_array.rs
b/arrow-array/src/array/map_array.rs
index 0f7c435430..c3b809083e 100644
--- a/arrow-array/src/array/map_array.rs
+++ b/arrow-array/src/array/map_array.rs
@@ -61,6 +61,7 @@ impl MapArray {
/// * `entries.null_count() != 0`
/// * `entries.columns().len() != 2`
/// * `field.data_type() != entries.data_type()`
+ /// * the keys field is nullable
pub fn try_new(
field: FieldRef,
offsets: OffsetBuffer<i32>,
@@ -109,6 +110,16 @@ impl MapArray {
)));
}
+ // The Arrow spec requires the "key" field to be non-nullable
+ //
<https://github.com/apache/arrow/blob/98347d233f03bcf4d116d77f1769d498902b1fc8/format/Schema.fbs#L138>
+ if entries.fields()[0].is_nullable() {
+ return Err(ArrowError::InvalidArgumentError(
+ "MapArray keys field cannot be nullable".to_string(),
+ ));
+ }
+ // No need to verify if key contain nulls since `StructArray`
+ // already disallow nulls for non nullable fields
+
Ok(Self {
data_type: DataType::Map(field, ordered),
nulls,
@@ -931,6 +942,27 @@ mod tests {
);
}
+ #[test]
+ fn test_try_new_nullable_keys_field() {
+ // https://github.com/apache/arrow-rs/issues/10268
+ let keys = Int32Array::from(vec![Some(1), None]);
+ let values = Int32Array::from(vec![None, Some(2)]);
+ let fields = Fields::from(vec![
+ Field::new("keys", DataType::Int32, true),
+ Field::new("values", DataType::Int32, true),
+ ]);
+ let entries =
+ StructArray::new(fields.clone(), vec![Arc::new(keys),
Arc::new(values)], None);
+ let field = Arc::new(Field::new("entries", DataType::Struct(fields),
false));
+
+ let err = MapArray::try_new(field, OffsetBuffer::from_lengths([2]),
entries, None, false)
+ .unwrap_err();
+ assert_eq!(
+ err.to_string(),
+ "Invalid argument error: MapArray keys field cannot be nullable"
+ );
+ }
+
#[test]
fn test_from_vec_of_maps() {
for ordered in [true, false] {
diff --git a/arrow-data/src/data.rs b/arrow-data/src/data.rs
index 19e3037846..905a57dc4f 100644
--- a/arrow-data/src/data.rs
+++ b/arrow-data/src/data.rs
@@ -991,6 +991,11 @@ impl ArrayData {
)));
}
}
+ DataType::Map(f, _) if f.is_nullable() => {
+ return Err(ArrowError::InvalidArgumentError(
+ "The nullable should be set to false for the map entries
field.".to_string(),
+ ));
+ }
_ => {}
};
@@ -1140,7 +1145,7 @@ impl ArrayData {
/// Validates the layout of `child_data` ArrayData structures
fn validate_child_data(&self) -> Result<(), ArrowError> {
match &self.data_type {
- DataType::List(field) | DataType::Map(field, _) => {
+ DataType::List(field) => {
let values_data =
self.get_single_valid_child_data(field.data_type())?;
self.validate_offsets::<i32>(values_data.len)?;
Ok(())
@@ -1150,6 +1155,30 @@ impl ArrayData {
self.validate_offsets::<i64>(values_data.len)?;
Ok(())
}
+ DataType::Map(field, _) => {
+ let DataType::Struct(entries_fields) = field.data_type() else {
+ return Err(ArrowError::InvalidArgumentError(format!(
+ "Map field should be a entries struct data type, got
{:?} instead",
+ field.data_type()
+ )));
+ };
+ if entries_fields.len() != 2 {
+ return Err(ArrowError::InvalidArgumentError(format!(
+ "Map entries data type should be a struct containing 2
fields, got {} fields",
+ entries_fields.len()
+ )));
+ }
+
+ // Key field
+ if entries_fields[0].is_nullable() {
+ return Err(ArrowError::InvalidArgumentError(
+ "Map key field must not be nullable".to_string(),
+ ));
+ }
+ let values_data =
self.get_single_valid_child_data(field.data_type())?;
+ self.validate_offsets::<i32>(values_data.len)?;
+ Ok(())
+ }
DataType::ListView(field) => {
let values_data =
self.get_single_valid_child_data(field.data_type())?;
self.validate_offsets_and_sizes::<i32>(values_data.len)?;
@@ -2303,6 +2332,7 @@ pub(crate) fn get_fixed_size_binary_width(data_type:
&DataType) -> usize {
#[cfg(test)]
mod tests {
use super::*;
+ use arrow_buffer::{OffsetBuffer, ScalarBuffer};
use arrow_schema::{Field, Fields};
// See arrow/tests/array_data_validation.rs for test of array validation
@@ -2766,4 +2796,329 @@ mod tests {
format!("Invalid argument error: Last offset 2 of Utf8 is larger
than values length 0",)
);
}
+
+ #[test]
+ fn should_fail_validation_when_having_map_field_type_is_not_struct() {
+ let map_field = Field::new("key", DataType::Int32, false);
+
+ let map_field_data = valid_non_nullable_int32_array_data(2);
+
+ let results = test_both_builder_and_array_data(
+ DataType::Map(map_field.into(), false),
+ 1,
+ None,
+ 0,
+ vec![
+ OffsetBuffer::<i32>::from_lengths(vec![2])
+ .into_inner()
+ .into(),
+ ],
+ vec![map_field_data],
+ );
+
+ for result in results {
+ let array_data_err = result.expect_err("should fail for non struct
field");
+
+ match array_data_err {
+ ArrowError::InvalidArgumentError(msg) => {
+ assert_eq!(
+ msg,
+ "Map field should be a entries struct data type, got
Int32 instead"
+ )
+ }
+ _ => panic!("unexpected error type {array_data_err}"),
+ };
+ }
+ }
+
+ #[test]
+ fn should_fail_validation_when_having_map_entries_only_have_1_field() {
+ let struct_data_type = DataType::Struct(Fields::from(vec![Field::new(
+ "key",
+ DataType::Int32,
+ false,
+ )]));
+
+ let key_array_data = valid_non_nullable_int32_array_data(2);
+
+ let struct_data = {
+ let builder = ArrayDataBuilder::new(struct_data_type.clone())
+ .len(2)
+ .nulls(None)
+ .child_data(vec![key_array_data]);
+
+ builder.build().unwrap()
+ };
+
+ let results = test_both_builder_and_array_data(
+ DataType::Map(Field::new("entries", struct_data_type,
false).into(), false),
+ 1,
+ None,
+ 0,
+ vec![
+ OffsetBuffer::<i32>::from_lengths(vec![2])
+ .into_inner()
+ .into(),
+ ],
+ vec![struct_data],
+ );
+
+ for result in results {
+ let array_data_err = result.expect_err("should fail for nullable
key");
+
+ match array_data_err {
+ ArrowError::InvalidArgumentError(msg) => {
+ assert_eq!(
+ msg,
+ "Map entries data type should be a struct containing 2
fields, got 1 fields"
+ )
+ }
+ _ => panic!("unexpected error type {array_data_err}"),
+ };
+ }
+ }
+
+ #[test]
+ fn should_fail_validation_when_having_map_entries_have_3_fields() {
+ let struct_data_type = DataType::Struct(Fields::from(vec![
+ Field::new("key", DataType::Int32, false),
+ Field::new("values", DataType::Utf8, true),
+ Field::new("other", DataType::Int32, true),
+ ]));
+
+ let key_array_data = valid_non_nullable_int32_array_data(2);
+
+ let values_array_data = valid_string_array_data(2);
+
+ let other_array_data = key_array_data.clone();
+
+ let struct_data = {
+ let builder = ArrayDataBuilder::new(struct_data_type.clone())
+ .len(2)
+ .nulls(None)
+ .child_data(vec![key_array_data, values_array_data,
other_array_data]);
+
+ builder.build().unwrap()
+ };
+
+ let results = test_both_builder_and_array_data(
+ DataType::Map(Field::new("entries", struct_data_type,
false).into(), false),
+ 1,
+ None,
+ 0,
+ vec![
+ OffsetBuffer::<i32>::from_lengths(vec![2])
+ .into_inner()
+ .into(),
+ ],
+ vec![struct_data],
+ );
+
+ for result in results {
+ let array_data_err = result.expect_err("should fail for nullable
key");
+
+ match array_data_err {
+ ArrowError::InvalidArgumentError(msg) => {
+ assert_eq!(
+ msg,
+ "Map entries data type should be a struct containing 2
fields, got 3 fields"
+ )
+ }
+ _ => panic!("unexpected error type {array_data_err}"),
+ };
+ }
+ }
+
+ #[test]
+ fn should_fail_validation_when_having_nullable_map_keys() {
+ let struct_data_type = DataType::Struct(Fields::from(vec![
+ Field::new("key", DataType::Int32, true),
+ Field::new("values", DataType::Utf8, true),
+ ]));
+
+ let key_array_data = valid_non_nullable_int32_array_data(2);
+ let values_array_data = valid_string_array_data(2);
+
+ let struct_data = {
+ let builder = ArrayDataBuilder::new(struct_data_type.clone())
+ .len(2)
+ .nulls(None)
+ .child_data(vec![key_array_data, values_array_data]);
+
+ builder.build().unwrap()
+ };
+
+ let results = test_both_builder_and_array_data(
+ DataType::Map(Field::new("entries", struct_data_type,
false).into(), false),
+ 1,
+ None,
+ 0,
+ vec![
+ OffsetBuffer::<i32>::from_lengths(vec![2])
+ .into_inner()
+ .into(),
+ ],
+ vec![struct_data],
+ );
+
+ for result in results {
+ let array_data_err = result.expect_err("should fail for nullable
key");
+
+ match array_data_err {
+ ArrowError::InvalidArgumentError(msg) => {
+ assert_eq!(msg, "Map key field must not be nullable")
+ }
+ _ => panic!("unexpected error type {array_data_err}"),
+ };
+ }
+ }
+
+ #[test]
+ fn should_fail_validation_when_having_entries_is_nullable_for_map() {
+ let struct_data_type = DataType::Struct(Fields::from(vec![
+ Field::new("key", DataType::Int32, false),
+ Field::new("values", DataType::Utf8, true),
+ ]));
+
+ let key_array_data = valid_non_nullable_int32_array_data(2);
+
+ let values_array_data = valid_string_array_data(2);
+
+ let struct_data = {
+ let builder = ArrayDataBuilder::new(struct_data_type.clone())
+ .len(2)
+ .nulls(None)
+ .child_data(vec![key_array_data, values_array_data]);
+
+ builder.build().unwrap()
+ };
+
+ let results = test_both_builder_and_array_data(
+ DataType::Map(Field::new("entries", struct_data_type,
true).into(), false),
+ 1,
+ None,
+ 0,
+ vec![
+ OffsetBuffer::<i32>::from_lengths(vec![2])
+ .into_inner()
+ .into(),
+ ],
+ vec![struct_data],
+ );
+
+ for result in results {
+ let array_data_err = result.expect_err("should fail for nullable
entries");
+
+ match array_data_err {
+ ArrowError::InvalidArgumentError(msg) => assert_eq!(
+ msg,
+ "The nullable should be set to false for the map entries
field."
+ ),
+ _ => panic!("unexpected error type {array_data_err}"),
+ };
+ }
+ }
+
+ #[test]
+ fn should_allow_to_create_map_from_data() {
+ let struct_data_type = DataType::Struct(Fields::from(vec![
+ Field::new("key", DataType::Int32, false),
+ Field::new("values", DataType::Utf8, true),
+ ]));
+
+ let key_array_data = valid_non_nullable_int32_array_data(2);
+ let values_array_data = valid_string_array_data(2);
+
+ let struct_data = {
+ let builder = ArrayDataBuilder::new(struct_data_type.clone())
+ .len(2)
+ .nulls(None)
+ .child_data(vec![key_array_data, values_array_data]);
+
+ builder.build().unwrap()
+ };
+
+ let results = test_both_builder_and_array_data(
+ DataType::Map(Field::new("entries", struct_data_type,
false).into(), false),
+ 1,
+ None,
+ 0,
+ vec![
+ OffsetBuffer::<i32>::from_lengths(vec![2])
+ .into_inner()
+ .into(),
+ ],
+ vec![struct_data],
+ );
+
+ for result in results {
+ result.expect("should be able to create map ArrayData");
+ }
+ }
+
+ fn valid_string_array_data(length: usize) -> ArrayData {
+ let offsets = OffsetBuffer::<i32>::from_lengths(vec![0; length])
+ .into_inner()
+ .into_inner();
+ let empty_bytes = Buffer::default();
+
+ let builder = ArrayDataBuilder::new(DataType::Utf8)
+ .len(length)
+ .buffers(vec![offsets, empty_bytes])
+ .nulls(None);
+
+ builder.build().unwrap()
+ }
+
+ fn valid_non_nullable_int32_array_data(length: usize) -> ArrayData {
+ let builder = ArrayDataBuilder::new(DataType::Int32)
+ .len(length)
+ .nulls(None)
+ .buffers(vec![
+ ScalarBuffer::<i32>::from(vec![1; length]).into_inner(),
+ ]);
+
+ builder.build().unwrap()
+ }
+
+ #[test]
+ fn empty_and_null_map_array_should_pass_validation() {
+ let dt = DataType::Map(
+ Field::new(
+ "entries",
+ DataType::Struct(Fields::from(vec![
+ Field::new("key", DataType::Int32, false),
+ Field::new("values", DataType::Utf8, true),
+ ])),
+ false,
+ )
+ .into(),
+ false,
+ );
+
+ ArrayData::new_empty(&dt).validate_full().unwrap();
+ ArrayData::new_null(&dt, 1).validate_full().unwrap();
+ }
+
+ fn test_both_builder_and_array_data(
+ data_type: DataType,
+ len: usize,
+ null_bit_buffer: Option<Buffer>,
+ offset: usize,
+ buffers: Vec<Buffer>,
+ child_data: Vec<ArrayData>,
+ ) -> [Result<ArrayData, ArrowError>; 2] {
+ let from_builder_res = ArrayData::builder(data_type.clone())
+ .len(len)
+ .add_buffers(buffers.clone())
+ .null_bit_buffer(null_bit_buffer.clone())
+ .offset(offset)
+ .child_data(child_data.clone())
+ .build();
+
+ let from_try_new_res =
+ ArrayData::try_new(data_type, len, null_bit_buffer, offset,
buffers, child_data);
+
+ [from_builder_res, from_try_new_res]
+ }
}
diff --git a/arrow-ipc/src/reader.rs b/arrow-ipc/src/reader.rs
index 6cb1d7e6b0..5ee7da2015 100644
--- a/arrow-ipc/src/reader.rs
+++ b/arrow-ipc/src/reader.rs
@@ -2742,14 +2742,14 @@ mod tests {
let value_dict_keys = Int8Array::from_iter_values([0, 1, 1, 2, 3, 1]);
let value_dict_array = DictionaryArray::new(value_dict_keys,
values.clone());
- let key_dict_keys = Int8Array::from_iter_values([0, 0, 2, 1, 1, 3]);
+ let key_dict_keys = Int8Array::from_iter_values([0, 0, 2, 2, 2, 3]);
let key_dict_array = DictionaryArray::new(key_dict_keys, values);
#[allow(deprecated)]
let keys_field = Arc::new(Field::new_dict(
"keys",
DataType::Dictionary(Box::new(DataType::Int8),
Box::new(DataType::Utf8)),
- true, // It is technically not legal for this field to be null.
+ false,
1,
false,
));
@@ -2954,13 +2954,13 @@ mod tests {
let bin_view_array = Arc::new(BinaryViewArray::from_iter(bin_values));
let utf8_view_array =
Arc::new(StringViewArray::from_iter(utf8_values));
- let key_dict_keys = Int8Array::from_iter_values([0, 0, 1, 2, 0, 1, 3]);
+ let key_dict_keys = Int8Array::from_iter_values([0, 0, 2, 2, 0, 2, 3]);
let key_dict_array = DictionaryArray::new(key_dict_keys,
utf8_view_array.clone());
#[allow(deprecated)]
let keys_field = Arc::new(Field::new_dict(
"keys",
DataType::Dictionary(Box::new(DataType::Int8),
Box::new(DataType::Utf8View)),
- true,
+ false,
1,
false,
));
diff --git a/arrow-row/src/lib.rs b/arrow-row/src/lib.rs
index 8ed2debb6a..257707469d 100644
--- a/arrow-row/src/lib.rs
+++ b/arrow-row/src/lib.rs
@@ -6391,34 +6391,6 @@ mod tests {
assert_eq!(&map, &back[0]);
}
- // Test Map<Null, Null> - both keys and values are Null type
- #[test]
- fn test_map_null_keys_and_null_values() {
- let null_keys = Arc::new(NullArray::new(3)) as ArrayRef;
- let null_values = Arc::new(NullArray::new(3)) as ArrayRef;
-
- let offsets = OffsetBuffer::new(vec![0, 1, 1, 3].into());
- let entries_fields = vec![
- Arc::new(Field::new("keys", DataType::Null, true)),
- Arc::new(Field::new("values", DataType::Null, true)),
- ];
- let struct_field = Arc::new(Field::new(
- "entries",
- DataType::Struct(entries_fields.clone().into()),
- false,
- ));
- let entries = StructArray::new(entries_fields.into(), vec![null_keys,
null_values], None);
-
- let map: ArrayRef = Arc::new(MapArray::new(struct_field, offsets,
entries, None, false));
-
- let converter =
RowConverter::new(vec![SortField::new(map.data_type().clone())]).unwrap();
- let rows = converter.convert_columns(&[Arc::clone(&map)]).unwrap();
- let back = converter.convert_rows(&rows).unwrap();
- assert_eq!(back.len(), 1);
- back[0].to_data().validate_full().unwrap();
- assert_eq!(&map, &back[0]);
- }
-
// Test Map<Utf8, Null> all empty maps
#[test]
fn test_map_null_all_empty() {