blob: a16116410254baeaf1be578659836b014c4bf743 [file]
/*
* 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 <array>
#include <format>
#include <initializer_list>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <arrow/array.h>
#include <arrow/array/array_base.h>
#include <arrow/array/builder_binary.h>
#include <arrow/array/builder_primitive.h>
#include <arrow/c/bridge.h>
#include <arrow/extension/uuid.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/expression/literal.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/test/test_resource.h"
#include "iceberg/type.h"
#include "iceberg/util/checked_cast.h"
#include "iceberg/util/uuid.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());
}
constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes1 = {
0x12, 0x3e, 0x45, 0x67, 0xe8, 0x9b, 0x12, 0xd3,
0xa4, 0x56, 0x42, 0x66, 0x14, 0x17, 0x40, 0x00};
constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes2 = {
0xf7, 0x9c, 0x3e, 0x09, 0x67, 0x7c, 0x4b, 0xbd,
0xa4, 0x79, 0x3f, 0x34, 0x9c, 0xb7, 0x85, 0xe7};
std::shared_ptr<::arrow::Array> MakeUuidArray(
std::initializer_list<const std::array<uint8_t, Uuid::kLength>*> values) {
::arrow::FixedSizeBinaryBuilder uuid_storage_builder(
::arrow::fixed_size_binary(Uuid::kLength));
for (const auto* value : values) {
EXPECT_TRUE(uuid_storage_builder.Append(value->data()).ok());
}
auto uuid_storage = uuid_storage_builder.Finish().ValueOrDie();
return ::arrow::ExtensionType::WrapArray(::arrow::extension::uuid(), uuid_storage);
}
std::shared_ptr<::arrow::Array> MakeInt32Array(std::initializer_list<int32_t> values) {
::arrow::Int32Builder builder;
for (auto value : values) {
EXPECT_TRUE(builder.Append(value).ok());
}
return builder.Finish().ValueOrDie();
}
std::shared_ptr<::arrow::Array> MakeStringArray(
std::initializer_list<std::string_view> values) {
::arrow::StringBuilder builder;
for (auto value : values) {
EXPECT_TRUE(builder.Append(value).ok());
}
return builder.Finish().ValueOrDie();
}
} // 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 CreateRowLineageAvroFile() {
auto schema = RowLineageSchema();
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, null, null],
[2, 777, 8],
[3, null, null]])")
.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());
}
std::shared_ptr<Schema> RowLineageSchema() {
return std::make_shared<Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "id", int32()),
MetadataColumns::kRowId,
MetadataColumns::kLastUpdatedSequenceNumber,
});
}
Result<std::unique_ptr<Reader>> OpenRowLineageReader(
const std::shared_ptr<Schema>& schema,
std::optional<int64_t> first_row_id = std::nullopt,
std::optional<int64_t> data_sequence_number = std::nullopt) {
ReaderProperties properties;
properties.Set(ReaderProperties::kAvroSkipDatum, skip_datum_);
return ReaderFactoryRegistry::Open(FileFormatType::kAvro,
{.path = temp_avro_file_,
.io = file_io_,
.projection = schema,
.first_row_id = first_row_id,
.data_sequence_number = data_sequence_number,
.properties = std::move(properties)});
}
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, ReadMissingFieldsWithDefaults) {
// The file contains only fields 1 and 2; the projected schema adds fields 3 and 4
// with initial-defaults, which are filled for all rows written before the columns
// existed.
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>()),
SchemaField(3, "score", std::make_shared<LongType>(), /*optional=*/false,
/*doc=*/{}, std::make_shared<const Literal>(Literal::Long(100))),
SchemaField(4, "status", std::make_shared<StringType>(), /*optional=*/true,
/*doc=*/{}, std::make_shared<const Literal>(Literal::String("active"))),
});
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", 100, "active"],
[2, "Bob", 100, "active"],
[3, "Charlie", 100, "active"]])"));
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, ProjectEmptyStructsOfDifferentWidths) {
auto make_struct = [](int32_t first_id, int count) {
std::vector<SchemaField> fields;
for (int i = 0; i < count; ++i) {
fields.push_back(SchemaField::MakeRequired(first_id + i, std::format("f{}", i),
std::make_shared<IntType>()));
}
return std::make_shared<StructType>(std::move(fields));
};
auto write_schema = std::make_shared<Schema>(
std::vector<SchemaField>{SchemaField::MakeRequired(1, "narrow", make_struct(10, 1)),
SchemaField::MakeRequired(2, "wide", make_struct(20, 6))});
ArrowSchema arrow_c_schema;
ASSERT_THAT(ToArrowSchema(*write_schema, &arrow_c_schema), IsOk());
auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie();
auto array =
::arrow::json::ArrayFromJSONString(
arrow_schema, R"([[[1], [1, 2, 3, 4, 5, 6]], [[7], [7, 8, 9, 10, 11, 12]]])")
.ValueOrDie();
struct ArrowArray arrow_array;
ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
auto writer_result = WriterFactoryRegistry::Open(
FileFormatType::kAvro,
{.path = temp_avro_file_, .schema = write_schema, .io = file_io_});
ASSERT_THAT(writer_result, IsOk());
auto writer = std::move(writer_result.value());
ASSERT_THAT(writer->Write(&arrow_array), IsOk());
ASSERT_THAT(writer->Close(), IsOk());
auto read_schema = std::make_shared<Schema>(
std::vector<SchemaField>{SchemaField::MakeRequired(1, "narrow", make_struct(10, 0)),
SchemaField::MakeRequired(2, "wide", make_struct(20, 0))});
ReaderProperties reader_properties;
reader_properties.Set(ReaderProperties::kAvroSkipDatum, true);
auto reader_result = ReaderFactoryRegistry::Open(
FileFormatType::kAvro, {.path = temp_avro_file_,
.io = file_io_,
.projection = read_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"([[[], []], [[], []]])"));
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, TimestampTzTypes) {
auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "timestamptz_col", iceberg::timestamp_tz()),
SchemaField::MakeRequired(2, "timestamptz_ns_col", iceberg::timestamptz_ns())});
std::string expected_string = R"([
[1640995200000000, 1640995200000000001],
[1641081599000000, 1641081599000000002]
])";
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, ReadRowLineage) {
temp_avro_file_ = "avro_row_lineage.avro";
CreateSimpleAvroFile();
ICEBERG_UNWRAP_OR_FAIL(auto reader, OpenRowLineageReader(RowLineageSchema(), 100L, 7L));
VerifyNextBatch(*reader, R"([[1, 100, 7], [2, 101, 7], [3, 102, 7]])");
}
TEST_P(AvroReaderParameterizedTest, ReadPartialLineage) {
temp_avro_file_ = "avro_partial_lineage.avro";
CreateSimpleAvroFile();
auto schema = RowLineageSchema();
ICEBERG_UNWRAP_OR_FAIL(auto reader, OpenRowLineageReader(schema));
VerifyNextBatch(*reader, R"([[1, null, null], [2, null, null], [3, null, null]])");
ICEBERG_UNWRAP_OR_FAIL(auto reader_with_row_id, OpenRowLineageReader(schema, 100L));
VerifyNextBatch(*reader_with_row_id,
R"([[1, 100, null], [2, 101, null], [3, 102, null]])");
ICEBERG_UNWRAP_OR_FAIL(auto reader_with_sequence,
OpenRowLineageReader(schema, std::nullopt, 7L));
VerifyNextBatch(*reader_with_sequence, R"([[1, null, 7], [2, null, 7], [3, null, 7]])");
}
TEST_P(AvroReaderParameterizedTest, ReadPhysicalLineage) {
temp_avro_file_ = "avro_physical_lineage.avro";
CreateRowLineageAvroFile();
auto schema = RowLineageSchema();
ICEBERG_UNWRAP_OR_FAIL(auto reader, OpenRowLineageReader(schema, 100L));
VerifyNextBatch(*reader, R"([[1, 100, null], [2, 777, 8], [3, 102, null]])");
}
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"));
}
TEST_P(AvroReaderParameterizedTest, SplitWithRowIdNotSupported) {
CreateSimpleAvroFile();
auto schema = std::make_shared<Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "id", int32()),
MetadataColumns::kRowId,
});
ReaderProperties properties;
properties.Set(ReaderProperties::kAvroSkipDatum, skip_datum_);
auto reader_result = ReaderFactoryRegistry::Open(
FileFormatType::kAvro, {.path = temp_avro_file_,
.split = Split{.offset = 100, .length = 200},
.io = file_io_,
.projection = schema,
.first_row_id = 100L,
.properties = std::move(properties)});
ASSERT_THAT(reader_result, IsError(ErrorKind::kNotSupported));
EXPECT_THAT(reader_result,
HasErrorMessage("'_row_id' 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();
}
void WriteArrowArrayAndVerify(std::shared_ptr<Schema> schema,
const std::shared_ptr<::arrow::Array>& array) {
ArrowArray arrow_array;
ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
WriterProperties writer_properties;
writer_properties.Set(WriterProperties::kAvroSkipDatum, skip_datum_);
ICEBERG_UNWRAP_OR_FAIL(writer_, WriterFactoryRegistry::Open(
FileFormatType::kAvro,
{.path = temp_avro_file_,
.schema = schema,
.io = file_io_,
.properties = std::move(writer_properties)}));
ASSERT_THAT(writer_->Write(&arrow_array), IsOk());
ASSERT_THAT(writer_->Close(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto written_length, writer_->length());
ICEBERG_UNWRAP_OR_FAIL(
auto reader,
ReaderFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_,
.length = written_length,
.io = file_io_,
.projection = schema}));
ICEBERG_UNWRAP_OR_FAIL(auto data, reader->Next());
ASSERT_TRUE(data.has_value());
ICEBERG_UNWRAP_OR_FAIL(auto arrow_c_schema, reader->Schema());
auto read_array = ::arrow::ImportArray(&data.value(), &arrow_c_schema).ValueOrDie();
ASSERT_TRUE(read_array->Equals(*array)) << "actual:\n"
<< read_array->ToString() << "\nexpected:\n"
<< array->ToString();
ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader));
}
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, WriteDecimalFixedWidths) {
auto schema = std::make_shared<Schema>(
std::vector<SchemaField>{SchemaField::MakeRequired(1, "tiny", decimal(1, 0)),
SchemaField::MakeRequired(2, "narrow", decimal(3, 0)),
SchemaField::MakeRequired(3, "wide", decimal(38, 0)),
SchemaField::MakeRequired(4, "sentinel", int32()),
SchemaField::MakeOptional(5, "optional", decimal(10, 2))});
const std::string data = R"([
["0", "127", "99999999999999999999999999999999999999", 7, "10.01"],
["1", "128", "0", 8, null],
["-1", "-128", "-99999999999999999999999999999999999999", 9, "-20.02"],
["0", "-129", "1", 10, "0.00"]
])";
ASSERT_NO_FATAL_FAILURE(WriteAvroFile(schema, data));
ASSERT_NO_FATAL_FAILURE(VerifyWrittenData(data));
}
TEST_P(AvroWriterTest, WriteUuidType) {
auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "uuid_col", iceberg::uuid())});
auto uuid_array = MakeUuidArray({&kUuidBytes1, &kUuidBytes2});
auto array =
::arrow::StructArray::Make(
{uuid_array},
{::arrow::field("uuid_col", ::arrow::extension::uuid(), /*nullable=*/false)})
.ValueOrDie();
ArrowArray arrow_array;
ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
WriterProperties writer_properties;
writer_properties.Set(WriterProperties::kAvroSkipDatum, skip_datum_);
ICEBERG_UNWRAP_OR_FAIL(
writer_, WriterFactoryRegistry::Open(FileFormatType::kAvro,
{.path = temp_avro_file_,
.schema = schema,
.io = file_io_,
.properties = std::move(writer_properties)}));
ASSERT_THAT(writer_->Write(&arrow_array), IsOk());
ASSERT_THAT(writer_->Close(), IsOk());
auto avro_schema = PhysicalAvroSchema();
auto root = avro_schema.root();
ASSERT_EQ(root->type(), ::avro::AVRO_RECORD);
ASSERT_EQ(root->leaves(), 1);
auto uuid_node = root->leafAt(0);
EXPECT_EQ(uuid_node->type(), ::avro::AVRO_FIXED);
EXPECT_EQ(uuid_node->logicalType().type(), ::avro::LogicalType::UUID);
EXPECT_EQ(uuid_node->fixedSize(), Uuid::kLength);
ICEBERG_UNWRAP_OR_FAIL(auto written_length, writer_->length());
ICEBERG_UNWRAP_OR_FAIL(
auto reader,
ReaderFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_,
.length = written_length,
.io = file_io_,
.projection = schema}));
ICEBERG_UNWRAP_OR_FAIL(auto data, reader->Next());
ASSERT_TRUE(data.has_value());
ICEBERG_UNWRAP_OR_FAIL(auto arrow_c_schema, reader->Schema());
auto read_array = ::arrow::ImportArray(&data.value(), &arrow_c_schema).ValueOrDie();
ASSERT_TRUE(read_array->Equals(*array)) << "actual:\n"
<< read_array->ToString() << "\nexpected:\n"
<< array->ToString();
ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader));
}
TEST_P(AvroWriterTest, WriteGeospatialTypesAsOpaqueBytes) {
auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
SchemaField::MakeOptional(1, "geom", iceberg::geometry()),
SchemaField::MakeRequired(
2, "nested",
iceberg::struct_({SchemaField::MakeOptional(
3, "geog", iceberg::geography("EPSG:4326", EdgeAlgorithm::kVincenty))})),
});
::arrow::BinaryBuilder geometry_builder;
const std::array<uint8_t, 3> geometry_bytes = {0xff, 0x00, 0x42};
ASSERT_TRUE(geometry_builder.Append(geometry_bytes.data(), geometry_bytes.size()).ok());
ASSERT_TRUE(geometry_builder.AppendNull().ok());
::arrow::BinaryBuilder geography_builder;
const std::array<uint8_t, 4> geography_bytes = {0x01, 0x02, 0x03, 0x04};
ASSERT_TRUE(geography_builder.AppendNull().ok());
ASSERT_TRUE(
geography_builder.Append(geography_bytes.data(), geography_bytes.size()).ok());
auto nested = ::arrow::StructArray::Make({geography_builder.Finish().ValueOrDie()},
{::arrow::field("geog", ::arrow::binary())})
.ValueOrDie();
auto array = ::arrow::StructArray::Make(
{geometry_builder.Finish().ValueOrDie(), nested},
{::arrow::field("geom", ::arrow::binary()),
::arrow::field("nested", nested->type(), /*nullable=*/false)})
.ValueOrDie();
ASSERT_NO_FATAL_FAILURE(WriteArrowArrayAndVerify(schema, array));
}
TEST_P(AvroWriterTest, WriteUuidListType) {
auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "uuid_list",
std::make_shared<ListType>(SchemaField::MakeRequired(
2, ListType::kElementName, iceberg::uuid())))});
auto list_values = MakeUuidArray({&kUuidBytes1, &kUuidBytes2, &kUuidBytes1});
auto list_offsets = MakeInt32Array({0, 2, 3});
auto list_type = ::arrow::list(::arrow::field(std::string(ListType::kElementName),
::arrow::extension::uuid(),
/*nullable=*/false));
auto list_array =
::arrow::ListArray::FromArrays(list_type, *list_offsets, *list_values).ValueOrDie();
auto array =
::arrow::StructArray::Make(
{list_array}, {::arrow::field("uuid_list", list_type, /*nullable=*/false)})
.ValueOrDie();
ASSERT_NO_FATAL_FAILURE(WriteArrowArrayAndVerify(schema, array));
}
TEST_P(AvroWriterTest, WriteUuidMapType) {
auto schema = std::make_shared<iceberg::Schema>(
std::vector<SchemaField>{SchemaField::MakeRequired(
1, "uuid_map",
std::make_shared<MapType>(
SchemaField::MakeRequired(2, MapType::kKeyName, iceberg::string()),
SchemaField::MakeRequired(3, MapType::kValueName, iceberg::uuid())))});
auto map_offsets = MakeInt32Array({0, 2, 3});
auto map_keys = MakeStringArray({"first", "second", "only"});
auto map_items = MakeUuidArray({&kUuidBytes1, &kUuidBytes2, &kUuidBytes1});
auto map_type =
::arrow::map(::arrow::utf8(), ::arrow::field(std::string(MapType::kValueName),
::arrow::extension::uuid(),
/*nullable=*/false));
auto map_array =
::arrow::MapArray::FromArrays(map_type, map_offsets, map_keys, map_items)
.ValueOrDie();
auto array =
::arrow::StructArray::Make(
{map_array}, {::arrow::field("uuid_map", map_type, /*nullable=*/false)})
.ValueOrDie();
ASSERT_NO_FATAL_FAILURE(WriteArrowArrayAndVerify(schema, array));
}
TEST_P(AvroWriterTest, WriteUuidMapKeyType) {
auto schema = std::make_shared<iceberg::Schema>(
std::vector<SchemaField>{SchemaField::MakeRequired(
1, "uuid_key_map",
std::make_shared<MapType>(
SchemaField::MakeRequired(2, MapType::kKeyName, iceberg::uuid()),
SchemaField::MakeRequired(3, MapType::kValueName, iceberg::string())))});
auto map_offsets = MakeInt32Array({0, 2, 3});
auto map_keys = MakeUuidArray({&kUuidBytes1, &kUuidBytes2, &kUuidBytes1});
auto map_items = MakeStringArray({"first", "second", "only"});
auto map_type =
::arrow::map(::arrow::extension::uuid(),
::arrow::field(std::string(MapType::kValueName), ::arrow::utf8(),
/*nullable=*/false));
auto map_array =
::arrow::MapArray::FromArrays(map_type, map_offsets, map_keys, map_items)
.ValueOrDie();
auto array =
::arrow::StructArray::Make(
{map_array}, {::arrow::field("uuid_key_map", map_type, /*nullable=*/false)})
.ValueOrDie();
ASSERT_NO_FATAL_FAILURE(WriteArrowArrayAndVerify(schema, array));
}
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, WriteTimestampTzTypes) {
auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "timestamp_col", iceberg::timestamp()),
SchemaField::MakeRequired(2, "timestamptz_col", iceberg::timestamp_tz()),
SchemaField::MakeRequired(3, "timestamp_ns_col", iceberg::timestamp_ns()),
SchemaField::MakeRequired(4, "timestamptz_ns_col", iceberg::timestamptz_ns())});
std::string test_data = R"([
[1640995200000000, 1640995200000000, 1640995200000000001, 1640995200000000001],
[1641081599000000, 1641081599000000, 1641081599000000002, 1641081599000000002]
])";
WriteAvroFile(schema, test_data);
auto root = PhysicalAvroSchema().root();
ASSERT_EQ(root->type(), ::avro::AVRO_RECORD);
ASSERT_EQ(root->leaves(), 4);
const std::vector<std::pair<::avro::LogicalType::Type, std::string>> expected = {
{::avro::LogicalType::TIMESTAMP_MICROS, "false"},
{::avro::LogicalType::TIMESTAMP_MICROS, "true"},
{::avro::LogicalType::TIMESTAMP_NANOS, "false"},
{::avro::LogicalType::TIMESTAMP_NANOS, "true"},
};
for (size_t i = 0; i < expected.size(); ++i) {
auto node = root->leafAt(i);
EXPECT_EQ(node->type(), ::avro::AVRO_LONG);
EXPECT_EQ(node->logicalType().type(), expected[i].first);
ASSERT_EQ(node->customAttributes(), 1);
EXPECT_EQ(node->customAttributesAt(0).getAttribute(std::string(kAdjustToUtcProp)),
expected[i].second);
}
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