@@ -319,11 +319,75 @@ class ParquetReaderTest : public TempFileTestBase {
319319 auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_);
320320 auto outfile = io.fs ()->OpenOutputStream (temp_parquet_file_).ValueOrDie ();
321321
322+ // Build ArrowWriterProperties to store the Arrow schema in ARROW:schema metadata
323+ auto arrow_writer_props =
324+ ::parquet::ArrowWriterProperties::Builder ().store_schema()->build();
325+
322326 // write a single row group so that one batch holds every row
323- ASSERT_TRUE (::parquet::arrow::WriteTable (*table, ::arrow::default_memory_pool (),
324- outfile, table->num_rows ())
327+ ASSERT_TRUE (::parquet::arrow::WriteTable (
328+ *table, ::arrow::default_memory_pool (), outfile, table->num_rows (),
329+ ::parquet::default_writer_properties (), arrow_writer_props)
330+ .ok());
331+ ASSERT_TRUE (outfile->Close ().ok ());
332+
333+ // Verify ARROW:schema is stored
334+ auto input_file = io.fs ()->OpenInputFile (temp_parquet_file_).ValueOrDie ();
335+ auto metadata = ::parquet::ReadMetaData (input_file);
336+ const auto & kv_metadata = metadata->key_value_metadata ();
337+ ASSERT_TRUE (kv_metadata != nullptr );
338+ ASSERT_TRUE (kv_metadata->FindKey (" ARROW:schema" ) >= 0 )
339+ << " ARROW:schema not found in file metadata" ;
340+ }
341+
342+ // Writes a mixed list/large_list parquet file with stored ARROW:schema.
343+ void CreateMixedListParquetFileWithArrowSchema () {
344+ const std::string kParquetFieldIdKey = " PARQUET:field_id" ;
345+ auto arrow_schema = ::arrow::schema (
346+ {::arrow::field (" id" , ::arrow::int32 (), /* nullable=*/ false ,
347+ ::arrow::KeyValueMetadata::Make ({kParquetFieldIdKey }, {" 1" })),
348+ ::arrow::field(
349+ " small_lists" ,
350+ ::arrow::list (::arrow::field(
351+ " element" , ::arrow::int32(), /* nullable=*/ true,
352+ ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey }, {" 101" }))),
353+ /* nullable=*/ true,
354+ ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey }, {" 2" })),
355+ ::arrow::field(
356+ " large_lists" ,
357+ ::arrow::large_list (::arrow::field(
358+ " element" , ::arrow::int32(), /* nullable=*/ true,
359+ ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey }, {" 102" }))),
360+ /* nullable=*/ true,
361+ ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey }, {" 3" }))});
362+ auto batch = ::arrow::RecordBatch::FromStructArray(
363+ ::arrow::json::ArrayFromJSONString (
364+ ::arrow::struct_ (arrow_schema->fields ()),
365+ R"([[1 , [10 , 20 ], [100 , 200 ]], [2 , [30 ], [300 ]]])")
366+ .ValueOrDie())
367+ .ValueOrDie();
368+ auto table = ::arrow::Table::FromRecordBatches(arrow_schema, {batch}).ValueOrDie();
369+
370+ auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_);
371+ auto outfile = io.fs()->OpenOutputStream(temp_parquet_file_).ValueOrDie();
372+
373+ // Build ArrowWriterProperties to store the Arrow schema in ARROW:schema metadata
374+ auto arrow_writer_props =
375+ ::parquet::ArrowWriterProperties::Builder ().store_schema()->build();
376+
377+ // write a single row group so that one batch holds every row
378+ ASSERT_TRUE (::parquet::arrow::WriteTable (
379+ *table, ::arrow::default_memory_pool (), outfile, table->num_rows (),
380+ ::parquet::default_writer_properties (), arrow_writer_props)
325381 .ok());
326382 ASSERT_TRUE (outfile->Close ().ok ());
383+
384+ // Verify ARROW:schema is stored
385+ auto input_file = io.fs ()->OpenInputFile (temp_parquet_file_).ValueOrDie ();
386+ auto metadata = ::parquet::ReadMetaData (input_file);
387+ const auto & kv_metadata = metadata->key_value_metadata ();
388+ ASSERT_TRUE (kv_metadata != nullptr );
389+ ASSERT_TRUE (kv_metadata->FindKey (" ARROW:schema" ) >= 0 )
390+ << " ARROW:schema not found in file metadata" ;
327391 }
328392
329393 void VerifyNextBatch (Reader& reader, std::string_view expected_json) {
@@ -655,6 +719,110 @@ TEST_F(ParquetReaderTest, ReadListAsLargeListWithArrowSchema) {
655719 }
656720}
657721
722+ TEST_F (ParquetReaderTest, ReadMixedListAndLargeListWithArrowSchema) {
723+ // Reading a file with both list and large_list fields (stored via ARROW:schema
724+ // metadata) must report an output schema that describes the actual types of each field,
725+ // preserving the per-field list type. This tests the per-field mapping logic.
726+ CreateMixedListParquetFileWithArrowSchema ();
727+
728+ auto schema = std::make_shared<Schema>(std::vector<SchemaField>{
729+ SchemaField::MakeRequired (1 , " id" , int32 ()),
730+ SchemaField::MakeOptional (2 , " small_lists" ,
731+ std::make_shared<ListType>(SchemaField::MakeOptional (
732+ /* field_id=*/ 101 , " element" , int32 ()))),
733+ SchemaField::MakeOptional (3 , " large_lists" ,
734+ std::make_shared<ListType>(SchemaField::MakeOptional (
735+ /* field_id=*/ 102 , " element" , int32 ())))});
736+
737+ // Read with default setting (use_large_list=false)
738+ auto reader_result = ReaderFactoryRegistry::Open (
739+ FileFormatType::kParquet ,
740+ {.path = temp_parquet_file_, .io = file_io_, .projection = schema});
741+ ASSERT_THAT (reader_result, IsOk ());
742+ auto reader = std::move (reader_result.value ());
743+
744+ // Verify the output schema has the correct per-field list types
745+ auto schema_result = reader->Schema ();
746+ ASSERT_THAT (schema_result, IsOk ());
747+ auto arrow_c_schema = std::move (schema_result.value ());
748+ auto arrow_type = ::arrow::ImportType (&arrow_c_schema).ValueOrDie ();
749+
750+ // small_lists should be LIST (as stored in the file)
751+ ASSERT_EQ (arrow_type->field (1 )->type ()->id (), ::arrow::Type::LIST )
752+ << " small_lists field should be LIST" ;
753+
754+ // large_lists should be LARGE_LIST (as stored in the file)
755+ ASSERT_EQ (arrow_type->field (2 )->type ()->id (), ::arrow::Type::LARGE_LIST )
756+ << " large_lists field should be LARGE_LIST" ;
757+
758+ // Verify the arrays are readable with the reported schema
759+ auto data = reader->Next ();
760+ ASSERT_THAT (data, IsOk ());
761+ ASSERT_TRUE (data.value ().has_value ());
762+ auto arrow_c_array = data.value ().value ();
763+
764+ auto import_result = ::arrow::ImportArray (&arrow_c_array, arrow_type);
765+ ASSERT_TRUE (import_result.ok ()) << import_result.status ().ToString ();
766+ auto arrow_array = import_result.ValueOrDie ();
767+ ASSERT_TRUE (arrow_array->ValidateFull ().ok ());
768+
769+ const auto & struct_array =
770+ internal::checked_cast<const ::arrow::StructArray&>(*arrow_array);
771+ ASSERT_EQ (struct_array.length (), 2 );
772+
773+ // Verify the field types in the actual arrays
774+ ASSERT_EQ (struct_array.field (1 )->type ()->id (), ::arrow::Type::LIST );
775+ ASSERT_EQ (struct_array.field (2 )->type ()->id (), ::arrow::Type::LARGE_LIST );
776+
777+ ASSERT_NO_FATAL_FAILURE (VerifyExhausted (*reader));
778+ }
779+
780+ TEST_F (ParquetReaderTest, ReadRenamedLargeListColumnWithArrowSchema) {
781+ // The projected column has a different name than the file but the same field id. The
782+ // output schema must be aligned to the reader by field id, not by name: the file stores
783+ // this column as large_list (via ARROW:schema), so a name-based match would miss the
784+ // rename, report the column as list while the array is large_list, and fail to import
785+ // it.
786+ CreateMixedListParquetFileWithArrowSchema ();
787+
788+ // field id 3 is stored in the file as a large_list named "large_lists"; project it
789+ // under a different name to force matching by field id
790+ auto schema = std::make_shared<Schema>(std::vector<SchemaField>{
791+ SchemaField::MakeOptional (3 , " renamed" ,
792+ std::make_shared<ListType>(SchemaField::MakeOptional (
793+ /* field_id=*/ 102 , " element" , int32 ())))});
794+
795+ // read with the default use_large_list=false, so only field-id matching can preserve
796+ // large_list
797+ auto reader_result = ReaderFactoryRegistry::Open (
798+ FileFormatType::kParquet ,
799+ {.path = temp_parquet_file_, .io = file_io_, .projection = schema});
800+ ASSERT_THAT (reader_result, IsOk ());
801+ auto reader = std::move (reader_result.value ());
802+
803+ auto schema_result = reader->Schema ();
804+ ASSERT_THAT (schema_result, IsOk ());
805+ auto arrow_c_schema = std::move (schema_result.value ());
806+ auto arrow_type = ::arrow::ImportType (&arrow_c_schema).ValueOrDie ();
807+
808+ // the renamed column keeps the file's large_list type because it is matched by field id
809+ ASSERT_EQ (arrow_type->field (0 )->type ()->id (), ::arrow::Type::LARGE_LIST )
810+ << " renamed column must keep the file's large_list type, matched by field id" ;
811+
812+ // importing the produced array against the reported schema must succeed; a name-based
813+ // mismatch would report list here while the array is large_list and this import would
814+ // fail
815+ auto data = reader->Next ();
816+ ASSERT_THAT (data, IsOk ());
817+ ASSERT_TRUE (data.value ().has_value ());
818+ auto arrow_c_array = data.value ().value ();
819+ auto import_result = ::arrow::ImportArray (&arrow_c_array, arrow_type);
820+ ASSERT_TRUE (import_result.ok ()) << import_result.status ().ToString ();
821+ ASSERT_TRUE (import_result.ValueOrDie ()->ValidateFull ().ok ());
822+
823+ ASSERT_NO_FATAL_FAILURE (VerifyExhausted (*reader));
824+ }
825+
658826TEST_F (ParquetReaderTest, ReadSplit) {
659827 CreateSplitParquetFile ();
660828
0 commit comments