Skip to content

Commit bec0ceb

Browse files
Angelia-Wangjoeyyjwang
andauthored
fix(manifest): write null instead of empty list/map for unset optional DataFile fields (#934)
AppendIntList, AppendIntMap, and AppendBinaryMap in src/iceberg/arrow_row_builder.cc always call ArrowArrayFinishElement() after appending entries, even when the input container is empty. This produced an empty-but-non-null list/map element rather than a null element whenever one of DataFile's optional list/map fields (split_offsets, equality_ids, column_sizes, value_counts, null_value_counts, nan_value_counts, lower_bounds, upper_bounds) was unset. Add an empty check to each of these helpers that delegates to the existing AppendNull() instead of finishing an empty element. AppendStringMap (used for required properties-style maps) is intentionally left unchanged. Add unit test coverage in arrow_row_builder_test.cc asserting that AppendIntList/AppendIntMap/AppendBinaryMap write a null element (not an empty one) for empty input, alongside existing non-empty-input coverage. Co-authored-by: joeyyjwang <joeyyjwang@tencent.com>
1 parent 307adb1 commit bec0ceb

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

‎src/iceberg/arrow_row_builder.cc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ Status AppendBytes(ArrowArray* array, std::span<const uint8_t> value) {
136136
}
137137

138138
Status AppendIntList(ArrowArray* array, const std::vector<int32_t>& values) {
139+
if (values.empty()) {
140+
return AppendNull(array);
141+
}
139142
auto list_array = array->children[0];
140143
for (const auto& value : values) {
141144
ICEBERG_NANOARROW_RETURN_UNEXPECTED(
@@ -146,6 +149,9 @@ Status AppendIntList(ArrowArray* array, const std::vector<int32_t>& values) {
146149
}
147150

148151
Status AppendIntList(ArrowArray* array, const std::vector<int64_t>& values) {
152+
if (values.empty()) {
153+
return AppendNull(array);
154+
}
149155
auto list_array = array->children[0];
150156
for (const auto& value : values) {
151157
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendInt(list_array, value));
@@ -174,6 +180,9 @@ Status AppendStringMap(ArrowArray* array,
174180
}
175181

176182
Status AppendIntMap(ArrowArray* array, const std::map<int32_t, int64_t>& entries) {
183+
if (entries.empty()) {
184+
return AppendNull(array);
185+
}
177186
auto map_array = array->children[0];
178187
if (map_array->n_children != 2) {
179188
return InvalidArrowData("Map array must have exactly 2 children.");
@@ -191,6 +200,9 @@ Status AppendIntMap(ArrowArray* array, const std::map<int32_t, int64_t>& entries
191200

192201
Status AppendBinaryMap(ArrowArray* array,
193202
const std::map<int32_t, std::vector<uint8_t>>& entries) {
203+
if (entries.empty()) {
204+
return AppendNull(array);
205+
}
194206
auto map_array = array->children[0];
195207
if (map_array->n_children != 2) {
196208
return InvalidArrowData("Map array must have exactly 2 children.");

‎src/iceberg/test/arrow_row_builder_test.cc‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,29 @@ std::shared_ptr<Schema> MakeTestSchema() {
5252
SchemaField::MakeRequired(7, "value", string())))});
5353
}
5454

55+
/// \brief A schema exercising the list/map append helpers backing Iceberg's
56+
/// optional DataFile fields (e.g. equality_ids, split_offsets, column_sizes,
57+
/// lower_bounds): list<int32>, list<int64>, map<int32, int64>, and
58+
/// map<int32, binary>.
59+
std::shared_ptr<Schema> MakeListMapTestSchema() {
60+
return std::make_shared<Schema>(std::vector<SchemaField>{
61+
SchemaField::MakeOptional(1, "int32_list",
62+
list(SchemaField::MakeRequired(
63+
2, std::string(ListType::kElementName), int32()))),
64+
SchemaField::MakeOptional(3, "int64_list",
65+
list(SchemaField::MakeRequired(
66+
4, std::string(ListType::kElementName), int64()))),
67+
SchemaField::MakeOptional(
68+
5, "int_map",
69+
map(SchemaField::MakeRequired(6, std::string(MapType::kKeyName), int32()),
70+
SchemaField::MakeRequired(7, std::string(MapType::kValueName), int64()))),
71+
SchemaField::MakeOptional(
72+
8, "binary_map",
73+
map(SchemaField::MakeRequired(9, std::string(MapType::kKeyName), int32()),
74+
SchemaField::MakeRequired(10, std::string(MapType::kValueName),
75+
binary())))});
76+
}
77+
5578
/// \brief Finish a builder and import the result into an Arrow RecordBatch.
5679
std::shared_ptr<::arrow::RecordBatch> FinishAndImport(ArrowRowBuilder builder,
5780
const Schema& schema) {
@@ -178,4 +201,86 @@ TEST(ArrowRowBuilderTest, ColumnIndexOutOfRangeReturnsNull) {
178201
EXPECT_EQ(builder.column(5), nullptr);
179202
}
180203

204+
TEST(ArrowRowBuilderTest, AppendIntListWritesNullForEmptyInput) {
205+
auto schema = MakeListMapTestSchema();
206+
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
207+
208+
// Row 0: non-empty lists.
209+
ASSERT_THAT(AppendIntList(builder.column(0), std::vector<int32_t>{1, 2}), IsOk());
210+
ASSERT_THAT(AppendIntList(builder.column(1), std::vector<int64_t>{3, 4}), IsOk());
211+
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
212+
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
213+
ASSERT_THAT(builder.FinishRow(), IsOk());
214+
215+
// Row 1: empty lists must be encoded as null, not an empty list.
216+
ASSERT_THAT(AppendIntList(builder.column(0), std::vector<int32_t>{}), IsOk());
217+
ASSERT_THAT(AppendIntList(builder.column(1), std::vector<int64_t>{}), IsOk());
218+
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
219+
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
220+
ASSERT_THAT(builder.FinishRow(), IsOk());
221+
222+
auto batch = FinishAndImport(std::move(builder), *schema);
223+
ASSERT_EQ(batch->num_rows(), 2);
224+
225+
auto int32_list = std::static_pointer_cast<::arrow::ListArray>(batch->column(0));
226+
EXPECT_FALSE(int32_list->IsNull(0));
227+
EXPECT_TRUE(int32_list->IsNull(1));
228+
229+
auto int64_list = std::static_pointer_cast<::arrow::ListArray>(batch->column(1));
230+
EXPECT_FALSE(int64_list->IsNull(0));
231+
EXPECT_TRUE(int64_list->IsNull(1));
232+
}
233+
234+
TEST(ArrowRowBuilderTest, AppendIntMapWritesNullForEmptyInput) {
235+
auto schema = MakeListMapTestSchema();
236+
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
237+
238+
// Row 0: a non-empty map.
239+
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
240+
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
241+
ASSERT_THAT(AppendIntMap(builder.column(2), {{1, 100}}), IsOk());
242+
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
243+
ASSERT_THAT(builder.FinishRow(), IsOk());
244+
245+
// Row 1: an empty map must be encoded as null, not an empty map.
246+
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
247+
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
248+
ASSERT_THAT(AppendIntMap(builder.column(2), {}), IsOk());
249+
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
250+
ASSERT_THAT(builder.FinishRow(), IsOk());
251+
252+
auto batch = FinishAndImport(std::move(builder), *schema);
253+
ASSERT_EQ(batch->num_rows(), 2);
254+
255+
auto int_map = std::static_pointer_cast<::arrow::MapArray>(batch->column(2));
256+
EXPECT_FALSE(int_map->IsNull(0));
257+
EXPECT_TRUE(int_map->IsNull(1));
258+
}
259+
260+
TEST(ArrowRowBuilderTest, AppendBinaryMapWritesNullForEmptyInput) {
261+
auto schema = MakeListMapTestSchema();
262+
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
263+
264+
// Row 0: a non-empty map.
265+
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
266+
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
267+
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
268+
ASSERT_THAT(AppendBinaryMap(builder.column(3), {{1, {0x01, 0x02}}}), IsOk());
269+
ASSERT_THAT(builder.FinishRow(), IsOk());
270+
271+
// Row 1: an empty map must be encoded as null, not an empty map.
272+
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
273+
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
274+
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
275+
ASSERT_THAT(AppendBinaryMap(builder.column(3), {}), IsOk());
276+
ASSERT_THAT(builder.FinishRow(), IsOk());
277+
278+
auto batch = FinishAndImport(std::move(builder), *schema);
279+
ASSERT_EQ(batch->num_rows(), 2);
280+
281+
auto binary_map = std::static_pointer_cast<::arrow::MapArray>(batch->column(3));
282+
EXPECT_FALSE(binary_map->IsNull(0));
283+
EXPECT_TRUE(binary_map->IsNull(1));
284+
}
285+
181286
} // namespace iceberg

0 commit comments

Comments
 (0)