|
26 | 26 | #include <string> |
27 | 27 | #include <vector> |
28 | 28 |
|
| 29 | +#include <arrow/array.h> |
| 30 | +#include <arrow/c/bridge.h> |
| 31 | +#include <arrow/json/from_string.h> |
29 | 32 | #include <gtest/gtest.h> |
30 | 33 |
|
31 | 34 | #include "iceberg/arrow/arrow_io_util.h" |
32 | 35 | #include "iceberg/avro/avro_register.h" |
33 | 36 | #include "iceberg/expression/expressions.h" |
| 37 | +#include "iceberg/file_writer.h" |
34 | 38 | #include "iceberg/manifest/manifest_entry.h" |
35 | 39 | #include "iceberg/manifest/manifest_list.h" |
36 | 40 | #include "iceberg/manifest/manifest_writer.h" |
37 | 41 | #include "iceberg/partition_spec.h" |
38 | 42 | #include "iceberg/schema.h" |
| 43 | +#include "iceberg/schema_internal.h" |
39 | 44 | #include "iceberg/test/matchers.h" |
40 | 45 | #include "iceberg/transform.h" |
41 | 46 | #include "iceberg/type.h" |
@@ -156,6 +161,54 @@ class TestManifestReader : public testing::TestWithParam<int8_t> { |
156 | 161 | return std::move(manifest_result.value()); |
157 | 162 | } |
158 | 163 |
|
| 164 | + // Writes manifest entries given as JSON rows of |
| 165 | + // [status, snapshot_id, [content, file_path, file_format, [partition], record_count, |
| 166 | + // file_size_in_bytes]], bypassing ManifestWriter validation. |
| 167 | + ManifestFile WriteRawManifest(const std::string& json_rows) { |
| 168 | + auto partition_type = spec_->PartitionType(*schema_); |
| 169 | + EXPECT_THAT(partition_type, IsOk()); |
| 170 | + auto data_file_type = std::make_shared<StructType>(std::vector<SchemaField>{ |
| 171 | + DataFile::kContent, DataFile::kFilePath, |
| 172 | + // Written as optional so tests can bypass the writer's own non-null |
| 173 | + // enforcement and exercise the reader's required-field validation instead. |
| 174 | + SchemaField::MakeOptional(DataFile::kFileFormatFieldId, "file_format", string()), |
| 175 | + SchemaField::MakeRequired(DataFile::kPartitionFieldId, DataFile::kPartitionField, |
| 176 | + std::move(partition_type.value())), |
| 177 | + DataFile::kRecordCount, DataFile::kFileSize}); |
| 178 | + auto entry_schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| 179 | + ManifestEntry::kStatus, ManifestEntry::kSnapshotId, |
| 180 | + SchemaField::MakeRequired(ManifestEntry::kDataFileFieldId, |
| 181 | + ManifestEntry::kDataFileField, data_file_type)}); |
| 182 | + |
| 183 | + ArrowSchema arrow_c_schema; |
| 184 | + EXPECT_THAT(ToArrowSchema(*entry_schema, &arrow_c_schema), IsOk()); |
| 185 | + auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); |
| 186 | + auto array = ::arrow::json::ArrayFromJSONString(arrow_schema, json_rows).ValueOrDie(); |
| 187 | + ArrowArray arrow_array; |
| 188 | + EXPECT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok()); |
| 189 | + |
| 190 | + const std::string manifest_path = MakeManifestPath(); |
| 191 | + auto writer_result = WriterFactoryRegistry::Open( |
| 192 | + FileFormatType::kAvro, |
| 193 | + {.path = manifest_path, .schema = entry_schema, .io = file_io_}); |
| 194 | + EXPECT_THAT(writer_result, IsOk()); |
| 195 | + auto writer = std::move(writer_result.value()); |
| 196 | + EXPECT_THAT(writer->Write(&arrow_array), IsOk()); |
| 197 | + EXPECT_THAT(writer->Close(), IsOk()); |
| 198 | + auto length = writer->length(); |
| 199 | + EXPECT_THAT(length, IsOk()); |
| 200 | + |
| 201 | + return ManifestFile{ |
| 202 | + .manifest_path = manifest_path, |
| 203 | + .manifest_length = length.value(), |
| 204 | + .partition_spec_id = spec_->spec_id(), |
| 205 | + .content = ManifestContent::kData, |
| 206 | + .sequence_number = 0, |
| 207 | + .min_sequence_number = 0, |
| 208 | + .added_snapshot_id = 1000L, |
| 209 | + }; |
| 210 | + } |
| 211 | + |
159 | 212 | std::shared_ptr<FileIO> file_io_; |
160 | 213 | std::shared_ptr<Schema> schema_; |
161 | 214 | std::shared_ptr<PartitionSpec> spec_; |
@@ -629,6 +682,39 @@ TEST_P(TestManifestReader, SelectDistinguishesEmptyAndWildcard) { |
629 | 682 | EXPECT_EQ(all_entries.front().data_file->partition.values().front(), Literal::Int(3)); |
630 | 683 | } |
631 | 684 |
|
| 685 | +TEST_P(TestManifestReader, RejectsInvalidDataFileContent) { |
| 686 | + auto manifest = WriteRawManifest( |
| 687 | + R"([[1, 1000, [3, "/path/to/data.parquet", "parquet", [0], 1, 10]]])"); |
| 688 | + |
| 689 | + ICEBERG_UNWRAP_OR_FAIL(auto reader, |
| 690 | + ManifestReader::Make(manifest, file_io_, schema_, spec_)); |
| 691 | + auto entries = reader->Entries(); |
| 692 | + EXPECT_THAT(entries, IsError(ErrorKind::kInvalidArgument)); |
| 693 | + EXPECT_THAT(entries, HasErrorMessage("Invalid data file content: 3")); |
| 694 | +} |
| 695 | + |
| 696 | +TEST_P(TestManifestReader, RejectsInvalidEntryStatus) { |
| 697 | + auto manifest = WriteRawManifest( |
| 698 | + R"([[7, 1000, [0, "/path/to/data.parquet", "parquet", [0], 1, 10]]])"); |
| 699 | + |
| 700 | + ICEBERG_UNWRAP_OR_FAIL(auto reader, |
| 701 | + ManifestReader::Make(manifest, file_io_, schema_, spec_)); |
| 702 | + auto entries = reader->Entries(); |
| 703 | + EXPECT_THAT(entries, IsError(ErrorKind::kInvalidArgument)); |
| 704 | + EXPECT_THAT(entries, HasErrorMessage("Invalid manifest status: 7")); |
| 705 | +} |
| 706 | + |
| 707 | +TEST_P(TestManifestReader, RejectsNullRequiredFileFormat) { |
| 708 | + auto manifest = |
| 709 | + WriteRawManifest(R"([[0, 1000, [0, "/path/to/data.parquet", null, [0], 1, 10]]])"); |
| 710 | + |
| 711 | + ICEBERG_UNWRAP_OR_FAIL(auto reader, |
| 712 | + ManifestReader::Make(manifest, file_io_, schema_, spec_)); |
| 713 | + auto entries = reader->Entries(); |
| 714 | + EXPECT_THAT(entries, IsError(ErrorKind::kInvalidArgument)); |
| 715 | + EXPECT_THAT(entries, HasErrorMessage("Required field 'file_format' is null at row 0")); |
| 716 | +} |
| 717 | + |
632 | 718 | TEST(ManifestReaderStaticTest, TestShouldDropStats) { |
633 | 719 | EXPECT_TRUE(ManifestReader::ShouldDropStats({})); |
634 | 720 | EXPECT_FALSE(ManifestReader::ShouldDropStats({std::string(Schema::kAllColumns)})); |
|
0 commit comments