This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] 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 1514ea4579 fix(parquet): support non-primitive map keys in record
reader (#11229)
1514ea4579 is described below
commit 1514ea4579340f680b460739d5a96fab38b1314c
Author: hsdfat <[email protected]>
AuthorDate: Fri Oct 2 05:01:54 2026 +0000
fix(parquet): support non-primitive map keys in record reader (#11229)
# Which issue does this PR close?
- Closes #7768.
# Rationale for this change
The record API (`RowIter` / `TreeBuilder`) asserted that map keys are
primitive, so reading a file with e.g. struct- or list-keyed maps
panicked. The Parquet spec has no such restriction and the arrow reader
already handles these files.
This picks up the fix from #7769 by @njaremko, which went stale waiting
on a test file in apache/parquet-testing#87. Thanks @njaremko for the
original fix.
# What changes are included in this PR?
- Remove the primitive-key assertion in `TreeBuilder::reader_tree`. The
key reader was already built via `reader_tree` in the same way as the
value reader, so no other reader changes are needed.
- `Map` stores entries as `Vec<(Field, Field)>`, so complex keys need no
`Hash`/`Eq`; `Display` and `to_json_value` already handle non-primitive
keys.
# Are these changes tested?
Yes. `test_compound_map_key` writes a file in memory with `ArrowWriter`
containing a map with struct keys and a map with list keys (including an
empty map and an empty-list key), then reads it back with the record API
and checks the rows. Generating the file in the test avoids depending on
apache/parquet-testing#87 and addresses the review feedback on #7769
(descriptive test name, only the relevant columns). The test panics
without the fix.
# Are there any user-facing changes?
Files with non-primitive map keys can now be read with the record API
instead of panicking. No API changes.
AI usage: this change was prepared with the help of AI assistants
(Claude, OpenAI models). I reviewed the change and the reader's
key/value level handling, and verified the test fails before and passes
after the fix.
Co-authored-by: Ed Seidl <[email protected]>
Co-authored-by: Jeffrey Vo <[email protected]>
---
parquet/src/record/reader.rs | 113 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 109 insertions(+), 4 deletions(-)
diff --git a/parquet/src/record/reader.rs b/parquet/src/record/reader.rs
index 2e2ddd4db5..f5886e05d1 100644
--- a/parquet/src/record/reader.rs
+++ b/parquet/src/record/reader.rs
@@ -230,10 +230,6 @@ impl TreeBuilder {
path.push(String::from(key_value_type.name()));
let key_type = &key_value_type.get_fields()[0];
- assert!(
- key_type.is_primitive(),
- "Map key type is expected to be a primitive type, but
found {key_type:?}"
- );
let key_reader = self.reader_tree(
key_type.clone(),
path,
@@ -1890,6 +1886,115 @@ mod tests {
}
}
+ #[test]
+ #[cfg(feature = "arrow")]
+ fn test_compound_map_key() {
+ use crate::arrow::ArrowWriter;
+ use arrow_array::builder::{Int32Builder, ListBuilder};
+ use arrow_array::{
+ Array, ArrayRef, Int32Array, MapArray, RecordBatch, StringArray,
StructArray,
+ };
+ use arrow_buffer::OffsetBuffer;
+ use arrow_schema::{DataType, Field as ArrowField, Fields};
+
+ let map_array = |keys: ArrayRef, offsets: Vec<i32>, values: Vec<i32>| {
+ let entries = StructArray::from(vec![
+ (
+ Arc::new(ArrowField::new("key", keys.data_type().clone(),
false)),
+ keys,
+ ),
+ (
+ Arc::new(ArrowField::new("value", DataType::Int32, true)),
+ Arc::new(Int32Array::from(values)) as ArrayRef,
+ ),
+ ]);
+ let entries_field = ArrowField::new("key_value",
entries.data_type().clone(), false);
+ let map = MapArray::try_new(
+ Arc::new(entries_field),
+ OffsetBuffer::new(offsets.into()),
+ entries,
+ None,
+ false,
+ )
+ .unwrap();
+ Arc::new(map) as ArrayRef
+ };
+
+ // struct keys: [{(1, "a") -> 10, (2, "b") -> 20}, {}, {(3, "c") ->
30}]
+ let struct_keys = StructArray::new(
+ Fields::from(vec![
+ ArrowField::new("id", DataType::Int32, false),
+ ArrowField::new("name", DataType::Utf8, false),
+ ]),
+ vec![
+ Arc::new(Int32Array::from(vec![1, 2, 3])),
+ Arc::new(StringArray::from(vec!["a", "b", "c"])),
+ ],
+ None,
+ );
+ let struct_map = map_array(Arc::new(struct_keys), vec![0, 2, 2, 3],
vec![10, 20, 30]);
+
+ // list keys: [{[1, 2] -> 10, [3] -> 20}, {}, {[] -> 30}]
+ let mut list_keys = ListBuilder::new(Int32Builder::new());
+ list_keys.append_value([Some(1), Some(2)]);
+ list_keys.append_value([Some(3)]);
+ list_keys.append_value([]);
+ let list_map = map_array(
+ Arc::new(list_keys.finish()),
+ vec![0, 2, 2, 3],
+ vec![10, 20, 30],
+ );
+
+ let batch =
+ RecordBatch::try_from_iter([("struct_map", struct_map),
("list_map", list_map)])
+ .unwrap();
+ let mut buffer = Vec::new();
+ let mut writer = ArrowWriter::try_new(&mut buffer, batch.schema(),
None).unwrap();
+ writer.write(&batch).unwrap();
+ writer.close().unwrap();
+
+ let reader = SerializedFileReader::new(Bytes::from(buffer)).unwrap();
+ let rows: Vec<Row> = reader
+ .get_row_iter(None)
+ .unwrap()
+ .map(|row| row.unwrap())
+ .collect();
+
+ let key = |id: i32, name: &str| {
+ group![
+ ("id".to_string(), Field::Int(id)),
+ ("name".to_string(), Field::Str(name.to_string()))
+ ]
+ };
+ let expected_rows = vec![
+ row![
+ (
+ "struct_map".to_string(),
+ map![(key(1, "a"), Field::Int(10)), (key(2, "b"),
Field::Int(20))]
+ ),
+ (
+ "list_map".to_string(),
+ map![
+ (list![Field::Int(1), Field::Int(2)], Field::Int(10)),
+ (list![Field::Int(3)], Field::Int(20))
+ ]
+ ),
+ ],
+ row![
+ ("struct_map".to_string(), map![]),
+ ("list_map".to_string(), map![]),
+ ],
+ row![
+ (
+ "struct_map".to_string(),
+ map![(key(3, "c"), Field::Int(30))]
+ ),
+ ("list_map".to_string(), map![(list![], Field::Int(30))]),
+ ],
+ ];
+ assert_eq!(rows, expected_rows);
+ }
+
fn test_file_reader_rows(file_name: &str, schema: Option<Type>) ->
Result<Vec<Row>> {
let file = get_test_file(file_name);
let file_reader: Box<dyn FileReader> =
Box::new(SerializedFileReader::new(file)?);