Skip to content

Commit 9a2e21d

Browse files
authored
fix: skip only null partition value rows in ParseDataFile (#756)
## Summary `ParseDataFile()` in `manifest_reader.cc` iterated over rows for each partition child field and used `break` to skip null partition values. `break` exits the entire inner row loop, so once a null value was encountered, **all subsequent rows for that partition column were skipped** and never had their partition values parsed. The fix changes `break` to `continue`, so only the current null row is skipped while the remaining rows are still processed. ## Changes - `src/iceberg/manifest/manifest_reader.cc`: `break` → `continue` in the partition-value row loop. - `src/iceberg/test/manifest_reader_test.cc`: add regression test `NullPartitionValueDoesNotSkipSubsequentRows` that writes a manifest with a null partition value before non-null ones and verifies the later rows are parsed correctly. ## Testing - New test passes for manifest versions V1/V2/V3. - Confirmed the test fails with the original `break` (`partition.num_fields()` is 0 instead of 1 for rows after the null one). - Full `manifest_test` suite passes (171 tests).
1 parent 92feae1 commit 9a2e21d

2 files changed

Lines changed: 110 additions & 33 deletions

File tree

src/iceberg/manifest/manifest_reader.cc

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -389,39 +389,40 @@ Result<std::vector<ManifestFile>> ParseManifestList(ArrowSchema* arrow_schema,
389389
}
390390

391391
Status ParsePartitionValues(ArrowArrayView* view, int64_t row_idx,
392+
const std::shared_ptr<PrimitiveType>& field_type,
392393
std::vector<ManifestEntry>& manifest_entries) {
394+
auto& partition = manifest_entries[row_idx].data_file->partition;
395+
if (view->storage_type == ArrowType::NANOARROW_TYPE_NA ||
396+
ArrowArrayViewIsNull(view, row_idx)) {
397+
partition.AddValue(Literal::Null(field_type));
398+
return {};
399+
}
393400
switch (view->storage_type) {
394-
case ArrowType::NANOARROW_TYPE_BOOL: {
395-
auto value = ArrowArrayViewGetUIntUnsafe(view, row_idx);
396-
manifest_entries[row_idx].data_file->partition.AddValue(
397-
Literal::Boolean(value != 0));
398-
} break;
399-
case ArrowType::NANOARROW_TYPE_INT32: {
400-
auto value = ArrowArrayViewGetIntUnsafe(view, row_idx);
401-
manifest_entries[row_idx].data_file->partition.AddValue(Literal::Int(value));
402-
} break;
403-
case ArrowType::NANOARROW_TYPE_INT64: {
404-
auto value = ArrowArrayViewGetIntUnsafe(view, row_idx);
405-
manifest_entries[row_idx].data_file->partition.AddValue(Literal::Long(value));
406-
} break;
407-
case ArrowType::NANOARROW_TYPE_FLOAT: {
408-
auto value = ArrowArrayViewGetDoubleUnsafe(view, row_idx);
409-
manifest_entries[row_idx].data_file->partition.AddValue(Literal::Float(value));
410-
} break;
411-
case ArrowType::NANOARROW_TYPE_DOUBLE: {
412-
auto value = ArrowArrayViewGetDoubleUnsafe(view, row_idx);
413-
manifest_entries[row_idx].data_file->partition.AddValue(Literal::Double(value));
414-
} break;
401+
case ArrowType::NANOARROW_TYPE_BOOL:
402+
partition.AddValue(
403+
Literal::Boolean(ArrowArrayViewGetUIntUnsafe(view, row_idx) != 0));
404+
break;
405+
case ArrowType::NANOARROW_TYPE_INT32:
406+
partition.AddValue(Literal::Int(ArrowArrayViewGetIntUnsafe(view, row_idx)));
407+
break;
408+
case ArrowType::NANOARROW_TYPE_INT64:
409+
partition.AddValue(Literal::Long(ArrowArrayViewGetIntUnsafe(view, row_idx)));
410+
break;
411+
case ArrowType::NANOARROW_TYPE_FLOAT:
412+
partition.AddValue(Literal::Float(ArrowArrayViewGetDoubleUnsafe(view, row_idx)));
413+
break;
414+
case ArrowType::NANOARROW_TYPE_DOUBLE:
415+
partition.AddValue(Literal::Double(ArrowArrayViewGetDoubleUnsafe(view, row_idx)));
416+
break;
415417
case ArrowType::NANOARROW_TYPE_STRING: {
416-
auto value = ArrowArrayViewGetStringUnsafe(view, row_idx);
417-
manifest_entries[row_idx].data_file->partition.AddValue(
418-
Literal::String(std::string(value.data, value.size_bytes)));
418+
auto str_value = ArrowArrayViewGetStringUnsafe(view, row_idx);
419+
partition.AddValue(
420+
Literal::String(std::string(str_value.data, str_value.size_bytes)));
419421
} break;
420422
case ArrowType::NANOARROW_TYPE_BINARY: {
421-
auto buffer = ArrowArrayViewGetBytesUnsafe(view, row_idx);
422-
manifest_entries[row_idx].data_file->partition.AddValue(
423-
Literal::Binary(std::vector<uint8_t>(buffer.data.as_char,
424-
buffer.data.as_char + buffer.size_bytes)));
423+
auto buf_value = ArrowArrayViewGetBytesUnsafe(view, row_idx);
424+
partition.AddValue(Literal::Binary(std::vector<uint8_t>(
425+
buf_value.data.as_char, buf_value.data.as_char + buf_value.size_bytes)));
425426
} break;
426427
default:
427428
return InvalidManifest("Unsupported type {} for partition values",
@@ -473,14 +474,15 @@ Status ParseDataFile(const std::shared_ptr<StructType>& data_file_schema,
473474
case DataFile::kPartitionFieldId: {
474475
ICEBERG_RETURN_UNEXPECTED(
475476
AssertViewType(field_view, ArrowType::NANOARROW_TYPE_STRUCT, field_name));
477+
const auto& partition_type =
478+
internal::checked_cast<const StructType&>(*field->get().type());
476479
for (int64_t part_idx = 0; part_idx < field_view->n_children; part_idx++) {
477480
auto part_view = field_view->children[part_idx];
481+
auto part_field_type = internal::checked_pointer_cast<PrimitiveType>(
482+
partition_type.fields()[part_idx].type());
478483
for (int64_t row_idx = 0; row_idx < part_view->length; row_idx++) {
479-
if (ArrowArrayViewIsNull(part_view, row_idx)) {
480-
break;
481-
}
482-
ICEBERG_RETURN_UNEXPECTED(
483-
ParsePartitionValues(part_view, row_idx, manifest_entries));
484+
ICEBERG_RETURN_UNEXPECTED(ParsePartitionValues(
485+
part_view, row_idx, part_field_type, manifest_entries));
484486
}
485487
}
486488
} break;

src/iceberg/test/manifest_reader_test.cc

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,81 @@ TEST_P(TestManifestReader, TestManifestReaderWithPartitionMetadata) {
288288
EXPECT_EQ(read_entry.data_file->partition.values()[0], Literal::Int(0));
289289
}
290290

291+
TEST_P(TestManifestReader, NullPartitionValuePreservedPositionallyAcrossRows) {
292+
auto version = GetParam();
293+
294+
// Two partition fields, with several entries. The first entry has a null in
295+
// partition field 0. The null must be preserved as a typed null in place so
296+
// that (a) the tuple keeps one value per partition field (arity matches the
297+
// spec), (b) the non-null value for field 1 stays in position 1 instead of
298+
// shifting into position 0, and (c) parsing of the remaining rows is not
299+
// aborted. This mirrors Iceberg Java's PartitionData, a fixed-size positional
300+
// array that stores null in place.
301+
auto multi_schema = std::make_shared<Schema>(std::vector<SchemaField>{
302+
SchemaField::MakeRequired(/*field_id=*/3, "id", int32()),
303+
SchemaField::MakeRequired(/*field_id=*/4, "data", string())});
304+
305+
std::shared_ptr<PartitionSpec> multi_spec;
306+
ICEBERG_UNWRAP_OR_FAIL(
307+
multi_spec,
308+
PartitionSpec::Make(
309+
/*spec_id=*/0, {PartitionField(/*source_id=*/3, /*field_id=*/1000, "id_part",
310+
Transform::Identity()),
311+
PartitionField(/*source_id=*/4, /*field_id=*/1001,
312+
"data_bucket", Transform::Bucket(16))}));
313+
314+
const std::string manifest_path = MakeManifestPath();
315+
auto writer_result = ManifestWriter::MakeWriter(
316+
version, /*snapshot_id=*/1000L, manifest_path, file_io_, multi_spec, multi_schema,
317+
ManifestContent::kData, /*first_row_id=*/0L);
318+
ASSERT_THAT(writer_result, IsOk());
319+
auto writer = std::move(writer_result.value());
320+
321+
// First entry: partition field 0 (identity int) is null, field 1 (bucket) is 9.
322+
auto file_null =
323+
MakeDataFile("/path/to/data-null.parquet",
324+
PartitionValues({Literal::Null(int32()), Literal::Int(9)}));
325+
auto file_b = MakeDataFile("/path/to/data-b.parquet",
326+
PartitionValues({Literal::Int(1), Literal::Int(5)}));
327+
auto file_c = MakeDataFile("/path/to/data-c.parquet",
328+
PartitionValues({Literal::Int(2), Literal::Int(7)}));
329+
ASSERT_THAT(
330+
writer->WriteEntry(MakeEntry(ManifestStatus::kAdded, 1000L, std::move(file_null))),
331+
IsOk());
332+
ASSERT_THAT(
333+
writer->WriteEntry(MakeEntry(ManifestStatus::kAdded, 1000L, std::move(file_b))),
334+
IsOk());
335+
ASSERT_THAT(
336+
writer->WriteEntry(MakeEntry(ManifestStatus::kAdded, 1000L, std::move(file_c))),
337+
IsOk());
338+
ASSERT_THAT(writer->Close(), IsOk());
339+
ICEBERG_UNWRAP_OR_FAIL(auto manifest, writer->ToManifestFile());
340+
341+
ICEBERG_UNWRAP_OR_FAIL(
342+
auto reader, ManifestReader::Make(manifest, file_io_, multi_schema, multi_spec));
343+
ICEBERG_UNWRAP_OR_FAIL(auto read_entries, reader->Entries());
344+
345+
ASSERT_EQ(read_entries.size(), 3U);
346+
347+
// The null-partition row keeps both fields: a null in position 0 and the
348+
// non-null value in position 1 (no shifting).
349+
EXPECT_EQ(read_entries[0].data_file->file_path, "/path/to/data-null.parquet");
350+
ASSERT_EQ(read_entries[0].data_file->partition.num_fields(), 2);
351+
EXPECT_TRUE(read_entries[0].data_file->partition.values()[0].IsNull());
352+
EXPECT_EQ(read_entries[0].data_file->partition.values()[1], Literal::Int(9));
353+
354+
// Rows after the null one must still have their partition values parsed.
355+
EXPECT_EQ(read_entries[1].data_file->file_path, "/path/to/data-b.parquet");
356+
ASSERT_EQ(read_entries[1].data_file->partition.num_fields(), 2);
357+
EXPECT_EQ(read_entries[1].data_file->partition.values()[0], Literal::Int(1));
358+
EXPECT_EQ(read_entries[1].data_file->partition.values()[1], Literal::Int(5));
359+
360+
EXPECT_EQ(read_entries[2].data_file->file_path, "/path/to/data-c.parquet");
361+
ASSERT_EQ(read_entries[2].data_file->partition.num_fields(), 2);
362+
EXPECT_EQ(read_entries[2].data_file->partition.values()[0], Literal::Int(2));
363+
EXPECT_EQ(read_entries[2].data_file->partition.values()[1], Literal::Int(7));
364+
}
365+
291366
TEST_P(TestManifestReader, ReadsEntriesWhenPartitionSourceFieldIsMissing) {
292367
auto version = GetParam();
293368
auto file = MakeDataFile("/path/to/historical-data.parquet",

0 commit comments

Comments
 (0)