|
26 | 26 | #include <gtest/gtest.h> |
27 | 27 |
|
28 | 28 | #include "iceberg/arrow/arrow_io_internal.h" |
| 29 | +#include "iceberg/arrow_c_data_guard_internal.h" |
29 | 30 | #include "iceberg/avro/avro_register.h" |
30 | 31 | #include "iceberg/data/equality_delete_writer.h" |
31 | 32 | #include "iceberg/data/position_delete_writer.h" |
|
45 | 46 |
|
46 | 47 | namespace iceberg { |
47 | 48 |
|
| 49 | +using ::testing::ElementsAre; |
48 | 50 | using ::testing::HasSubstr; |
| 51 | +using ::testing::UnorderedElementsAre; |
49 | 52 |
|
50 | 53 | class DataWriterTest : public ::testing::Test { |
51 | 54 | protected: |
@@ -344,18 +347,12 @@ class PositionDeleteWriterTest : public DataWriterTest { |
344 | 347 | }; |
345 | 348 | } |
346 | 349 |
|
347 | | - std::shared_ptr<::arrow::Array> CreatePositionDeleteData() { |
| 350 | + std::shared_ptr<::arrow::Array> CreatePositionDeleteData( |
| 351 | + std::string_view json = |
| 352 | + R"([["data_file_1.parquet", 0], ["data_file_1.parquet", 5], ["data_file_1.parquet", 10]])") { |
348 | 353 | auto delete_schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
349 | 354 | MetadataColumns::kDeleteFilePath, MetadataColumns::kDeleteFilePos}); |
350 | | - |
351 | | - ArrowSchema arrow_c_schema; |
352 | | - ICEBERG_THROW_NOT_OK(ToArrowSchema(*delete_schema, &arrow_c_schema)); |
353 | | - auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); |
354 | | - |
355 | | - return ::arrow::json::ArrayFromJSONString( |
356 | | - ::arrow::struct_(arrow_type->fields()), |
357 | | - R"([["data_file_1.parquet", 0], ["data_file_1.parquet", 5], ["data_file_1.parquet", 10]])") |
358 | | - .ValueOrDie(); |
| 355 | + return CreateArray(*delete_schema, json); |
359 | 356 | } |
360 | 357 | }; |
361 | 358 |
|
@@ -458,6 +455,200 @@ TEST_F(PositionDeleteWriterTest, WriteBatchData) { |
458 | 455 | const auto& data_file = metadata_result.value().data_files[0]; |
459 | 456 | EXPECT_EQ(data_file->content, DataFile::Content::kPositionDeletes); |
460 | 457 | EXPECT_GT(data_file->file_size_in_bytes, 0); |
| 458 | + ASSERT_TRUE(data_file->referenced_data_file.has_value()); |
| 459 | + EXPECT_EQ(data_file->referenced_data_file.value(), "data_file_1.parquet"); |
| 460 | + // Bounds for delete metadata columns are kept when referencing a single file. |
| 461 | + EXPECT_TRUE(data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); |
| 462 | + EXPECT_TRUE(data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); |
| 463 | + EXPECT_TRUE(data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); |
| 464 | + EXPECT_TRUE(data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); |
| 465 | +} |
| 466 | + |
| 467 | +TEST_F(PositionDeleteWriterTest, WriteBatchRejectsSlicedData) { |
| 468 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 469 | + ASSERT_THAT(writer_result, IsOk()); |
| 470 | + auto writer = std::move(writer_result.value()); |
| 471 | + |
| 472 | + auto test_data = CreatePositionDeleteData( |
| 473 | + R"([["data_file_1.parquet", 0], ["data_file_1.parquet", 5]])"); |
| 474 | + auto sliced = test_data->Slice(1, 1); |
| 475 | + ArrowArray arrow_array; |
| 476 | + ASSERT_TRUE(::arrow::ExportArray(*sliced, &arrow_array).ok()); |
| 477 | + |
| 478 | + auto result = writer->Write(&arrow_array); |
| 479 | + EXPECT_EQ(arrow_array.release, nullptr); |
| 480 | + internal::ArrowArrayGuard array_guard(&arrow_array); |
| 481 | + ASSERT_THAT(result, IsError(ErrorKind::kInvalidArgument)); |
| 482 | + EXPECT_THAT( |
| 483 | + result, |
| 484 | + HasErrorMessage("Position delete data with a non-zero offset is not supported")); |
| 485 | +} |
| 486 | + |
| 487 | +TEST_F(PositionDeleteWriterTest, FailedBatchWriteDoesNotTrackReferencedFiles) { |
| 488 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 489 | + ASSERT_THAT(writer_result, IsOk()); |
| 490 | + auto writer = std::move(writer_result.value()); |
| 491 | + |
| 492 | + auto good_data = CreatePositionDeleteData(R"([["data_file_1.parquet", 0]])"); |
| 493 | + ArrowArray good_array; |
| 494 | + ASSERT_TRUE(::arrow::ExportArray(*good_data, &good_array).ok()); |
| 495 | + ASSERT_THAT(writer->Write(&good_array), IsOk()); |
| 496 | + |
| 497 | + // The batch references a valid path before the null path rejects it, and none of |
| 498 | + // its paths may end up in the metadata. |
| 499 | + auto bad_data = |
| 500 | + CreatePositionDeleteData(R"([["data_file_bad.parquet", 1], [null, 2]])"); |
| 501 | + ArrowArray bad_array; |
| 502 | + ASSERT_TRUE(::arrow::ExportArray(*bad_data, &bad_array).ok()); |
| 503 | + internal::ArrowArrayGuard bad_array_guard(&bad_array); |
| 504 | + ASSERT_THAT(writer->Write(&bad_array), IsError(ErrorKind::kInvalidArrowData)); |
| 505 | + |
| 506 | + ASSERT_THAT(writer->Close(), IsOk()); |
| 507 | + |
| 508 | + auto metadata_result = writer->Metadata(); |
| 509 | + ASSERT_THAT(metadata_result, IsOk()); |
| 510 | + |
| 511 | + const auto& write_result = metadata_result.value(); |
| 512 | + const auto& data_file = write_result.data_files[0]; |
| 513 | + ASSERT_TRUE(data_file->referenced_data_file.has_value()); |
| 514 | + EXPECT_EQ(data_file->referenced_data_file.value(), "data_file_1.parquet"); |
| 515 | + EXPECT_THAT(write_result.referenced_data_files, ElementsAre("data_file_1.parquet")); |
| 516 | +} |
| 517 | + |
| 518 | +TEST_F(PositionDeleteWriterTest, WriteBatchDataForMultipleFiles) { |
| 519 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 520 | + ASSERT_THAT(writer_result, IsOk()); |
| 521 | + auto writer = std::move(writer_result.value()); |
| 522 | + |
| 523 | + // Disjoint paths across two successful batches must be unioned, not replaced. |
| 524 | + auto first_data = CreatePositionDeleteData(R"([["data_file_1.parquet", 0]])"); |
| 525 | + ArrowArray first_array; |
| 526 | + ASSERT_TRUE(::arrow::ExportArray(*first_data, &first_array).ok()); |
| 527 | + ASSERT_THAT(writer->Write(&first_array), IsOk()); |
| 528 | + |
| 529 | + auto second_data = CreatePositionDeleteData(R"([["data_file_2.parquet", 5]])"); |
| 530 | + ArrowArray second_array; |
| 531 | + ASSERT_TRUE(::arrow::ExportArray(*second_data, &second_array).ok()); |
| 532 | + ASSERT_THAT(writer->Write(&second_array), IsOk()); |
| 533 | + |
| 534 | + ASSERT_THAT(writer->Close(), IsOk()); |
| 535 | + |
| 536 | + auto metadata_result = writer->Metadata(); |
| 537 | + ASSERT_THAT(metadata_result, IsOk()); |
| 538 | + |
| 539 | + const auto& write_result = metadata_result.value(); |
| 540 | + const auto& data_file = write_result.data_files[0]; |
| 541 | + EXPECT_FALSE(data_file->referenced_data_file.has_value()); |
| 542 | + EXPECT_THAT(write_result.referenced_data_files, |
| 543 | + UnorderedElementsAre("data_file_1.parquet", "data_file_2.parquet")); |
| 544 | + EXPECT_FALSE( |
| 545 | + data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); |
| 546 | + EXPECT_FALSE(data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); |
| 547 | + EXPECT_FALSE( |
| 548 | + data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); |
| 549 | + EXPECT_FALSE(data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); |
| 550 | +} |
| 551 | + |
| 552 | +TEST_F(PositionDeleteWriterTest, WriteBatchThenDeleteTracksAllReferencedFiles) { |
| 553 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 554 | + ASSERT_THAT(writer_result, IsOk()); |
| 555 | + auto writer = std::move(writer_result.value()); |
| 556 | + |
| 557 | + auto test_data = CreatePositionDeleteData(R"([["data_file_1.parquet", 0]])"); |
| 558 | + ArrowArray arrow_array; |
| 559 | + ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); |
| 560 | + ASSERT_THAT(writer->Write(&arrow_array), IsOk()); |
| 561 | + ASSERT_THAT(writer->WriteDelete("data_file_2.parquet", 5), IsOk()); |
| 562 | + ASSERT_THAT(writer->Close(), IsOk()); |
| 563 | + |
| 564 | + auto metadata_result = writer->Metadata(); |
| 565 | + ASSERT_THAT(metadata_result, IsOk()); |
| 566 | + const auto& write_result = metadata_result.value(); |
| 567 | + EXPECT_FALSE(write_result.data_files[0]->referenced_data_file.has_value()); |
| 568 | + EXPECT_THAT(write_result.referenced_data_files, |
| 569 | + UnorderedElementsAre("data_file_1.parquet", "data_file_2.parquet")); |
| 570 | +} |
| 571 | + |
| 572 | +TEST_F(PositionDeleteWriterTest, WriteBatchRejectsInvalidInput) { |
| 573 | + // A null array. |
| 574 | + { |
| 575 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 576 | + ASSERT_THAT(writer_result, IsOk()); |
| 577 | + auto writer = std::move(writer_result.value()); |
| 578 | + |
| 579 | + auto result = writer->Write(nullptr); |
| 580 | + ASSERT_THAT(result, IsError(ErrorKind::kInvalidArgument)); |
| 581 | + EXPECT_THAT(result, HasErrorMessage("Position delete data must not be null")); |
| 582 | + } |
| 583 | + |
| 584 | + // A null file path. |
| 585 | + { |
| 586 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 587 | + ASSERT_THAT(writer_result, IsOk()); |
| 588 | + auto writer = std::move(writer_result.value()); |
| 589 | + |
| 590 | + auto test_data = CreatePositionDeleteData(R"([[null, 0]])"); |
| 591 | + ArrowArray arrow_array; |
| 592 | + ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); |
| 593 | + |
| 594 | + auto result = writer->Write(&arrow_array); |
| 595 | + EXPECT_EQ(arrow_array.release, nullptr); |
| 596 | + internal::ArrowArrayGuard array_guard(&arrow_array); |
| 597 | + ASSERT_THAT(result, IsError(ErrorKind::kInvalidArrowData)); |
| 598 | + EXPECT_THAT(result, HasErrorMessage( |
| 599 | + "Position delete file paths must not contain null values")); |
| 600 | + } |
| 601 | + |
| 602 | + // A null position. |
| 603 | + { |
| 604 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 605 | + ASSERT_THAT(writer_result, IsOk()); |
| 606 | + auto writer = std::move(writer_result.value()); |
| 607 | + |
| 608 | + auto test_data = CreatePositionDeleteData(R"([["data_file_1.parquet", null]])"); |
| 609 | + ArrowArray arrow_array; |
| 610 | + ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); |
| 611 | + |
| 612 | + auto result = writer->Write(&arrow_array); |
| 613 | + EXPECT_EQ(arrow_array.release, nullptr); |
| 614 | + internal::ArrowArrayGuard array_guard(&arrow_array); |
| 615 | + ASSERT_THAT(result, IsError(ErrorKind::kInvalidArrowData)); |
| 616 | + EXPECT_THAT(result, HasErrorMessage( |
| 617 | + "Position delete positions must not contain null values")); |
| 618 | + } |
| 619 | + |
| 620 | + // An empty file path. |
| 621 | + { |
| 622 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 623 | + ASSERT_THAT(writer_result, IsOk()); |
| 624 | + auto writer = std::move(writer_result.value()); |
| 625 | + |
| 626 | + auto test_data = CreatePositionDeleteData(R"([["", 0]])"); |
| 627 | + ArrowArray arrow_array; |
| 628 | + ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); |
| 629 | + |
| 630 | + auto result = writer->Write(&arrow_array); |
| 631 | + EXPECT_EQ(arrow_array.release, nullptr); |
| 632 | + internal::ArrowArrayGuard array_guard(&arrow_array); |
| 633 | + ASSERT_THAT(result, IsError(ErrorKind::kInvalidArrowData)); |
| 634 | + EXPECT_THAT(result, HasErrorMessage("Position delete file paths must not be empty")); |
| 635 | + } |
| 636 | +} |
| 637 | + |
| 638 | +TEST_F(PositionDeleteWriterTest, WriteEmptyBatchDoesNotAddReferencedFiles) { |
| 639 | + auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); |
| 640 | + ASSERT_THAT(writer_result, IsOk()); |
| 641 | + auto writer = std::move(writer_result.value()); |
| 642 | + |
| 643 | + auto test_data = CreatePositionDeleteData("[]"); |
| 644 | + ArrowArray arrow_array; |
| 645 | + ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); |
| 646 | + ASSERT_THAT(writer->Write(&arrow_array), IsOk()); |
| 647 | + ASSERT_THAT(writer->Close(), IsOk()); |
| 648 | + |
| 649 | + auto metadata_result = writer->Metadata(); |
| 650 | + ASSERT_THAT(metadata_result, IsOk()); |
| 651 | + EXPECT_FALSE(metadata_result.value().data_files[0]->referenced_data_file.has_value()); |
461 | 652 | } |
462 | 653 |
|
463 | 654 | TEST_F(PositionDeleteWriterTest, AutoFlushOnThreshold) { |
|
0 commit comments