| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| #include "iceberg/data/data_writer.h" | |
| #include <arrow/array.h> | |
| #include <arrow/c/bridge.h> | |
| #include <arrow/json/from_string.h> | |
| #include <gmock/gmock.h> | |
| #include <gtest/gtest.h> | |
| #include "iceberg/arrow/arrow_io_internal.h" | |
| #include "iceberg/arrow_c_data_guard_internal.h" | |
| #include "iceberg/avro/avro_register.h" | |
| #include "iceberg/data/equality_delete_writer.h" | |
| #include "iceberg/data/position_delete_writer.h" | |
| #include "iceberg/file_format.h" | |
| #include "iceberg/file_reader.h" | |
| #include "iceberg/manifest/manifest_entry.h" | |
| #include "iceberg/metadata_columns.h" | |
| #include "iceberg/parquet/parquet_register.h" | |
| #include "iceberg/partition_spec.h" | |
| #include "iceberg/row/partition_values.h" | |
| #include "iceberg/schema.h" | |
| #include "iceberg/schema_field.h" | |
| #include "iceberg/schema_internal.h" | |
| #include "iceberg/test/matchers.h" | |
| #include "iceberg/type.h" | |
| #include "iceberg/util/macros.h" | |
| namespace iceberg { | |
| using ::testing::ElementsAre; | |
| using ::testing::HasSubstr; | |
| using ::testing::UnorderedElementsAre; | |
| class DataWriterTest : public ::testing::Test { | |
| protected: | |
| static void SetUpTestSuite() { | |
| parquet::RegisterAll(); | |
| avro::RegisterAll(); | |
| } | |
| void SetUp() override { | |
| file_io_ = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); | |
| schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "name", string())}); | |
| partition_spec_ = PartitionSpec::Unpartitioned(); | |
| } | |
| DataWriterOptions MakeDefaultOptions( | |
| std::optional<int32_t> sort_order_id = std::nullopt, | |
| PartitionValues partition = PartitionValues{}) { | |
| return DataWriterOptions{ | |
| .path = "test_data.parquet", | |
| .schema = schema_, | |
| .spec = partition_spec_, | |
| .partition = std::move(partition), | |
| .format = FileFormatType::kParquet, | |
| .io = file_io_, | |
| .sort_order_id = sort_order_id, | |
| .properties = {{"write.parquet.compression-codec", "uncompressed"}}, | |
| }; | |
| } | |
| std::shared_ptr<::arrow::Array> CreateArray(const Schema& schema, | |
| std::string_view json) { | |
| ArrowSchema arrow_c_schema; | |
| ICEBERG_THROW_NOT_OK(ToArrowSchema(schema, &arrow_c_schema)); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| return ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| std::string(json)) | |
| .ValueOrDie(); | |
| } | |
| std::shared_ptr<::arrow::Array> CreateTestData() { | |
| return CreateArray(*schema_, R"([[1, "Alice"], [2, "Bob"], [3, "Charlie"]])"); | |
| } | |
| std::shared_ptr<Schema> RowLineageSchema() { | |
| return std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), MetadataColumns::kRowId, | |
| MetadataColumns::kLastUpdatedSequenceNumber}); | |
| } | |
| std::unordered_map<std::string, std::string> FormatProperties(FileFormatType format) { | |
| if (format == FileFormatType::kParquet) { | |
| return {{"write.parquet.compression-codec", "uncompressed"}}; | |
| } | |
| return {}; | |
| } | |
| void WriteTestDataToWriter(DataWriter* writer) { | |
| auto test_data = CreateTestData(); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); | |
| } | |
| void VerifyNextBatch(Reader& reader, const Schema& schema, | |
| std::string_view expected_json) { | |
| auto data = reader.Next(); | |
| ASSERT_THAT(data, IsOk()) << "Reader.Next() failed: " << data.error().message; | |
| ASSERT_TRUE(data.value().has_value()) << "Reader.Next() returned no data"; | |
| auto expected_array = CreateArray(schema, expected_json); | |
| auto actual_array = | |
| ::arrow::ImportArray(&data.value().value(), expected_array->type()).ValueOrDie(); | |
| ASSERT_TRUE(actual_array->Equals(expected_array)) | |
| << "Expected: " << expected_array->ToString() | |
| << "\nActual: " << actual_array->ToString(); | |
| } | |
| std::shared_ptr<FileIO> file_io_; | |
| std::shared_ptr<Schema> schema_; | |
| std::shared_ptr<PartitionSpec> partition_spec_; | |
| }; | |
| class DataWriterFormatTest | |
| : public DataWriterTest, | |
| public ::testing::WithParamInterface<std::pair<FileFormatType, std::string>> {}; | |
| TEST_P(DataWriterFormatTest, CreateWithFormat) { | |
| auto [format, path] = GetParam(); | |
| DataWriterOptions options{ | |
| .path = path, | |
| .schema = schema_, | |
| .spec = partition_spec_, | |
| .partition = PartitionValues{}, | |
| .format = format, | |
| .io = file_io_, | |
| .properties = FormatProperties(format), | |
| }; | |
| auto writer_result = DataWriter::Make(options); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| ASSERT_NE(writer, nullptr); | |
| } | |
| TEST_P(DataWriterFormatTest, WriteRowLineage) { | |
| auto [format, path] = GetParam(); | |
| auto schema = RowLineageSchema(); | |
| DataWriterOptions options{ | |
| .path = path, | |
| .schema = schema, | |
| .spec = partition_spec_, | |
| .partition = PartitionValues{}, | |
| .format = format, | |
| .io = file_io_, | |
| .properties = FormatProperties(format), | |
| }; | |
| ICEBERG_UNWRAP_OR_FAIL(auto writer, DataWriter::Make(options)); | |
| auto array = CreateArray(*schema, R"([[1, null, null], | |
| [2, 777, 8], | |
| [3, null, null]])"); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok()); | |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, ReaderFactoryRegistry::Open(format, {.path = path, | |
| .io = file_io_, | |
| .projection = schema, | |
| .first_row_id = 100L, | |
| .data_sequence_number = 7L})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, *schema, R"([[1, 100, 7], | |
| [2, 777, 8], | |
| [3, 102, 7]])")); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| FormatTypes, DataWriterFormatTest, | |
| ::testing::Values(std::make_pair(FileFormatType::kParquet, "test_data.parquet"), | |
| std::make_pair(FileFormatType::kAvro, "test_data.avro"))); | |
| TEST_F(DataWriterTest, WriteAndClose) { | |
| auto writer_result = DataWriter::Make(MakeDefaultOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| // Write data | |
| WriteTestDataToWriter(writer.get()); | |
| // Length should be greater than 0 after write | |
| auto length_result = writer->Length(); | |
| ASSERT_THAT(length_result, IsOk()); | |
| EXPECT_GT(length_result.value(), 0); | |
| // Close | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| } | |
| TEST_F(DataWriterTest, MetadataAfterClose) { | |
| auto writer_result = DataWriter::Make(MakeDefaultOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| // Get metadata | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& write_result = metadata_result.value(); | |
| ASSERT_EQ(write_result.data_files.size(), 1); | |
| const auto& data_file = write_result.data_files[0]; | |
| EXPECT_EQ(data_file->content, DataFile::Content::kData); | |
| EXPECT_EQ(data_file->file_path, "test_data.parquet"); | |
| EXPECT_EQ(data_file->file_format, FileFormatType::kParquet); | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| // Metrics availability depends on the underlying writer implementation | |
| EXPECT_GE(data_file->column_sizes.size(), 0); | |
| EXPECT_GE(data_file->value_counts.size(), 0); | |
| EXPECT_GE(data_file->null_value_counts.size(), 0); | |
| } | |
| TEST_F(DataWriterTest, MetadataBeforeCloseReturnsError) { | |
| auto writer_result = DataWriter::Make(MakeDefaultOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| // Try to get metadata before closing | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsError(ErrorKind::kValidationFailed)); | |
| EXPECT_THAT(metadata_result, | |
| HasErrorMessage("Cannot get metadata before closing the writer")); | |
| } | |
| TEST_F(DataWriterTest, CloseIsIdempotent) { | |
| auto writer_result = DataWriter::Make(MakeDefaultOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| } | |
| TEST_F(DataWriterTest, SortOrderIdInMetadata) { | |
| // Test with explicit sort order id | |
| { | |
| const int32_t sort_order_id = 42; | |
| auto writer_result = DataWriter::Make(MakeDefaultOptions(sort_order_id)); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| ASSERT_TRUE(data_file->sort_order_id.has_value()); | |
| EXPECT_EQ(data_file->sort_order_id.value(), sort_order_id); | |
| } | |
| // Test without sort order id (should be nullopt) | |
| { | |
| auto writer_result = DataWriter::Make(MakeDefaultOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| EXPECT_FALSE(data_file->sort_order_id.has_value()); | |
| } | |
| } | |
| TEST_F(DataWriterTest, PartitionValuesPreserved) { | |
| PartitionValues partition_values({Literal::Int(42), Literal::String("test")}); | |
| auto writer_result = | |
| DataWriter::Make(MakeDefaultOptions(std::nullopt, partition_values)); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| EXPECT_EQ(data_file->partition.num_fields(), partition_values.num_fields()); | |
| EXPECT_EQ(data_file->partition.num_fields(), 2); | |
| } | |
| TEST_F(DataWriterTest, WriteMultipleBatches) { | |
| auto writer_result = DataWriter::Make(MakeDefaultOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToWriter(writer.get()); | |
| WriteTestDataToWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| } | |
| class PositionDeleteWriterTest : public DataWriterTest { | |
| protected: | |
| PositionDeleteWriterOptions MakeDeleteOptions(int64_t flush_threshold = 1000) { | |
| return PositionDeleteWriterOptions{ | |
| .path = "test_deletes.parquet", | |
| .schema = schema_, | |
| .spec = partition_spec_, | |
| .partition = PartitionValues{}, | |
| .format = FileFormatType::kParquet, | |
| .io = file_io_, | |
| .flush_threshold = flush_threshold, | |
| .properties = {{"write.parquet.compression-codec", "uncompressed"}}, | |
| }; | |
| } | |
| std::shared_ptr<::arrow::Array> CreatePositionDeleteData( | |
| std::string_view json = | |
| R"([["data_file_1.parquet", 0], ["data_file_1.parquet", 5], ["data_file_1.parquet", 10]])") { | |
| auto delete_schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| MetadataColumns::kDeleteFilePath, MetadataColumns::kDeleteFilePos}); | |
| return CreateArray(*delete_schema, json); | |
| } | |
| }; | |
| TEST_F(PositionDeleteWriterTest, WriteDeleteAndClose) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", 0), IsOk()); | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", 5), IsOk()); | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", 10), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto length_result = writer->Length(); | |
| ASSERT_THAT(length_result, IsOk()); | |
| EXPECT_GT(length_result.value(), 0); | |
| } | |
| TEST_F(PositionDeleteWriterTest, MetadataAfterClose) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", 0), IsOk()); | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", 5), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& write_result = metadata_result.value(); | |
| ASSERT_EQ(write_result.data_files.size(), 1); | |
| const auto& data_file = write_result.data_files[0]; | |
| EXPECT_EQ(data_file->content, DataFile::Content::kPositionDeletes); | |
| EXPECT_EQ(data_file->file_path, "test_deletes.parquet"); | |
| EXPECT_EQ(data_file->file_format, FileFormatType::kParquet); | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| EXPECT_FALSE(data_file->sort_order_id.has_value()); | |
| } | |
| TEST_F(PositionDeleteWriterTest, MetadataBeforeCloseReturnsError) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsError(ErrorKind::kValidationFailed)); | |
| EXPECT_THAT(metadata_result, | |
| HasErrorMessage("Cannot get metadata before closing the writer")); | |
| } | |
| TEST_F(PositionDeleteWriterTest, CloseIsIdempotent) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", 0), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| } | |
| TEST_F(PositionDeleteWriterTest, WriteMultipleDeletes) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| for (int64_t i = 0; i < 100; ++i) { | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", i), IsOk()); | |
| } | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| EXPECT_EQ(data_file->content, DataFile::Content::kPositionDeletes); | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| } | |
| TEST_F(PositionDeleteWriterTest, WriteBatchData) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto test_data = CreatePositionDeleteData(); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| EXPECT_EQ(data_file->content, DataFile::Content::kPositionDeletes); | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| ASSERT_TRUE(data_file->referenced_data_file.has_value()); | |
| EXPECT_EQ(data_file->referenced_data_file.value(), "data_file_1.parquet"); | |
| // Bounds for delete metadata columns are kept when referencing a single file. | |
| EXPECT_TRUE(data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); | |
| EXPECT_TRUE(data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); | |
| EXPECT_TRUE(data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); | |
| EXPECT_TRUE(data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); | |
| } | |
| TEST_F(PositionDeleteWriterTest, WriteBatchRejectsSlicedData) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto test_data = CreatePositionDeleteData( | |
| R"([["data_file_1.parquet", 0], ["data_file_1.parquet", 5]])"); | |
| auto sliced = test_data->Slice(1, 1); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*sliced, &arrow_array).ok()); | |
| auto result = writer->Write(&arrow_array); | |
| EXPECT_EQ(arrow_array.release, nullptr); | |
| internal::ArrowArrayGuard array_guard(&arrow_array); | |
| ASSERT_THAT(result, IsError(ErrorKind::kInvalidArgument)); | |
| EXPECT_THAT( | |
| result, | |
| HasErrorMessage("Position delete data with a non-zero offset is not supported")); | |
| } | |
| TEST_F(PositionDeleteWriterTest, FailedBatchWriteDoesNotTrackReferencedFiles) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto good_data = CreatePositionDeleteData(R"([["data_file_1.parquet", 0]])"); | |
| ArrowArray good_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*good_data, &good_array).ok()); | |
| ASSERT_THAT(writer->Write(&good_array), IsOk()); | |
| // The batch references a valid path before the null path rejects it, and none of | |
| // its paths may end up in the metadata. | |
| auto bad_data = | |
| CreatePositionDeleteData(R"([["data_file_bad.parquet", 1], [null, 2]])"); | |
| ArrowArray bad_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*bad_data, &bad_array).ok()); | |
| internal::ArrowArrayGuard bad_array_guard(&bad_array); | |
| ASSERT_THAT(writer->Write(&bad_array), IsError(ErrorKind::kInvalidArrowData)); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& write_result = metadata_result.value(); | |
| const auto& data_file = write_result.data_files[0]; | |
| ASSERT_TRUE(data_file->referenced_data_file.has_value()); | |
| EXPECT_EQ(data_file->referenced_data_file.value(), "data_file_1.parquet"); | |
| EXPECT_THAT(write_result.referenced_data_files, ElementsAre("data_file_1.parquet")); | |
| } | |
| TEST_F(PositionDeleteWriterTest, WriteBatchDataForMultipleFiles) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| // Disjoint paths across two successful batches must be unioned, not replaced. | |
| auto first_data = CreatePositionDeleteData(R"([["data_file_1.parquet", 0]])"); | |
| ArrowArray first_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*first_data, &first_array).ok()); | |
| ASSERT_THAT(writer->Write(&first_array), IsOk()); | |
| auto second_data = CreatePositionDeleteData(R"([["data_file_2.parquet", 5]])"); | |
| ArrowArray second_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*second_data, &second_array).ok()); | |
| ASSERT_THAT(writer->Write(&second_array), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& write_result = metadata_result.value(); | |
| const auto& data_file = write_result.data_files[0]; | |
| EXPECT_FALSE(data_file->referenced_data_file.has_value()); | |
| EXPECT_THAT(write_result.referenced_data_files, | |
| UnorderedElementsAre("data_file_1.parquet", "data_file_2.parquet")); | |
| EXPECT_FALSE( | |
| data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); | |
| EXPECT_FALSE(data_file->lower_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); | |
| EXPECT_FALSE( | |
| data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePathColumnId)); | |
| EXPECT_FALSE(data_file->upper_bounds.contains(MetadataColumns::kDeleteFilePosColumnId)); | |
| } | |
| TEST_F(PositionDeleteWriterTest, WriteBatchThenDeleteTracksAllReferencedFiles) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto test_data = CreatePositionDeleteData(R"([["data_file_1.parquet", 0]])"); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); | |
| ASSERT_THAT(writer->WriteDelete("data_file_2.parquet", 5), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& write_result = metadata_result.value(); | |
| EXPECT_FALSE(write_result.data_files[0]->referenced_data_file.has_value()); | |
| EXPECT_THAT(write_result.referenced_data_files, | |
| UnorderedElementsAre("data_file_1.parquet", "data_file_2.parquet")); | |
| } | |
| TEST_F(PositionDeleteWriterTest, WriteBatchRejectsInvalidInput) { | |
| // A null array. | |
| { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto result = writer->Write(nullptr); | |
| ASSERT_THAT(result, IsError(ErrorKind::kInvalidArgument)); | |
| EXPECT_THAT(result, HasErrorMessage("Position delete data must not be null")); | |
| } | |
| // A null file path. | |
| { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto test_data = CreatePositionDeleteData(R"([[null, 0]])"); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| auto result = writer->Write(&arrow_array); | |
| EXPECT_EQ(arrow_array.release, nullptr); | |
| internal::ArrowArrayGuard array_guard(&arrow_array); | |
| ASSERT_THAT(result, IsError(ErrorKind::kInvalidArrowData)); | |
| EXPECT_THAT(result, HasErrorMessage( | |
| "Position delete file paths must not contain null values")); | |
| } | |
| // A null position. | |
| { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto test_data = CreatePositionDeleteData(R"([["data_file_1.parquet", null]])"); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| auto result = writer->Write(&arrow_array); | |
| EXPECT_EQ(arrow_array.release, nullptr); | |
| internal::ArrowArrayGuard array_guard(&arrow_array); | |
| ASSERT_THAT(result, IsError(ErrorKind::kInvalidArrowData)); | |
| EXPECT_THAT(result, HasErrorMessage( | |
| "Position delete positions must not contain null values")); | |
| } | |
| // An empty file path. | |
| { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto test_data = CreatePositionDeleteData(R"([["", 0]])"); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| auto result = writer->Write(&arrow_array); | |
| EXPECT_EQ(arrow_array.release, nullptr); | |
| internal::ArrowArrayGuard array_guard(&arrow_array); | |
| ASSERT_THAT(result, IsError(ErrorKind::kInvalidArrowData)); | |
| EXPECT_THAT(result, HasErrorMessage("Position delete file paths must not be empty")); | |
| } | |
| } | |
| TEST_F(PositionDeleteWriterTest, WriteEmptyBatchDoesNotAddReferencedFiles) { | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto test_data = CreatePositionDeleteData("[]"); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| EXPECT_FALSE(metadata_result.value().data_files[0]->referenced_data_file.has_value()); | |
| } | |
| TEST_F(PositionDeleteWriterTest, AutoFlushOnThreshold) { | |
| // Use a small flush threshold to trigger automatic flush | |
| const int64_t flush_threshold = 5; | |
| auto writer_result = PositionDeleteWriter::Make(MakeDeleteOptions(flush_threshold)); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| // Write more deletes than the threshold to trigger auto-flush | |
| for (int64_t i = 0; i < 12; ++i) { | |
| ASSERT_THAT(writer->WriteDelete("data_file.parquet", i), IsOk()); | |
| } | |
| // Length should be > 0 since auto-flush should have written data | |
| auto length_result = writer->Length(); | |
| ASSERT_THAT(length_result, IsOk()); | |
| EXPECT_GT(length_result.value(), 0); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| EXPECT_EQ(data_file->content, DataFile::Content::kPositionDeletes); | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| } | |
| class EqualityDeleteWriterTest : public DataWriterTest { | |
| protected: | |
| EqualityDeleteWriterOptions MakeDeleteOptions( | |
| std::vector<int32_t> equality_field_ids = {1, 2}, | |
| std::optional<int32_t> sort_order_id = std::nullopt) { | |
| return EqualityDeleteWriterOptions{ | |
| .path = "test_eq_deletes.parquet", | |
| .schema = schema_, | |
| .spec = partition_spec_, | |
| .partition = PartitionValues{}, | |
| .format = FileFormatType::kParquet, | |
| .io = file_io_, | |
| .equality_field_ids = std::move(equality_field_ids), | |
| .sort_order_id = sort_order_id, | |
| .properties = {{"write.parquet.compression-codec", "uncompressed"}}, | |
| }; | |
| } | |
| void WriteTestDataToEqualityWriter(EqualityDeleteWriter* writer) { | |
| auto test_data = CreateTestData(); | |
| ArrowArray arrow_array; | |
| ASSERT_TRUE(::arrow::ExportArray(*test_data, &arrow_array).ok()); | |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); | |
| } | |
| }; | |
| TEST_F(EqualityDeleteWriterTest, WriteAndClose) { | |
| auto writer_result = EqualityDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToEqualityWriter(writer.get()); | |
| auto length_result = writer->Length(); | |
| ASSERT_THAT(length_result, IsOk()); | |
| EXPECT_GT(length_result.value(), 0); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| } | |
| TEST_F(EqualityDeleteWriterTest, MetadataAfterClose) { | |
| auto writer_result = EqualityDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToEqualityWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& write_result = metadata_result.value(); | |
| ASSERT_EQ(write_result.data_files.size(), 1); | |
| const auto& data_file = write_result.data_files[0]; | |
| EXPECT_EQ(data_file->content, DataFile::Content::kEqualityDeletes); | |
| EXPECT_EQ(data_file->file_path, "test_eq_deletes.parquet"); | |
| EXPECT_EQ(data_file->file_format, FileFormatType::kParquet); | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| // Partition spec id must be set | |
| ASSERT_TRUE(data_file->partition_spec_id.has_value()); | |
| EXPECT_EQ(data_file->partition_spec_id.value(), PartitionSpec::kInitialSpecId); | |
| // Equality field ids must be set | |
| ASSERT_EQ(data_file->equality_ids.size(), 2); | |
| EXPECT_EQ(data_file->equality_ids[0], 1); | |
| EXPECT_EQ(data_file->equality_ids[1], 2); | |
| } | |
| TEST_F(EqualityDeleteWriterTest, MetadataBeforeCloseReturnsError) { | |
| auto writer_result = EqualityDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsError(ErrorKind::kValidationFailed)); | |
| EXPECT_THAT(metadata_result, | |
| HasErrorMessage("Cannot get metadata before closing the writer")); | |
| } | |
| TEST_F(EqualityDeleteWriterTest, CloseIsIdempotent) { | |
| auto writer_result = EqualityDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToEqualityWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| } | |
| TEST_F(EqualityDeleteWriterTest, SortOrderIdInMetadata) { | |
| const int32_t sort_order_id = 7; | |
| auto writer_result = EqualityDeleteWriter::Make(MakeDeleteOptions({1}, sort_order_id)); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToEqualityWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| ASSERT_TRUE(data_file->sort_order_id.has_value()); | |
| EXPECT_EQ(data_file->sort_order_id.value(), sort_order_id); | |
| } | |
| TEST_F(EqualityDeleteWriterTest, EqualityFieldIdsAccessor) { | |
| std::vector<int32_t> field_ids = {1, 2, 3}; | |
| auto writer_result = EqualityDeleteWriter::Make(MakeDeleteOptions(field_ids)); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| auto ids = writer->equality_field_ids(); | |
| ASSERT_EQ(ids.size(), 3); | |
| EXPECT_EQ(ids[0], 1); | |
| EXPECT_EQ(ids[1], 2); | |
| EXPECT_EQ(ids[2], 3); | |
| } | |
| TEST_F(EqualityDeleteWriterTest, WriteMultipleBatches) { | |
| auto writer_result = EqualityDeleteWriter::Make(MakeDeleteOptions()); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| WriteTestDataToEqualityWriter(writer.get()); | |
| WriteTestDataToEqualityWriter(writer.get()); | |
| ASSERT_THAT(writer->Close(), IsOk()); | |
| auto metadata_result = writer->Metadata(); | |
| ASSERT_THAT(metadata_result, IsOk()); | |
| const auto& data_file = metadata_result.value().data_files[0]; | |
| EXPECT_EQ(data_file->content, DataFile::Content::kEqualityDeletes); | |
| EXPECT_GT(data_file->file_size_in_bytes, 0); | |
| } | |
| } // namespace iceberg |