blob: 5dcfd2511d674a1af07a6e8035e9dc1470f39721 [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 <algorithm>
#include <cstring>
#include <arrow/array.h>
#include <arrow/extension_type.h>
#include <arrow/type.h>
#include <avro/Specific.hh>
#include "iceberg/avro/avro_direct_encoder_internal.h"
#include "iceberg/type.h"
#include "iceberg/util/checked_cast.h"
#include "iceberg/util/macros.h"
namespace iceberg::avro {
namespace {
// Utility struct for union branch information
struct UnionBranches {
size_t null_index;
size_t value_index;
::avro::NodePtr value_node;
};
Result<UnionBranches> ValidateUnion(const ::avro::NodePtr& union_node) {
ICEBERG_PRECHECK(union_node->leaves() == 2,
"Union must have exactly 2 branches, got {}", union_node->leaves());
const auto& branch_0 = union_node->leafAt(0);
const auto& branch_1 = union_node->leafAt(1);
if (branch_0->type() == ::avro::AVRO_NULL && branch_1->type() != ::avro::AVRO_NULL) {
return UnionBranches{.null_index = 0, .value_index = 1, .value_node = branch_1};
}
if (branch_1->type() == ::avro::AVRO_NULL && branch_0->type() != ::avro::AVRO_NULL) {
return UnionBranches{.null_index = 1, .value_index = 0, .value_node = branch_0};
}
return InvalidArgument("Union must have exactly one null branch");
}
} // namespace
Status EncodeArrowToAvro(const ::avro::NodePtr& avro_node, ::avro::Encoder& encoder,
const Type& type, const ::arrow::Array& array, int64_t row_index,
EncodeContext& ctx) {
ICEBERG_PRECHECK(row_index >= 0 && row_index < array.length(),
"Row index {} out of bounds {}", row_index, array.length());
const bool is_null = array.IsNull(row_index);
if (avro_node->type() == ::avro::AVRO_UNION) {
ICEBERG_ASSIGN_OR_RAISE(auto branches, ValidateUnion(avro_node));
if (is_null) {
encoder.encodeUnionIndex(branches.null_index);
encoder.encodeNull();
return {};
}
encoder.encodeUnionIndex(branches.value_index);
return EncodeArrowToAvro(branches.value_node, encoder, type, array, row_index, ctx);
}
if (avro_node->type() == ::avro::AVRO_NULL) {
encoder.encodeNull();
return {};
}
if (is_null) {
return InvalidArgument("Null value in non-nullable field");
}
switch (avro_node->type()) {
case ::avro::AVRO_BOOL: {
const auto& bool_array =
internal::checked_cast<const ::arrow::BooleanArray&>(array);
encoder.encodeBool(bool_array.Value(row_index));
return {};
}
case ::avro::AVRO_INT: {
// AVRO_INT can represent: int32, date (days since epoch)
switch (array.type()->id()) {
case ::arrow::Type::INT32: {
const auto& int32_array =
internal::checked_cast<const ::arrow::Int32Array&>(array);
encoder.encodeInt(int32_array.Value(row_index));
return {};
}
case ::arrow::Type::DATE32: {
const auto& date_array =
internal::checked_cast<const ::arrow::Date32Array&>(array);
encoder.encodeInt(date_array.Value(row_index));
return {};
}
default:
return InvalidArgument("AVRO_INT expects Int32Array or Date32Array, got {}",
array.type()->ToString());
}
}
case ::avro::AVRO_LONG: {
// AVRO_LONG can represent: int64, time (microseconds), timestamp (microseconds)
switch (array.type()->id()) {
case ::arrow::Type::INT64: {
const auto& int64_array =
internal::checked_cast<const ::arrow::Int64Array&>(array);
encoder.encodeLong(int64_array.Value(row_index));
return {};
}
case ::arrow::Type::TIME64: {
const auto& time_array =
internal::checked_cast<const ::arrow::Time64Array&>(array);
encoder.encodeLong(time_array.Value(row_index));
return {};
}
case ::arrow::Type::TIMESTAMP: {
const auto& timestamp_array =
internal::checked_cast<const ::arrow::TimestampArray&>(array);
encoder.encodeLong(timestamp_array.Value(row_index));
return {};
}
default:
return InvalidArgument(
"AVRO_LONG expects Int64Array, Time64Array, or TimestampArray, got {}",
array.type()->ToString());
}
}
case ::avro::AVRO_FLOAT: {
const auto& float_array = internal::checked_cast<const ::arrow::FloatArray&>(array);
encoder.encodeFloat(float_array.Value(row_index));
return {};
}
case ::avro::AVRO_DOUBLE: {
const auto& double_array =
internal::checked_cast<const ::arrow::DoubleArray&>(array);
encoder.encodeDouble(double_array.Value(row_index));
return {};
}
case ::avro::AVRO_STRING: {
const auto& string_array =
internal::checked_cast<const ::arrow::StringArray&>(array);
std::string_view value = string_array.GetView(row_index);
encoder.encodeString(std::string(value));
return {};
}
case ::avro::AVRO_BYTES: {
const auto& binary_array =
internal::checked_cast<const ::arrow::BinaryArray&>(array);
std::string_view value = binary_array.GetView(row_index);
ctx.bytes_scratch.assign(value.begin(), value.end());
encoder.encodeBytes(ctx.bytes_scratch);
return {};
}
case ::avro::AVRO_FIXED: {
// Handle UUID
if (avro_node->logicalType().type() == ::avro::LogicalType::UUID) {
const auto& extension_array =
internal::checked_cast<const ::arrow::ExtensionArray&>(array);
const auto& fixed_array =
internal::checked_cast<const ::arrow::FixedSizeBinaryArray&>(
*extension_array.storage());
std::string_view value = fixed_array.GetView(row_index);
ctx.bytes_scratch.assign(value.begin(), value.end());
encoder.encodeFixed(ctx.bytes_scratch.data(), ctx.bytes_scratch.size());
return {};
}
// Handle DECIMAL
if (avro_node->logicalType().type() == ::avro::LogicalType::DECIMAL) {
const auto& decimal_array =
internal::checked_cast<const ::arrow::Decimal128Array&>(array);
std::string_view decimal_value = decimal_array.GetView(row_index);
ctx.bytes_scratch.assign(decimal_value.begin(), decimal_value.end());
// Arrow Decimal128 bytes are in little-endian order, Avro requires big-endian
std::ranges::reverse(ctx.bytes_scratch);
encoder.encodeFixed(ctx.bytes_scratch.data(), ctx.bytes_scratch.size());
return {};
}
// Handle regular FIXED
const auto& fixed_array =
internal::checked_cast<const ::arrow::FixedSizeBinaryArray&>(array);
std::string_view value = fixed_array.GetView(row_index);
ctx.bytes_scratch.assign(value.begin(), value.end());
encoder.encodeFixed(ctx.bytes_scratch.data(), ctx.bytes_scratch.size());
return {};
}
case ::avro::AVRO_RECORD: {
ICEBERG_PRECHECK(array.type()->id() == ::arrow::Type::STRUCT,
"AVRO_RECORD expects StructArray, got {}",
array.type()->ToString());
ICEBERG_PRECHECK(type.type_id() == TypeId::kStruct,
"AVRO_RECORD expects struct type, got type {}", type.ToString());
const auto& struct_array =
internal::checked_cast<const ::arrow::StructArray&>(array);
const auto& struct_type = internal::checked_cast<const StructType&>(type);
const size_t num_fields = avro_node->leaves();
ICEBERG_PRECHECK(
struct_array.num_fields() == static_cast<int>(num_fields),
"Field count mismatch: Arrow struct has {} fields, Avro node has {} fields",
struct_array.num_fields(), num_fields);
ICEBERG_PRECHECK(
struct_type.fields().size() == num_fields,
"Field count mismatch: Iceberg struct has {} fields, Avro node has {} fields",
struct_type.fields().size(), num_fields);
for (size_t i = 0; i < num_fields; ++i) {
const auto& field_node = avro_node->leafAt(i);
const auto& field_array = struct_array.field(static_cast<int>(i));
const auto& field_schema = struct_type.fields()[i];
ICEBERG_RETURN_UNEXPECTED(EncodeArrowToAvro(
field_node, encoder, *field_schema.type(), *field_array, row_index, ctx));
}
return {};
}
case ::avro::AVRO_ARRAY: {
const auto& element_node = avro_node->leafAt(0);
// Handle ListArray
if (array.type()->id() == ::arrow::Type::LIST) {
const auto& list_array = internal::checked_cast<const ::arrow::ListArray&>(array);
const auto& list_type = internal::checked_cast<const ListType&>(type);
const auto start = list_array.value_offset(row_index);
const auto end = list_array.value_offset(row_index + 1);
const auto length = end - start;
encoder.arrayStart();
if (length > 0) {
encoder.setItemCount(length);
const auto& values = list_array.values();
const auto& element_type = *list_type.fields()[0].type();
for (int64_t i = start; i < end; ++i) {
encoder.startItem();
ICEBERG_RETURN_UNEXPECTED(
EncodeArrowToAvro(element_node, encoder, element_type, *values, i, ctx));
}
}
encoder.arrayEnd();
return {};
}
// Handle MapArray (for Avro maps with non-string keys)
if (array.type()->id() == ::arrow::Type::MAP) {
ICEBERG_PRECHECK(
element_node->type() == ::avro::AVRO_RECORD && element_node->leaves() == 2,
"Expected AVRO_RECORD for map key-value pair, got {}",
::avro::toString(element_node->type()));
const auto& map_array = internal::checked_cast<const ::arrow::MapArray&>(array);
const auto& map_type = internal::checked_cast<const MapType&>(type);
const auto start = map_array.value_offset(row_index);
const auto end = map_array.value_offset(row_index + 1);
const auto length = end - start;
encoder.arrayStart();
if (length > 0) {
encoder.setItemCount(length);
const auto& keys = map_array.keys();
const auto& values = map_array.items();
const auto& key_type = *map_type.key().type();
const auto& value_type = *map_type.value().type();
// The element_node should be a RECORD with "key" and "value" fields
for (int64_t i = start; i < end; ++i) {
const auto& key_node = element_node->leafAt(0);
const auto& value_node = element_node->leafAt(1);
encoder.startItem();
ICEBERG_RETURN_UNEXPECTED(
EncodeArrowToAvro(key_node, encoder, key_type, *keys, i, ctx));
ICEBERG_RETURN_UNEXPECTED(
EncodeArrowToAvro(value_node, encoder, value_type, *values, i, ctx));
}
}
encoder.arrayEnd();
return {};
}
return InvalidArgument("AVRO_ARRAY must map to ListArray or MapArray, got {}",
array.type()->ToString());
}
case ::avro::AVRO_MAP: {
ICEBERG_PRECHECK(array.type()->id() == ::arrow::Type::MAP,
"AVRO_MAP expects MapArray, got {}", array.type()->ToString());
ICEBERG_PRECHECK(type.type_id() == TypeId::kMap,
"AVRO_MAP expects MapType, got type {}", type.ToString());
const auto& map_array = internal::checked_cast<const ::arrow::MapArray&>(array);
const auto& map_type = internal::checked_cast<const MapType&>(type);
const auto start = map_array.value_offset(row_index);
const auto end = map_array.value_offset(row_index + 1);
const auto length = end - start;
encoder.mapStart();
if (length > 0) {
encoder.setItemCount(length);
const auto& keys = map_array.keys();
const auto& values = map_array.items();
const auto& value_type = *map_type.value().type();
const auto& value_node = avro_node->leafAt(1);
ICEBERG_PRECHECK(keys->type()->id() == ::arrow::Type::STRING ||
keys->type()->id() == ::arrow::Type::LARGE_STRING,
"AVRO_MAP keys must be StringArray, got {}",
keys->type()->ToString());
for (int64_t i = start; i < end; ++i) {
encoder.startItem();
if (keys->type()->id() == ::arrow::Type::STRING) {
const auto& string_array =
internal::checked_cast<const ::arrow::StringArray&>(*keys);
std::string_view key_value = string_array.GetView(i);
encoder.encodeString(std::string(key_value));
} else {
const auto& large_string_array =
internal::checked_cast<const ::arrow::LargeStringArray&>(*keys);
std::string_view key_value = large_string_array.GetView(i);
encoder.encodeString(std::string(key_value));
}
ICEBERG_RETURN_UNEXPECTED(
EncodeArrowToAvro(value_node, encoder, value_type, *values, i, ctx));
}
}
encoder.mapEnd();
return {};
}
case ::avro::AVRO_ENUM:
return NotSupported("ENUM type encoding not yet implemented");
case ::avro::AVRO_UNION:
// Already handled above
return Invalid("Unexpected union handling");
default:
return NotSupported("Unsupported Avro type: {}",
::avro::toString(avro_node->type()));
}
}
} // namespace iceberg::avro