@@ -704,6 +704,63 @@ TEST(AppendDatumToBuilderTest, StructWithMissingDefaultFields) {
704704 avro_data, expected_json));
705705}
706706
707+ TEST (AppendDatumToBuilderTest, NestedListOfListStructWithMissingDefaultField) {
708+ // A default on a struct nested two collection levels deep must still be filled, which
709+ // requires recursing through both list builders rather than only an immediate struct.
710+ auto inner_struct = std::make_shared<StructType>(std::vector<SchemaField>{
711+ SchemaField::MakeRequired (5 , " x" , iceberg::int32 ()),
712+ SchemaField (6 , " y" , iceberg::int64 (), /* optional=*/ false , /* doc=*/ {},
713+ std::make_shared<const Literal>(Literal::Long (7 ))),
714+ });
715+ auto inner_list =
716+ std::make_shared<ListType>(SchemaField::MakeRequired (4 , " element" , inner_struct));
717+ auto outer_list =
718+ std::make_shared<ListType>(SchemaField::MakeRequired (3 , " element" , inner_list));
719+ Schema iceberg_schema ({
720+ SchemaField::MakeRequired (1 , " id" , iceberg::int32 ()),
721+ SchemaField::MakeRequired (2 , " matrix" , outer_list),
722+ });
723+
724+ // The Avro schema's innermost record only has `x`, so `y` comes from its default.
725+ std::string avro_schema_json = R"( {
726+ "type": "record",
727+ "name": "outer",
728+ "fields": [
729+ {"name": "id", "type": "int", "field-id": 1},
730+ {"name": "matrix", "field-id": 2, "type": {
731+ "type": "array", "element-id": 3, "items": {
732+ "type": "array", "element-id": 4, "items": {
733+ "type": "record", "name": "point",
734+ "fields": [{"name": "x", "type": "int", "field-id": 5}]
735+ }
736+ }
737+ }}
738+ ]
739+ })" ;
740+ auto avro_schema = ::avro::compileJsonSchemaFromString (avro_schema_json);
741+
742+ std::vector<::avro::GenericDatum> avro_data;
743+ ::avro::GenericDatum avro_datum (avro_schema.root ());
744+ auto & record = avro_datum.value <::avro::GenericRecord>();
745+ record.fieldAt (0 ).value <int32_t >() = 1 ;
746+ auto & outer_array = record.fieldAt (1 ).value <::avro::GenericArray>();
747+ ::avro::GenericDatum inner_datum (avro_schema.root ()->leafAt (1 )->leafAt (0 ));
748+ auto & inner_array = inner_datum.value <::avro::GenericArray>();
749+ for (int32_t x : {10 , 20 }) {
750+ ::avro::GenericDatum point_datum (avro_schema.root ()->leafAt (1 )->leafAt (0 )->leafAt (0 ));
751+ point_datum.value <::avro::GenericRecord>().fieldAt (0 ).value <int32_t >() = x;
752+ inner_array.value ().push_back (point_datum);
753+ }
754+ outer_array.value ().push_back (inner_datum);
755+ avro_data.push_back (avro_datum);
756+
757+ const std::string expected_json = R"( [
758+ {"id": 1, "matrix": [[{"x": 10, "y": 7}, {"x": 20, "y": 7}]]}
759+ ])" ;
760+ ASSERT_NO_FATAL_FAILURE (VerifyAppendDatumToBuilder (iceberg_schema, avro_schema.root (),
761+ avro_data, expected_json));
762+ }
763+
707764TEST (AppendDefaultToBuilderTest, AppendsValue) {
708765 ::arrow::Int64Builder builder;
709766 ASSERT_THAT (AppendDefaultToBuilder (Literal::Long (42 ), &builder), IsOk ());
@@ -758,6 +815,45 @@ TEST(AppendDefaultToBuilderTest, ReusesPreparedScalar) {
758815 ASSERT_EQ (long_array.Value (1 ), 42 );
759816}
760817
818+ TEST (AppendDefaultToBuilderTest, PreparesScalarUnderNestedCollections) {
819+ // A default under `list<list<struct<...>>>` must be prepared too, so decoding reuses
820+ // the cached scalar instead of rebuilding it for every element.
821+ auto pool = ::arrow::default_memory_pool ();
822+ auto leaf = std::make_shared<::arrow::Int64Builder>(pool);
823+ auto point_builder = std::make_shared<::arrow::StructBuilder>(
824+ ::arrow::struct_ ({::arrow::field (" y" , ::arrow::int64 ())}), pool,
825+ std::vector<std::shared_ptr<::arrow::ArrayBuilder>>{leaf});
826+ auto inner_list = std::make_shared<::arrow::ListBuilder>(pool, point_builder);
827+ auto outer_list = std::make_shared<::arrow::ListBuilder>(pool, inner_list);
828+ ::arrow::StructBuilder root_builder (
829+ ::arrow::struct_ ({::arrow::field (" matrix" , outer_list->type ())}), pool,
830+ std::vector<std::shared_ptr<::arrow::ArrayBuilder>>{outer_list});
831+
832+ FieldProjection leaf_default;
833+ leaf_default.kind = FieldProjection::Kind::kDefault ;
834+ leaf_default.from = Literal::Long (7 );
835+
836+ FieldProjection point_projection;
837+ point_projection.kind = FieldProjection::Kind::kProjected ;
838+ point_projection.children .push_back (leaf_default);
839+
840+ FieldProjection inner_element;
841+ inner_element.kind = FieldProjection::Kind::kProjected ;
842+ inner_element.children .push_back (point_projection);
843+
844+ FieldProjection outer_element;
845+ outer_element.kind = FieldProjection::Kind::kProjected ;
846+ outer_element.children .push_back (inner_element);
847+
848+ SchemaProjection schema_projection;
849+ schema_projection.fields .push_back (outer_element);
850+ ASSERT_THAT (PrepareDefaultScalars (schema_projection, &root_builder), IsOk ());
851+
852+ const auto & prepared = schema_projection.fields [0 ].children [0 ].children [0 ].children [0 ];
853+ ASSERT_NE (dynamic_cast <const AvroDefaultAttributes*>(prepared.attributes .get ()),
854+ nullptr );
855+ }
856+
761857TEST (AppendDatumToBuilderTest, NestedStructWithMissingOptionalFields) {
762858 Schema iceberg_schema ({
763859 SchemaField::MakeRequired (1 , " id" , iceberg::int32 ()),
0 commit comments