@@ -646,6 +646,156 @@ TEST_F(ParquetFileBatchReaderTest, TestReadSchemaWithMapSelectedKeysMetadata) {
646646 << " expected: " << expected_array->ToString () << " \n actual: " << result_array->ToString ();
647647}
648648
649+ TEST_F (ParquetFileBatchReaderTest, TestNestedListTimestampTimezoneAndMapFieldName) {
650+ const std::string timezone = " Asia/Shanghai" ;
651+ paimon::test::TimezoneGuard timezone_guard (timezone);
652+
653+ auto write_attrs_type =
654+ std::make_shared<arrow::MapType>(arrow::field (" key" , arrow::utf8 (), /* nullable=*/ false ),
655+ arrow::field (" attrs" , arrow::utf8 ()));
656+ auto write_element_type = arrow::struct_ ({
657+ arrow::field (" key" , arrow::utf8 ()),
658+ arrow::field (" attrs" , write_attrs_type),
659+ arrow::field (" updated_at" , arrow::timestamp (arrow::TimeUnit::MICRO , timezone)),
660+ });
661+ auto write_schema = arrow::schema (
662+ {arrow::field (" annotations" , arrow::list (arrow::field (" element" , write_element_type)))});
663+
664+ const std::string data_json = R"( [
665+ [[ ["ann-1", [["source", "model"]], "2026-07-16 12:00:00.000001"] ]],
666+ [[ ["ann-2", [], "2026-07-16 12:00:00.000002"],
667+ ["ann-3", null, null] ]],
668+ [null]
669+ ])" ;
670+ auto write_array = std::dynamic_pointer_cast<arrow::StructArray>(
671+ arrow::ipc::internal::json::ArrayFromJSON (arrow::struct_ (write_schema->fields ()), data_json)
672+ .ValueOrDie ());
673+ WriteArray (file_path_, write_array, write_schema, /* write_batch_size=*/ write_array->length (),
674+ /* enable_dictionary=*/ false , /* max_row_group_length=*/ write_array->length ());
675+
676+ auto read_element_type = arrow::struct_ ({
677+ arrow::field (" key" , arrow::utf8 ()),
678+ arrow::field (" attrs" , arrow::map (arrow::utf8 (), arrow::utf8 ())),
679+ arrow::field (" updated_at" , arrow::timestamp (arrow::TimeUnit::MICRO , timezone)),
680+ });
681+ auto read_schema = arrow::schema (
682+ {arrow::field (" annotations" , arrow::list (arrow::field (" element" , read_element_type)))});
683+ auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
684+ arrow::ipc::internal::json::ArrayFromJSON (arrow::struct_ (read_schema->fields ()), data_json)
685+ .ValueOrDie ());
686+
687+ auto parquet_batch_reader =
688+ PrepareParquetFileBatchReader (file_path_, read_schema, /* predicate=*/ nullptr ,
689+ /* selection_bitmap=*/ std::nullopt , /* batch_size=*/ 2 );
690+ ASSERT_OK_AND_ASSIGN (auto result_array, paimon::test::ReadResultCollector::CollectResult (
691+ parquet_batch_reader.get ()));
692+ auto expected_chunked_array = arrow::ChunkedArray::Make ({expected_array}).ValueOrDie ();
693+ ASSERT_TRUE (result_array->Equals (expected_chunked_array))
694+ << " expected: " << expected_chunked_array->ToString ()
695+ << " \n actual: " << result_array->ToString ();
696+
697+ auto projected_element_type = arrow::struct_ ({
698+ arrow::field (" key" , arrow::utf8 ()),
699+ arrow::field (" attrs" , arrow::map (arrow::utf8 (), arrow::utf8 ())),
700+ });
701+ auto partial_read_schema = arrow::schema ({arrow::field (
702+ " annotations" , arrow::list (arrow::field (" element" , projected_element_type)))});
703+ auto c_partial_read_schema = std::make_unique<ArrowSchema>();
704+ ASSERT_TRUE (arrow::ExportSchema (*partial_read_schema, c_partial_read_schema.get ()).ok ());
705+ ASSERT_NOK_WITH_MSG (
706+ parquet_batch_reader->SetReadSchema (c_partial_read_schema.get (), /* predicate=*/ nullptr ,
707+ /* selection_bitmap=*/ std::nullopt ),
708+ " Parquet does not support partial projection inside list/map" );
709+
710+ auto mismatched_element_type = arrow::struct_ ({
711+ arrow::field (" key" , arrow::utf8 ()),
712+ arrow::field (" attrs" , arrow::map (arrow::utf8 (), arrow::utf8 ())),
713+ arrow::field (" updated_at" , arrow::utf8 ()),
714+ });
715+ auto mismatched_read_schema = arrow::schema ({arrow::field (
716+ " annotations" , arrow::list (arrow::field (" element" , mismatched_element_type)))});
717+ auto c_mismatched_read_schema = std::make_unique<ArrowSchema>();
718+ ASSERT_TRUE (arrow::ExportSchema (*mismatched_read_schema, c_mismatched_read_schema.get ()).ok ());
719+ ASSERT_NOK_WITH_MSG (
720+ parquet_batch_reader->SetReadSchema (c_mismatched_read_schema.get (), /* predicate=*/ nullptr ,
721+ /* selection_bitmap=*/ std::nullopt ),
722+ " Parquet does not support partial projection inside list/map" );
723+
724+ auto unsupported_timestamp_element_type = arrow::struct_ ({
725+ arrow::field (" key" , arrow::utf8 ()),
726+ arrow::field (" attrs" , arrow::map (arrow::utf8 (), arrow::utf8 ())),
727+ arrow::field (" updated_at" , arrow::timestamp (arrow::TimeUnit::NANO , timezone)),
728+ });
729+ auto unsupported_timestamp_schema = arrow::schema ({arrow::field (
730+ " annotations" , arrow::list (arrow::field (" element" , unsupported_timestamp_element_type)))});
731+ auto c_unsupported_timestamp_schema = std::make_unique<ArrowSchema>();
732+ ASSERT_TRUE (
733+ arrow::ExportSchema (*unsupported_timestamp_schema, c_unsupported_timestamp_schema.get ())
734+ .ok ());
735+ ASSERT_NOK_WITH_MSG (parquet_batch_reader->SetReadSchema (c_unsupported_timestamp_schema.get (),
736+ /* predicate=*/ nullptr ,
737+ /* selection_bitmap=*/ std::nullopt ),
738+ " Parquet does not support partial projection inside list/map" );
739+ }
740+
741+ TEST_F (ParquetFileBatchReaderTest, TestNestedTimestampSecondReadFromMilliFile) {
742+ const std::string timezone = " Asia/Shanghai" ;
743+ paimon::test::TimezoneGuard timezone_guard (timezone);
744+
745+ // Parquet has no second-precision timestamp, so the writer stores second timestamps as
746+ // milliseconds. Reading them back with a second-precision schema must cast milli to second,
747+ // including for timestamp leaves nested inside list/struct/map.
748+ auto event_type = arrow::struct_ ({
749+ arrow::field (" name" , arrow::utf8 ()),
750+ arrow::field (" ts_sec" , arrow::timestamp (arrow::TimeUnit::SECOND )),
751+ arrow::field (" ts_tz_sec" , arrow::timestamp (arrow::TimeUnit::SECOND , timezone)),
752+ });
753+ auto schema = arrow::schema ({
754+ arrow::field (" events" , arrow::list (arrow::field (" element" , event_type))),
755+ arrow::field (" marks" , arrow::map (arrow::utf8 (), arrow::timestamp (arrow::TimeUnit::SECOND ))),
756+ });
757+
758+ const std::string data_json = R"( [
759+ [[ ["e-1", "2026-07-16 12:00:01", "2026-07-16 12:00:02"] ],
760+ [["begin", "2026-07-16 12:00:03"]]],
761+ [[ ["e-2", "2026-07-16 12:00:04", null],
762+ ["e-3", null, "2026-07-16 12:00:05"] ], []],
763+ [[null], null]
764+ ])" ;
765+ auto write_array = std::dynamic_pointer_cast<arrow::StructArray>(
766+ arrow::ipc::internal::json::ArrayFromJSON (arrow::struct_ (schema->fields ()), data_json)
767+ .ValueOrDie ());
768+ WriteArray (file_path_, write_array, schema, /* write_batch_size=*/ write_array->length (),
769+ /* enable_dictionary=*/ false , /* max_row_group_length=*/ write_array->length ());
770+
771+ auto parquet_batch_reader =
772+ PrepareParquetFileBatchReader (file_path_, schema, /* predicate=*/ nullptr ,
773+ /* selection_bitmap=*/ std::nullopt , /* batch_size=*/ 2 );
774+
775+ // The nested second timestamps are physically stored as milliseconds in the file.
776+ ASSERT_OK_AND_ASSIGN (auto c_file_schema, parquet_batch_reader->GetFileSchema ());
777+ auto file_schema = arrow::ImportSchema (c_file_schema.get ()).ValueOr (nullptr );
778+ ASSERT_TRUE (file_schema);
779+ auto file_event_type =
780+ static_cast <const arrow::ListType&>(*file_schema->field (0 )->type ()).value_type ();
781+ ASSERT_EQ (arrow::Type::STRUCT , file_event_type->id ());
782+ ASSERT_EQ (arrow::TimeUnit::MILLI ,
783+ static_cast <const arrow::TimestampType&>(*file_event_type->field (1 )->type ()).unit ());
784+ ASSERT_EQ (arrow::TimeUnit::MILLI ,
785+ static_cast <const arrow::TimestampType&>(*file_event_type->field (2 )->type ()).unit ());
786+ auto file_mark_type =
787+ static_cast <const arrow::MapType&>(*file_schema->field (1 )->type ()).item_type ();
788+ ASSERT_EQ (arrow::TimeUnit::MILLI ,
789+ static_cast <const arrow::TimestampType&>(*file_mark_type).unit ());
790+
791+ ASSERT_OK_AND_ASSIGN (
792+ std::shared_ptr<arrow::ChunkedArray> result_array,
793+ paimon::test::ReadResultCollector::CollectResult (parquet_batch_reader.get ()));
794+ auto expected_array = arrow::ChunkedArray::Make ({write_array}).ValueOrDie ();
795+ ASSERT_TRUE (result_array->Equals (expected_array))
796+ << " expected: " << expected_array->ToString () << " \n actual: " << result_array->ToString ();
797+ }
798+
649799TEST_F (ParquetFileBatchReaderTest, TestGetFileSchemaWithFieldId) {
650800 std::string file_name = paimon::test::GetDataDir () +
651801 " parquet/parquet_append_table.db/parquet_append_table/bucket-0/"
0 commit comments