| /* |
| * 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 <optional> |
| #include <sstream> |
| #include <string> |
| #include <string_view> |
| #include <unordered_map> |
| |
| #include <arrow/array.h> |
| #include <arrow/array/array_base.h> |
| #include <arrow/c/bridge.h> |
| #include <arrow/filesystem/filesystem.h> |
| #include <arrow/json/from_string.h> |
| #include <avro/DataFile.hh> |
| #include <avro/Generic.hh> |
| #include <gtest/gtest.h> |
| |
| #include "iceberg/arrow/arrow_io_internal.h" |
| #include "iceberg/avro/avro_constants.h" |
| #include "iceberg/avro/avro_register.h" |
| #include "iceberg/avro/avro_stream_internal.h" |
| #include "iceberg/avro/avro_writer.h" |
| #include "iceberg/file_reader.h" |
| #include "iceberg/metadata_columns.h" |
| #include "iceberg/schema.h" |
| #include "iceberg/schema_internal.h" |
| #include "iceberg/test/matchers.h" |
| #include "iceberg/test/std_io.h" |
| #include "iceberg/test/temp_file_test_base.h" |
| #include "iceberg/type.h" |
| #include "iceberg/util/checked_cast.h" |
| |
| namespace iceberg::avro { |
| |
| namespace { |
| |
| ::avro::NodePtr UnwrapOptional(const ::avro::NodePtr& node) { |
| if (node->type() != ::avro::AVRO_UNION) { |
| return node; |
| } |
| |
| for (size_t i = 0; i < node->leaves(); ++i) { |
| if (node->leafAt(i)->type() != ::avro::AVRO_NULL) { |
| return node->leafAt(i); |
| } |
| } |
| return node; |
| } |
| |
| std::optional<int32_t> FieldIdAt(const ::avro::NodePtr& node, size_t index) { |
| if (index >= node->customAttributes()) { |
| return std::nullopt; |
| } |
| |
| auto field_id = node->customAttributesAt(index).getAttribute(std::string(kFieldIdProp)); |
| if (!field_id.has_value()) { |
| return std::nullopt; |
| } |
| return std::stoi(field_id.value()); |
| } |
| |
| } // namespace |
| |
| class AvroReaderTest : public TempFileTestBase { |
| protected: |
| static void SetUpTestSuite() { RegisterAll(); } |
| |
| void SetUp() override { |
| TempFileTestBase::SetUp(); |
| file_io_ = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); |
| temp_avro_file_ = "avro_reader_test.avro"; |
| } |
| |
| bool skip_datum_{true}; |
| |
| void CreateSimpleAvroFile() { |
| // Create simple avro file using the writer API instead of direct Avro library |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeOptional(2, "name", std::make_shared<StringType>())}); |
| |
| ArrowSchema arrow_c_schema; |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); |
| |
| auto array = ::arrow::json::ArrayFromJSONString( |
| ::arrow::struct_(arrow_schema->fields()), |
| R"([[1, "Alice"], [2, "Bob"], [3, "Charlie"]])") |
| .ValueOrDie(); |
| |
| struct ArrowArray arrow_array; |
| auto export_result = ::arrow::ExportArray(*array, &arrow_array); |
| ASSERT_TRUE(export_result.ok()); |
| |
| auto writer_result = |
| WriterFactoryRegistry::Open(FileFormatType::kAvro, { |
| .path = temp_avro_file_, |
| .schema = schema, |
| .io = file_io_, |
| }); |
| ASSERT_TRUE(writer_result.has_value()); |
| auto writer = std::move(writer_result.value()); |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); |
| ASSERT_THAT(writer->Close(), IsOk()); |
| } |
| |
| void VerifyNextBatch(Reader& reader, std::string_view expected_json) { |
| // Boilerplate to get Arrow schema |
| auto schema_result = reader.Schema(); |
| ASSERT_THAT(schema_result, IsOk()); |
| auto arrow_c_schema = std::move(schema_result.value()); |
| auto import_schema_result = ::arrow::ImportType(&arrow_c_schema); |
| auto arrow_schema = import_schema_result.ValueOrDie(); |
| |
| // Boilerplate to get Arrow array |
| auto data = reader.Next(); |
| ASSERT_THAT(data, IsOk()); |
| ASSERT_TRUE(data.value().has_value()); |
| auto arrow_c_array = data.value().value(); |
| auto data_result = ::arrow::ImportArray(&arrow_c_array, arrow_schema); |
| auto arrow_array = data_result.ValueOrDie(); |
| |
| // Verify data |
| auto expected_array = |
| ::arrow::json::ArrayFromJSONString(arrow_schema, expected_json).ValueOrDie(); |
| ASSERT_TRUE(arrow_array->Equals(*expected_array)); |
| } |
| |
| void VerifyExhausted(Reader& reader) { |
| auto data = reader.Next(); |
| ASSERT_THAT(data, IsOk()); |
| ASSERT_FALSE(data.value().has_value()); |
| } |
| |
| void WriteAndVerify(std::shared_ptr<Schema> schema, |
| const std::string& expected_string) { |
| ArrowSchema arrow_c_schema; |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); |
| |
| auto arrow_schema_result = ::arrow::ImportType(&arrow_c_schema); |
| ASSERT_TRUE(arrow_schema_result.ok()); |
| auto arrow_schema = arrow_schema_result.ValueOrDie(); |
| |
| auto array_result = ::arrow::json::ArrayFromJSONString(arrow_schema, expected_string); |
| ASSERT_TRUE(array_result.ok()); |
| auto array = array_result.ValueOrDie(); |
| |
| struct ArrowArray arrow_array; |
| auto export_result = ::arrow::ExportArray(*array, &arrow_array); |
| ASSERT_TRUE(export_result.ok()); |
| |
| std::unordered_map<std::string, std::string> metadata = {{"k1", "v1"}, {"k2", "v2"}}; |
| |
| auto writer_result = |
| WriterFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_, |
| .schema = schema, |
| .io = file_io_, |
| .metadata = metadata}); |
| ASSERT_TRUE(writer_result.has_value()); |
| auto writer = std::move(writer_result.value()); |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); |
| ASSERT_THAT(writer->Close(), IsOk()); |
| ICEBERG_UNWRAP_OR_FAIL(auto written_length, writer->length()); |
| |
| ReaderProperties reader_properties; |
| reader_properties.Set(ReaderProperties::kAvroSkipDatum, skip_datum_); |
| |
| auto reader_result = ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, {.path = temp_avro_file_, |
| .length = written_length, |
| .io = file_io_, |
| .projection = schema, |
| .properties = std::move(reader_properties)}); |
| ASSERT_THAT(reader_result, IsOk()); |
| auto reader = std::move(reader_result.value()); |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, expected_string)); |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); |
| |
| auto metadata_result = reader->Metadata(); |
| ASSERT_THAT(metadata_result, IsOk()); |
| auto read_metadata = std::move(metadata_result.value()); |
| for (const auto& [key, value] : metadata) { |
| auto it = read_metadata.find(key); |
| ASSERT_NE(it, read_metadata.end()); |
| ASSERT_EQ(it->second, value); |
| } |
| } |
| |
| std::shared_ptr<FileIO> file_io_; |
| std::string temp_avro_file_; |
| }; |
| |
| TEST_F(AvroReaderTest, ReadTwoFields) { |
| CreateSimpleAvroFile(); |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeOptional(2, "name", std::make_shared<StringType>())}); |
| |
| auto reader_result = ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, |
| {.path = temp_avro_file_, .io = file_io_, .projection = schema}); |
| ASSERT_THAT(reader_result, IsOk()); |
| auto reader = std::move(reader_result.value()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyNextBatch(*reader, R"([[1, "Alice"], [2, "Bob"], [3, "Charlie"]])")); |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); |
| } |
| |
| TEST_F(AvroReaderTest, RoundTripWithGenericFileIO) { |
| file_io_ = std::make_shared<iceberg::test::StdFileIO>(); |
| temp_avro_file_ = CreateNewTempFilePathWithSuffix(".avro"); |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeOptional(2, "name", std::make_shared<StringType>())}); |
| |
| ASSERT_NO_FATAL_FAILURE(WriteAndVerify(schema, R"([[1, "Foo"], [2, "Bar"]])")); |
| } |
| |
| TEST_F(AvroReaderTest, ReadReorderedFieldsWithNulls) { |
| CreateSimpleAvroFile(); |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeOptional(2, "name", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeOptional(3, "score", std::make_shared<DoubleType>())}); |
| |
| auto reader_result = ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, |
| {.path = temp_avro_file_, .io = file_io_, .projection = schema}); |
| ASSERT_THAT(reader_result, IsOk()); |
| auto reader = std::move(reader_result.value()); |
| |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch( |
| *reader, R"([["Alice", 1, null], ["Bob", 2, null], ["Charlie", 3, null]])")); |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); |
| } |
| |
| TEST_F(AvroReaderTest, ReadWithBatchSize) { |
| CreateSimpleAvroFile(); |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>())}); |
| |
| ReaderProperties reader_properties; |
| reader_properties.Set(ReaderProperties::kBatchSize, int64_t{2}); |
| |
| auto reader_result = ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, {.path = temp_avro_file_, |
| .io = file_io_, |
| .projection = schema, |
| .properties = std::move(reader_properties)}); |
| ASSERT_THAT(reader_result, IsOk()); |
| auto reader = std::move(reader_result.value()); |
| |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[1], [2]])")); |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[3]])")); |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); |
| } |
| |
| TEST_F(AvroReaderTest, BufferSizeConfiguration) { |
| // Test default buffer size |
| ReaderProperties properties1; |
| ASSERT_EQ(properties1.Get(ReaderProperties::kAvroBufferSize), 1024 * 1024); |
| |
| // Test setting custom buffer size |
| ReaderProperties properties2; |
| constexpr int64_t kCustomBufferSize = 2 * 1024 * 1024; // 2MB |
| properties2.Set(ReaderProperties::kAvroBufferSize, kCustomBufferSize); |
| ASSERT_EQ(properties2.Get(ReaderProperties::kAvroBufferSize), kCustomBufferSize); |
| |
| // Test setting via FromMap |
| std::unordered_map<std::string, std::string> config_map = { |
| {"read.avro.buffer-size", "4194304"} // 4MB |
| }; |
| auto properties3 = ReaderProperties::FromMap(config_map); |
| ASSERT_EQ(properties3.Get(ReaderProperties::kAvroBufferSize), 4194304); |
| |
| // Test that unset returns to default |
| properties2.Unset(ReaderProperties::kAvroBufferSize); |
| ASSERT_EQ(properties2.Get(ReaderProperties::kAvroBufferSize), 1024 * 1024); |
| } |
| |
| // Parameterized test fixture for testing both DirectDecoder and GenericDatum modes |
| class AvroReaderParameterizedTest : public AvroReaderTest, |
| public ::testing::WithParamInterface<bool> { |
| protected: |
| void SetUp() override { |
| AvroReaderTest::SetUp(); |
| skip_datum_ = GetParam(); |
| } |
| }; |
| |
| TEST_P(AvroReaderParameterizedTest, AvroWriterBasicType) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "name", std::make_shared<StringType>())}); |
| |
| std::string expected_string = R"([["Hello"], ["世界"], ["nanoarrow"]])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, AvroWriterNestedType) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired( |
| 2, "info", |
| std::make_shared<iceberg::StructType>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(3, "name", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(4, "age", std::make_shared<IntType>())}))}); |
| |
| std::string expected_string = |
| R"([[1, ["Alice", 25]], [2, ["Bob", 30]], [3, ["Ivy", 35]]])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, AllPrimitiveTypes) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "bool_col", std::make_shared<BooleanType>()), |
| SchemaField::MakeRequired(2, "int_col", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(3, "long_col", std::make_shared<LongType>()), |
| SchemaField::MakeRequired(4, "float_col", std::make_shared<FloatType>()), |
| SchemaField::MakeRequired(5, "double_col", std::make_shared<DoubleType>()), |
| SchemaField::MakeRequired(6, "string_col", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(7, "binary_col", std::make_shared<BinaryType>())}); |
| |
| std::string expected_string = R"([ |
| [true, 42, 1234567890, 3.14, 2.71828, "test", "AQID"], |
| [false, -100, -9876543210, -1.5, 0.0, "hello", "BAUG"] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| // Skipping DecimalType test - requires specific decimal encoding in JSON |
| |
| TEST_P(AvroReaderParameterizedTest, DateTimeTypes) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "date_col", std::make_shared<DateType>()), |
| SchemaField::MakeRequired(2, "time_col", std::make_shared<TimeType>()), |
| SchemaField::MakeRequired(3, "timestamp_col", std::make_shared<TimestampType>())}); |
| |
| // Dates as days since epoch, time/timestamps as microseconds |
| std::string expected_string = R"([ |
| [18628, 43200000000, 1640995200000000], |
| [18629, 86399000000, 1641081599000000] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, NestedStruct) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired( |
| 2, "person", |
| std::make_shared<iceberg::StructType>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(3, "name", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(4, "age", std::make_shared<IntType>()), |
| SchemaField::MakeOptional( |
| 5, "address", |
| std::make_shared<iceberg::StructType>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(6, "street", |
| std::make_shared<StringType>()), |
| SchemaField::MakeRequired(7, "city", |
| std::make_shared<StringType>())}))}))}); |
| |
| std::string expected_string = R"([ |
| [1, ["Alice", 30, ["123 Main St", "NYC"]]], |
| [2, ["Bob", 25, ["456 Oak Ave", "LA"]]] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, ListType) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(2, "tags", |
| std::make_shared<ListType>(SchemaField::MakeRequired( |
| 3, "element", std::make_shared<StringType>())))}); |
| |
| std::string expected_string = R"([ |
| [1, ["tag1", "tag2", "tag3"]], |
| [2, ["foo", "bar"]], |
| [3, []] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, MapType) { |
| auto schema = std::make_shared<iceberg::Schema>( |
| std::vector<SchemaField>{SchemaField::MakeRequired( |
| 1, "properties", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(2, "key", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(3, "value", std::make_shared<IntType>())))}); |
| |
| std::string expected_string = R"([ |
| [[["key1", 100], ["key2", 200]]], |
| [[["a", 1], ["b", 2], ["c", 3]]] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, MapTypeWithNonStringKey) { |
| auto schema = std::make_shared<iceberg::Schema>( |
| std::vector<SchemaField>{SchemaField::MakeRequired( |
| 1, "int_map", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(2, "key", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(3, "value", std::make_shared<StringType>())))}); |
| |
| std::string expected_string = R"([ |
| [[[1, "one"], [2, "two"], [3, "three"]]], |
| [[[10, "ten"], [20, "twenty"]]] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_F(AvroReaderTest, ProjectionSubsetAndReorder) { |
| // Write file with full schema |
| auto write_schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(2, "name", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(3, "age", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(4, "city", std::make_shared<StringType>())}); |
| |
| std::string write_data = R"([ |
| [1, "Alice", 25, "NYC"], |
| [2, "Bob", 30, "SF"], |
| [3, "Charlie", 35, "LA"] |
| ])"; |
| |
| // Write with full schema |
| ArrowSchema arrow_c_schema; |
| ASSERT_THAT(ToArrowSchema(*write_schema, &arrow_c_schema), IsOk()); |
| auto arrow_schema_result = ::arrow::ImportType(&arrow_c_schema); |
| ASSERT_TRUE(arrow_schema_result.ok()); |
| auto arrow_schema = arrow_schema_result.ValueOrDie(); |
| |
| auto array_result = ::arrow::json::ArrayFromJSONString(arrow_schema, write_data); |
| ASSERT_TRUE(array_result.ok()); |
| auto array = array_result.ValueOrDie(); |
| |
| struct ArrowArray arrow_array; |
| auto export_result = ::arrow::ExportArray(*array, &arrow_array); |
| ASSERT_TRUE(export_result.ok()); |
| |
| std::unordered_map<std::string, std::string> metadata = {{"k1", "v1"}}; |
| auto writer_result = |
| WriterFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_, |
| .schema = write_schema, |
| .io = file_io_, |
| .metadata = metadata}); |
| ASSERT_TRUE(writer_result.has_value()); |
| auto writer = std::move(writer_result.value()); |
| ASSERT_THAT(writer->Write(&arrow_array), IsOk()); |
| ASSERT_THAT(writer->Close(), IsOk()); |
| ICEBERG_UNWRAP_OR_FAIL(auto written_length, writer->length()); |
| |
| // Read with projected schema: subset of columns (city, id) in different order |
| auto read_schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(4, "city", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>())}); |
| |
| auto reader_result = |
| ReaderFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_, |
| .length = written_length, |
| .io = file_io_, |
| .projection = read_schema}); |
| ASSERT_THAT(reader_result, IsOk()); |
| auto reader = std::move(reader_result.value()); |
| |
| // Verify reordered subset |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyNextBatch(*reader, R"([["NYC", 1], ["SF", 2], ["LA", 3]])")); |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, ComplexNestedTypes) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(2, "nested_list", |
| std::make_shared<ListType>(SchemaField::MakeRequired( |
| 3, "element", |
| std::make_shared<ListType>(SchemaField::MakeRequired( |
| 4, "element", std::make_shared<IntType>())))))}); |
| |
| std::string expected_string = R"([ |
| [1, [[1, 2], [3, 4]]], |
| [2, [[5], [6, 7, 8]]] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, OptionalFieldsWithNulls) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeOptional(2, "name", std::make_shared<StringType>()), |
| SchemaField::MakeOptional(3, "age", std::make_shared<IntType>())}); |
| |
| std::string expected_string = R"([ |
| [1, "Alice", 30], |
| [2, null, 25], |
| [3, "Charlie", null], |
| [4, null, null] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, LargeDataset) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<LongType>()), |
| SchemaField::MakeRequired(2, "value", std::make_shared<DoubleType>())}); |
| |
| // Generate large dataset JSON |
| std::ostringstream json; |
| json << "["; |
| for (int i = 0; i < 1000; ++i) { |
| if (i > 0) json << ", "; |
| json << "[" << i << ", " << (i * 1.5) << "]"; |
| } |
| json << "]"; |
| |
| WriteAndVerify(schema, json.str()); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, EmptyCollections) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(2, "list_col", |
| std::make_shared<ListType>(SchemaField::MakeRequired( |
| 3, "element", std::make_shared<IntType>())))}); |
| |
| std::string expected_string = R"([ |
| [1, []], |
| [2, [10, 20, 30]] |
| ])"; |
| |
| WriteAndVerify(schema, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, ReadFilePathColumn) { |
| temp_avro_file_ = "avro_metadata_path.avro"; |
| CreateSimpleAvroFile(); |
| |
| // Create schema with _path metadata column |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", int32()), |
| MetadataColumns::kFilePath, |
| }); |
| |
| const std::string expected_string = R"([ |
| [1, "avro_metadata_path.avro"], |
| [2, "avro_metadata_path.avro"], |
| [3, "avro_metadata_path.avro"] |
| ])"; |
| |
| ICEBERG_UNWRAP_OR_FAIL( |
| auto reader, ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, |
| {.path = temp_avro_file_, .io = file_io_, .projection = schema})); |
| |
| VerifyNextBatch(*reader, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, ReadRowPositionColumn) { |
| temp_avro_file_ = "avro_metadata_pos.avro"; |
| CreateSimpleAvroFile(); |
| |
| // Create schema with _pos metadata column |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", int32()), |
| MetadataColumns::kRowPosition, |
| }); |
| |
| const std::string expected_string = R"([[1, 0], [2, 1], [3, 2]])"; |
| |
| ICEBERG_UNWRAP_OR_FAIL( |
| auto reader, ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, |
| {.path = temp_avro_file_, .io = file_io_, .projection = schema})); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, ReadBothMetadataColumns) { |
| temp_avro_file_ = "avro_metadata_path_pos.avro"; |
| CreateSimpleAvroFile(); |
| |
| // Create schema with both _file and _pos metadata columns |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", int32()), |
| MetadataColumns::kFilePath, |
| MetadataColumns::kRowPosition, |
| }); |
| |
| const std::string expected_string = R"([ |
| [1, "avro_metadata_path_pos.avro", 0], |
| [2, "avro_metadata_path_pos.avro", 1], |
| [3, "avro_metadata_path_pos.avro", 2] |
| ])"; |
| |
| ICEBERG_UNWRAP_OR_FAIL( |
| auto reader, ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, |
| {.path = temp_avro_file_, .io = file_io_, .projection = schema})); |
| |
| VerifyNextBatch(*reader, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, ReadMetadataOnlyProjection) { |
| temp_avro_file_ = "avro_metadata_only.avro"; |
| CreateSimpleAvroFile(); |
| |
| // Create schema with only metadata columns (no data columns) |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| MetadataColumns::kFilePath, |
| MetadataColumns::kRowPosition, |
| }); |
| |
| const std::string expected_string = R"([ |
| ["avro_metadata_only.avro", 0], |
| ["avro_metadata_only.avro", 1], |
| ["avro_metadata_only.avro", 2] |
| ])"; |
| |
| ICEBERG_UNWRAP_OR_FAIL( |
| auto reader, ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, |
| {.path = temp_avro_file_, .io = file_io_, .projection = schema})); |
| |
| VerifyNextBatch(*reader, expected_string); |
| } |
| |
| TEST_P(AvroReaderParameterizedTest, SplitWithRowPositionNotSupported) { |
| CreateSimpleAvroFile(); |
| |
| // Create schema with _pos metadata column |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", int32()), |
| MetadataColumns::kRowPosition, |
| }); |
| |
| auto reader_result = ReaderFactoryRegistry::Open( |
| FileFormatType::kAvro, {.path = temp_avro_file_, |
| .split = Split{.offset = 100, .length = 200}, |
| .io = file_io_, |
| .projection = schema}); |
| |
| ASSERT_THAT(reader_result, IsError(ErrorKind::kNotSupported)); |
| EXPECT_THAT(reader_result, |
| HasErrorMessage("'_pos' metadata column with split is not supported")); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(DirectDecoderModes, AvroReaderParameterizedTest, |
| ::testing::Bool(), |
| [](const ::testing::TestParamInfo<bool>& info) { |
| return info.param ? "DirectDecoder" : "GenericDatum"; |
| }); |
| |
| // Parameterized test fixture for testing both direct encoder and GenericDatum modes |
| class AvroWriterTest : public ::testing::Test, |
| public ::testing::WithParamInterface<bool> { |
| protected: |
| static void SetUpTestSuite() { RegisterAll(); } |
| |
| void SetUp() override { |
| file_io_ = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); |
| temp_avro_file_ = "avro_writer_test.avro"; |
| skip_datum_ = GetParam(); |
| } |
| |
| void WriteAvroFile( |
| std::shared_ptr<Schema> schema, const std::string& json_data, |
| const std::unordered_map<std::string, std::string>& extra_properties = {}) { |
| ArrowSchema arrow_c_schema; |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); |
| |
| auto arrow_schema_result = ::arrow::ImportType(&arrow_c_schema); |
| ASSERT_TRUE(arrow_schema_result.ok()); |
| auto arrow_schema = arrow_schema_result.ValueOrDie(); |
| |
| auto array_result = ::arrow::json::ArrayFromJSONString(arrow_schema, json_data); |
| ASSERT_TRUE(array_result.ok()); |
| auto array = array_result.ValueOrDie(); |
| |
| struct ArrowArray arrow_array; |
| auto export_result = ::arrow::ExportArray(*array, &arrow_array); |
| ASSERT_TRUE(export_result.ok()); |
| |
| std::unordered_map<std::string, std::string> metadata = { |
| {"writer_test", "direct_encoder"}}; |
| |
| WriterProperties writer_properties; |
| writer_properties.Set(WriterProperties::kAvroSkipDatum, skip_datum_); |
| for (const auto& [key, value] : extra_properties) { |
| writer_properties.mutable_configs().emplace(key, value); |
| } |
| |
| auto writer_result = WriterFactoryRegistry::Open( |
| FileFormatType::kAvro, {.path = temp_avro_file_, |
| .schema = schema, |
| .io = file_io_, |
| .metadata = metadata, |
| .properties = std::move(writer_properties)}); |
| ASSERT_TRUE(writer_result.has_value()); |
| writer_ = std::move(writer_result.value()); |
| ASSERT_THAT(writer_->Write(&arrow_array), IsOk()); |
| ASSERT_THAT(writer_->Close(), IsOk()); |
| write_schema_ = schema; |
| } |
| |
| void VerifyNextBatch(Reader& reader, std::string_view expected_json) { |
| // Boilerplate to get Arrow schema |
| auto schema_result = reader.Schema(); |
| ASSERT_THAT(schema_result, IsOk()); |
| auto arrow_c_schema = std::move(schema_result.value()); |
| auto import_schema_result = ::arrow::ImportType(&arrow_c_schema); |
| auto arrow_schema = import_schema_result.ValueOrDie(); |
| |
| // Boilerplate to get Arrow array |
| auto data = reader.Next(); |
| ASSERT_THAT(data, IsOk()); |
| ASSERT_TRUE(data.value().has_value()); |
| auto arrow_c_array = data.value().value(); |
| auto data_result = ::arrow::ImportArray(&arrow_c_array, arrow_schema); |
| auto arrow_array = data_result.ValueOrDie(); |
| |
| // Verify data |
| auto expected_array = |
| ::arrow::json::ArrayFromJSONString(arrow_schema, expected_json).ValueOrDie(); |
| ASSERT_TRUE(arrow_array->Equals(*expected_array)); |
| } |
| |
| void VerifyExhausted(Reader& reader) { |
| auto data = reader.Next(); |
| ASSERT_THAT(data, IsOk()); |
| ASSERT_FALSE(data.value().has_value()); |
| } |
| |
| void VerifyWrittenData(const std::string& expected_json) { |
| ICEBERG_UNWRAP_OR_FAIL(auto written_length, writer_->length()); |
| |
| auto reader_result = |
| ReaderFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_, |
| .length = written_length, |
| .io = file_io_, |
| .projection = write_schema_}); |
| ASSERT_THAT(reader_result, IsOk()); |
| auto reader = std::move(reader_result.value()); |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, expected_json)); |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); |
| } |
| |
| ::avro::ValidSchema PhysicalAvroSchema() { |
| auto& mock_io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); |
| auto input = mock_io.fs()->OpenInputFile(temp_avro_file_).ValueOrDie(); |
| auto input_stream = std::make_unique<AvroInputStream>(std::move(input), 1024 * 1024); |
| ::avro::DataFileReader<::avro::GenericDatum> avro_reader(std::move(input_stream)); |
| return avro_reader.dataSchema(); |
| } |
| |
| std::shared_ptr<FileIO> file_io_; |
| std::string temp_avro_file_; |
| bool skip_datum_{true}; |
| std::shared_ptr<Schema> write_schema_; |
| std::unique_ptr<Writer> writer_; |
| }; |
| |
| TEST_P(AvroWriterTest, WritePrimitiveTypes) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "bool_col", std::make_shared<BooleanType>()), |
| SchemaField::MakeRequired(2, "int_col", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(3, "long_col", std::make_shared<LongType>()), |
| SchemaField::MakeRequired(4, "float_col", std::make_shared<FloatType>()), |
| SchemaField::MakeRequired(5, "double_col", std::make_shared<DoubleType>()), |
| SchemaField::MakeRequired(6, "string_col", std::make_shared<StringType>())}); |
| |
| std::string test_data = R"([ |
| [true, 42, 1234567890, 3.14, 2.71828, "hello"], |
| [false, -100, -9876543210, -1.5, 0.0, "world"] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteTemporalTypes) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "date_col", std::make_shared<DateType>()), |
| SchemaField::MakeRequired(2, "time_col", std::make_shared<TimeType>()), |
| SchemaField::MakeRequired(3, "timestamp_col", std::make_shared<TimestampType>())}); |
| |
| std::string test_data = R"([ |
| [18628, 43200000000, 1640995200000000], |
| [18629, 86399000000, 1641081599000000] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteNestedStruct) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired( |
| 2, "person", |
| std::make_shared<iceberg::StructType>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(3, "name", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(4, "age", std::make_shared<IntType>())}))}); |
| |
| std::string test_data = R"([ |
| [1, ["Alice", 30]], |
| [2, ["Bob", 25]] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteListType) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(2, "tags", |
| std::make_shared<ListType>(SchemaField::MakeRequired( |
| 3, "element", std::make_shared<StringType>())))}); |
| |
| std::string test_data = R"([ |
| [1, ["tag1", "tag2", "tag3"]], |
| [2, ["foo", "bar"]], |
| [3, []] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteMapTypeWithStringKey) { |
| auto schema = std::make_shared<iceberg::Schema>( |
| std::vector<SchemaField>{SchemaField::MakeRequired( |
| 1, "properties", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(2, "key", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(3, "value", std::make_shared<IntType>())))}); |
| |
| std::string test_data = R"([ |
| [[["key1", 100], ["key2", 200]]], |
| [[["a", 1], ["b", 2], ["c", 3]]] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteMapTypeWithNonStringKey) { |
| auto schema = std::make_shared<iceberg::Schema>( |
| std::vector<SchemaField>{SchemaField::MakeRequired( |
| 1, "int_map", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(2, "key", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(3, "value", std::make_shared<StringType>())))}); |
| |
| std::string test_data = R"([ |
| [[[1, "one"], [2, "two"], [3, "three"]]], |
| [[[10, "ten"], [20, "twenty"]]] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteEmptyMaps) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired( |
| 1, "string_map", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(2, "key", std::make_shared<StringType>()), |
| SchemaField::MakeRequired(3, "value", std::make_shared<IntType>()))), |
| SchemaField::MakeRequired( |
| 4, "int_map", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(5, "key", std::make_shared<IntType>()), |
| SchemaField::MakeRequired(6, "value", std::make_shared<StringType>())))}); |
| |
| // Test empty maps for both string and non-string keys |
| std::string test_data = R"([ |
| [[], []], |
| [[["a", 1]], []] |
| ])"; |
| |
| // Just verify writing succeeds (empty maps are handled correctly by the encoder) |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteOptionalFields) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeOptional(2, "name", std::make_shared<StringType>()), |
| SchemaField::MakeOptional(3, "age", std::make_shared<IntType>())}); |
| |
| std::string test_data = R"([ |
| [1, "Alice", 30], |
| [2, null, 25], |
| [3, "Charlie", null], |
| [4, null, null] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WritesUnknownFieldsAsAvroNull) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeOptional(1, "id", int32()), |
| SchemaField::MakeOptional(2, "mystery", unknown()), |
| SchemaField::MakeOptional(3, "profile", |
| std::make_shared<StructType>(std::vector<SchemaField>{ |
| SchemaField::MakeOptional(4, "name", string()), |
| SchemaField::MakeOptional(5, "secret", unknown()), |
| })), |
| }); |
| |
| std::string test_data = R"([ |
| [1, null, {"name": "Person0", "secret": null}], |
| [2, null, {"name": "Person1", "secret": null}] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| |
| auto avro_schema = PhysicalAvroSchema(); |
| auto root = avro_schema.root(); |
| ASSERT_EQ(root->type(), ::avro::AVRO_RECORD); |
| // Unknown fields are written as AVRO_NULL, not pruned. |
| ASSERT_EQ(root->leaves(), 3); |
| EXPECT_EQ(root->nameAt(0), "id"); |
| EXPECT_EQ(FieldIdAt(root, 0), std::make_optional(1)); |
| EXPECT_EQ(root->nameAt(1), "mystery"); |
| EXPECT_EQ(root->leafAt(1)->type(), ::avro::AVRO_NULL); |
| EXPECT_EQ(FieldIdAt(root, 1), std::make_optional(2)); |
| EXPECT_EQ(root->nameAt(2), "profile"); |
| EXPECT_EQ(FieldIdAt(root, 2), std::make_optional(3)); |
| |
| auto profile = UnwrapOptional(root->leafAt(2)); |
| ASSERT_EQ(profile->type(), ::avro::AVRO_RECORD); |
| ASSERT_EQ(profile->leaves(), 2); |
| EXPECT_EQ(profile->nameAt(0), "name"); |
| EXPECT_EQ(FieldIdAt(profile, 0), std::make_optional(4)); |
| EXPECT_EQ(profile->nameAt(1), "secret"); |
| EXPECT_EQ(profile->leafAt(1)->type(), ::avro::AVRO_NULL); |
| EXPECT_EQ(FieldIdAt(profile, 1), std::make_optional(5)); |
| |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WritesUnknownListElementsAndMapValues) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", int32()), |
| SchemaField::MakeRequired(2, "mysteries", |
| std::make_shared<ListType>(SchemaField::MakeOptional( |
| 3, ListType::kElementName, unknown()))), |
| SchemaField::MakeRequired( |
| 4, "properties", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(5, MapType::kKeyName, string()), |
| SchemaField::MakeOptional(6, MapType::kValueName, unknown()))), |
| }); |
| |
| std::string test_data = R"([ |
| [1, [null, null], [["a", null], ["b", null]]], |
| [2, [], []], |
| [3, [null], [["c", null]]] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| |
| auto avro_schema = PhysicalAvroSchema(); |
| auto root = avro_schema.root(); |
| ASSERT_EQ(root->type(), ::avro::AVRO_RECORD); |
| ASSERT_EQ(root->leaves(), 3); |
| |
| auto mysteries = root->leafAt(1); |
| ASSERT_EQ(mysteries->type(), ::avro::AVRO_ARRAY); |
| ASSERT_EQ(mysteries->leaves(), 1); |
| EXPECT_EQ(mysteries->leafAt(0)->type(), ::avro::AVRO_NULL); |
| |
| auto properties = root->leafAt(2); |
| ASSERT_EQ(properties->type(), ::avro::AVRO_MAP); |
| ASSERT_EQ(properties->leaves(), 2); |
| EXPECT_EQ(properties->leafAt(1)->type(), ::avro::AVRO_NULL); |
| |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WritesUnknownFieldsNestedInsideListOrMapStructs) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeOptional(1, "id", int32()), |
| SchemaField::MakeOptional(2, "events", |
| std::make_shared<ListType>(SchemaField::MakeOptional( |
| 3, ListType::kElementName, |
| std::make_shared<StructType>(std::vector<SchemaField>{ |
| SchemaField::MakeOptional(4, "name", string()), |
| SchemaField::MakeOptional(5, "secret", unknown()), |
| })))), |
| SchemaField::MakeOptional( |
| 6, "properties", |
| std::make_shared<MapType>( |
| SchemaField::MakeRequired(7, MapType::kKeyName, iceberg::string()), |
| SchemaField::MakeOptional( |
| 8, MapType::kValueName, |
| std::make_shared<StructType>(std::vector<SchemaField>{ |
| SchemaField::MakeOptional(9, "label", string()), |
| SchemaField::MakeOptional(10, "secret", unknown()), |
| })))), |
| }); |
| |
| std::string test_data = R"([ |
| [1, [{"name": "open", "secret": null}, {"name": "close", "secret": null}], [["a", {"label": "A", "secret": null}]]], |
| [2, [], []] |
| ])"; |
| |
| WriteAvroFile(schema, test_data); |
| |
| auto avro_schema = PhysicalAvroSchema(); |
| auto root = avro_schema.root(); |
| ASSERT_EQ(root->type(), ::avro::AVRO_RECORD); |
| ASSERT_EQ(root->leaves(), 3); |
| |
| auto events = UnwrapOptional(root->leafAt(1)); |
| ASSERT_EQ(events->type(), ::avro::AVRO_ARRAY); |
| auto event = UnwrapOptional(events->leafAt(0)); |
| ASSERT_EQ(event->type(), ::avro::AVRO_RECORD); |
| ASSERT_EQ(event->leaves(), 2); |
| EXPECT_EQ(event->nameAt(0), "name"); |
| EXPECT_EQ(FieldIdAt(event, 0), std::make_optional(4)); |
| EXPECT_EQ(event->nameAt(1), "secret"); |
| EXPECT_EQ(event->leafAt(1)->type(), ::avro::AVRO_NULL); |
| EXPECT_EQ(FieldIdAt(event, 1), std::make_optional(5)); |
| |
| auto properties = UnwrapOptional(root->leafAt(2)); |
| ASSERT_EQ(properties->type(), ::avro::AVRO_MAP); |
| ASSERT_EQ(properties->leaves(), 2); |
| auto value = UnwrapOptional(properties->leafAt(1)); |
| ASSERT_EQ(value->type(), ::avro::AVRO_RECORD); |
| ASSERT_EQ(value->leaves(), 2); |
| EXPECT_EQ(value->nameAt(0), "label"); |
| EXPECT_EQ(FieldIdAt(value, 0), std::make_optional(9)); |
| EXPECT_EQ(value->nameAt(1), "secret"); |
| EXPECT_EQ(value->leafAt(1)->type(), ::avro::AVRO_NULL); |
| EXPECT_EQ(FieldIdAt(value, 1), std::make_optional(10)); |
| |
| VerifyWrittenData(test_data); |
| } |
| |
| TEST_P(AvroWriterTest, WriteLargeDataset) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<LongType>()), |
| SchemaField::MakeRequired(2, "value", std::make_shared<DoubleType>())}); |
| |
| // Generate large dataset JSON |
| std::ostringstream json; |
| json << "["; |
| for (int i = 0; i < 1000; ++i) { |
| if (i > 0) json << ", "; |
| json << "[" << i << ", " << (i * 1.5) << "]"; |
| } |
| json << "]"; |
| |
| WriteAvroFile(schema, json.str()); |
| VerifyWrittenData(json.str()); |
| } |
| |
| TEST_P(AvroWriterTest, MultipleAvroBlocks) { |
| auto schema = std::make_shared<Schema>( |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int32()), |
| SchemaField::MakeRequired(2, "name", string())}); |
| |
| const std::string json_data = R"([ |
| [1, "Alice_with_a_very_long_name_to_exceed_sync_interval"], |
| [2, "Bob_with_another_very_long_name_to_exceed_sync_interval"], |
| [3, "Charlie_with_yet_another_very_long_name_to_exceed_sync"], |
| [4, "David_with_a_super_long_name_that_will_exceed_interval"], |
| [5, "Eve_with_an_extremely_long_name_to_force_new_block_here"] |
| ])"; |
| |
| const std::vector<std::pair</*sync_interval*/ std::string, /*num_blocks*/ size_t>> |
| test_cases = {{"32", 5}, {"65536", 1}}; |
| |
| for (const auto& [interval, num_blocks] : test_cases) { |
| WriteAvroFile(schema, json_data, |
| {{WriterProperties::kAvroSyncInterval.key(), interval}}); |
| VerifyWrittenData(json_data); |
| |
| // Use raw avro-cpp reader to count blocks by tracking previousSync() changes |
| auto mock_io = internal::checked_pointer_cast<arrow::ArrowFileSystemFileIO>(file_io_); |
| auto input = mock_io->fs()->OpenInputFile(temp_avro_file_).ValueOrDie(); |
| auto input_stream = std::make_unique<AvroInputStream>(std::move(input), 1024 * 1024); |
| ::avro::DataFileReader<::avro::GenericDatum> avro_reader(std::move(input_stream)); |
| ::avro::GenericDatum datum(avro_reader.dataSchema()); |
| |
| size_t block_count = 0; |
| int64_t last_sync = -1; |
| |
| while (avro_reader.read(datum)) { |
| if (int64_t current_sync = avro_reader.previousSync(); current_sync != last_sync) { |
| block_count++; |
| last_sync = current_sync; |
| } |
| } |
| |
| ASSERT_EQ(block_count, num_blocks); |
| } |
| } |
| |
| TEST_P(AvroWriterTest, Metrics) { |
| auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{ |
| SchemaField::MakeRequired(1, "id", std::make_shared<IntType>()), |
| SchemaField::MakeOptional(2, "name", std::make_shared<StringType>())}); |
| |
| std::string test_data = R"([[1, "Alice"], [2, "Bob"], [3, "Charlie"]])"; |
| |
| // Write data but don't close yet |
| ArrowSchema arrow_c_schema; |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); |
| auto array = ::arrow::json::ArrayFromJSONString(arrow_schema, test_data).ValueOrDie(); |
| struct ArrowArray arrow_array; |
| ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok()); |
| |
| ICEBERG_UNWRAP_OR_FAIL( |
| writer_, |
| WriterFactoryRegistry::Open( |
| FileFormatType::kAvro, |
| {.path = temp_avro_file_, .schema = schema, .io = file_io_, .properties = {}})); |
| ASSERT_THAT(writer_->Write(&arrow_array), IsOk()); |
| |
| // Metrics should fail before close |
| ASSERT_THAT(writer_->metrics(), IsError(ErrorKind::kInvalid)); |
| |
| // After close, metrics should succeed |
| ASSERT_THAT(writer_->Close(), IsOk()); |
| ICEBERG_UNWRAP_OR_FAIL(auto metrics, writer_->metrics()); |
| ASSERT_TRUE(metrics.row_count.has_value()); |
| EXPECT_EQ(metrics.row_count.value(), 3); |
| EXPECT_TRUE(metrics.column_sizes.empty()); |
| EXPECT_TRUE(metrics.value_counts.empty()); |
| EXPECT_TRUE(metrics.null_value_counts.empty()); |
| EXPECT_TRUE(metrics.nan_value_counts.empty()); |
| EXPECT_TRUE(metrics.lower_bounds.empty()); |
| EXPECT_TRUE(metrics.upper_bounds.empty()); |
| } |
| |
| // Instantiate parameterized tests for both direct encoder and GenericDatum paths |
| INSTANTIATE_TEST_SUITE_P(DirectEncoderModes, AvroWriterTest, |
| ::testing::Values(true, false), |
| [](const ::testing::TestParamInfo<bool>& info) { |
| return info.param ? "DirectEncoder" : "GenericDatum"; |
| }); |
| |
| } // namespace iceberg::avro |