blob: 34d8d7db018916d9c76f294ec9a171761226ab5c [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.
*/
// Adapted from Apache Iceberg C++
// https://github.com/apache/iceberg-cpp/blob/v0.2.0/src/iceberg/avro/avro_direct_decoder.cc
#include "paimon/format/avro/avro_direct_decoder.h"
#include "arrow/api.h"
#include "arrow/util/checked_cast.h"
#include "avro/Decoder.hh"
#include "avro/Node.hh"
#include "avro/Types.hh"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/format/avro/avro_utils.h"
namespace paimon::avro {
namespace {
/// Forward declaration for mutual recursion.
Status DecodeFieldToBuilder(const ::avro::NodePtr& avro_node,
const std::optional<std::set<size_t>>& projection,
::avro::Decoder* decoder, arrow::ArrayBuilder* array_builder,
AvroDirectDecoder::DecodeContext* ctx);
/// \brief Skip an Avro value based on its schema without decoding
Status SkipAvroValue(const ::avro::NodePtr& avro_node, ::avro::Decoder* decoder) {
switch (avro_node->type()) {
case ::avro::AVRO_NULL:
decoder->decodeNull();
return Status::OK();
case ::avro::AVRO_BOOL:
decoder->decodeBool();
return Status::OK();
case ::avro::AVRO_INT:
decoder->decodeInt();
return Status::OK();
case ::avro::AVRO_LONG:
decoder->decodeLong();
return Status::OK();
case ::avro::AVRO_FLOAT:
decoder->decodeFloat();
return Status::OK();
case ::avro::AVRO_DOUBLE:
decoder->decodeDouble();
return Status::OK();
case ::avro::AVRO_STRING:
decoder->skipString();
return Status::OK();
case ::avro::AVRO_BYTES:
decoder->skipBytes();
return Status::OK();
case ::avro::AVRO_RECORD: {
// Skip all fields in order
for (size_t i = 0; i < avro_node->leaves(); ++i) {
PAIMON_RETURN_NOT_OK(SkipAvroValue(avro_node->leafAt(i), decoder));
}
return Status::OK();
}
case ::avro::AVRO_ARRAY: {
const auto& element_node = avro_node->leafAt(0);
// skipArray() returns count like arrayStart(), must handle all blocks
int64_t block_count = decoder->skipArray();
while (block_count > 0) {
for (int64_t i = 0; i < block_count; ++i) {
PAIMON_RETURN_NOT_OK(SkipAvroValue(element_node, decoder));
}
block_count = decoder->arrayNext();
}
return Status::OK();
}
case ::avro::AVRO_MAP: {
const auto& value_node = avro_node->leafAt(1);
// skipMap() returns count like mapStart(), must handle all blocks
int64_t block_count = decoder->skipMap();
while (block_count > 0) {
for (int64_t i = 0; i < block_count; ++i) {
decoder->skipString(); // Skip key (always string in Avro maps)
PAIMON_RETURN_NOT_OK(SkipAvroValue(value_node, decoder));
}
block_count = decoder->mapNext();
}
return Status::OK();
}
case ::avro::AVRO_UNION: {
const size_t branch_index = decoder->decodeUnionIndex();
// Validate branch index
const size_t num_branches = avro_node->leaves();
if (branch_index >= num_branches) {
return Status::Invalid(fmt::format("Union branch index {} out of range [0, {})",
branch_index, num_branches));
}
return SkipAvroValue(avro_node->leafAt(branch_index), decoder);
}
default:
return Status::Invalid(fmt::format("Unsupported Avro type for skipping: {}",
AvroUtils::ToString(avro_node)));
}
}
/// Decode Avro record directly to Arrow struct builder.
Status DecodeStructToBuilder(const ::avro::NodePtr& avro_node,
const std::optional<std::set<size_t>>& projection,
::avro::Decoder* decoder, arrow::ArrayBuilder* array_builder,
AvroDirectDecoder::DecodeContext* ctx) {
if (avro_node->type() != ::avro::AVRO_RECORD) {
return Status::Invalid(
fmt::format("Expected Avro record, got type: {}", AvroUtils::ToString(avro_node)));
}
auto* struct_builder = arrow::internal::checked_cast<arrow::StructBuilder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(struct_builder->Append());
size_t skipped_fields = 0;
// Read all Avro fields in order (must maintain decoder position)
for (size_t avro_idx = 0; avro_idx < avro_node->leaves(); ++avro_idx) {
if (projection && projection->find(avro_idx) == projection->end()) {
skipped_fields++;
PAIMON_RETURN_NOT_OK(SkipAvroValue(avro_node->leafAt(avro_idx), decoder));
} else {
// Decode this field
const auto& avro_field_node = avro_node->leafAt(avro_idx);
auto* field_builder = struct_builder->field_builder(avro_idx - skipped_fields);
PAIMON_RETURN_NOT_OK(DecodeFieldToBuilder(avro_field_node, /*projection=*/std::nullopt,
decoder, field_builder, ctx));
}
}
return Status::OK();
}
/// Decode Avro array directly to Arrow list builder.
Status DecodeListToBuilder(const ::avro::NodePtr& avro_node, ::avro::Decoder* decoder,
arrow::ArrayBuilder* array_builder,
AvroDirectDecoder::DecodeContext* ctx) {
if (avro_node->type() != ::avro::AVRO_ARRAY) {
return Status::Invalid(
fmt::format("Expected Avro array, got type: {}", AvroUtils::ToString(avro_node)));
}
auto* list_builder = arrow::internal::checked_cast<arrow::ListBuilder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(list_builder->Append());
auto* value_builder = list_builder->value_builder();
const auto& element_node = avro_node->leafAt(0);
// Read array block count
int64_t block_count = decoder->arrayStart();
while (block_count != 0) {
for (int64_t i = 0; i < block_count; ++i) {
PAIMON_RETURN_NOT_OK(DecodeFieldToBuilder(element_node, /*projection=*/std::nullopt,
decoder, value_builder, ctx));
}
block_count = decoder->arrayNext();
}
return Status::OK();
}
/// Decode Avro map directly to Arrow map builder.
Status DecodeMapToBuilder(const ::avro::NodePtr& avro_node, ::avro::Decoder* decoder,
arrow::ArrayBuilder* array_builder,
AvroDirectDecoder::DecodeContext* ctx) {
auto* map_builder = arrow::internal::checked_cast<arrow::MapBuilder*>(array_builder);
if (avro_node->type() == ::avro::AVRO_MAP) {
// Handle regular Avro map: map<string, value>
const auto& key_node = avro_node->leafAt(0);
const auto& value_node = avro_node->leafAt(1);
PAIMON_RETURN_NOT_OK_FROM_ARROW(map_builder->Append());
auto* key_builder = map_builder->key_builder();
auto* item_builder = map_builder->item_builder();
// Read map block count
int64_t block_count = decoder->mapStart();
while (block_count != 0) {
for (int64_t i = 0; i < block_count; ++i) {
PAIMON_RETURN_NOT_OK(DecodeFieldToBuilder(key_node, /*projection=*/std::nullopt,
decoder, key_builder, ctx));
PAIMON_RETURN_NOT_OK(DecodeFieldToBuilder(value_node, /*projection=*/std::nullopt,
decoder, item_builder, ctx));
}
block_count = decoder->mapNext();
}
return Status::OK();
} else if (avro_node->type() == ::avro::AVRO_ARRAY && AvroUtils::HasMapLogicalType(avro_node)) {
// Handle array-based map: list<struct<key, value>>
PAIMON_RETURN_NOT_OK_FROM_ARROW(map_builder->Append());
auto* key_builder = map_builder->key_builder();
auto* item_builder = map_builder->item_builder();
const auto& record_node = avro_node->leafAt(0);
if (record_node->type() != ::avro::AVRO_RECORD || record_node->leaves() != 2) {
return Status::Invalid(
fmt::format("Array-based map must contain records with exactly 2 fields, got: {}",
AvroUtils::ToString(record_node)));
}
const auto& key_node = record_node->leafAt(0);
const auto& value_node = record_node->leafAt(1);
// Read array block count
int64_t block_count = decoder->arrayStart();
while (block_count != 0) {
for (int64_t i = 0; i < block_count; ++i) {
PAIMON_RETURN_NOT_OK(DecodeFieldToBuilder(key_node, /*projection=*/std::nullopt,
decoder, key_builder, ctx));
PAIMON_RETURN_NOT_OK(DecodeFieldToBuilder(value_node, /*projection=*/std::nullopt,
decoder, item_builder, ctx));
}
block_count = decoder->arrayNext();
}
return Status::OK();
} else {
return Status::Invalid(
fmt::format("Expected Avro map or array with map logical type, got: {}",
AvroUtils::ToString(avro_node)));
}
}
/// Decode Avro data directly to Arrow array builder.
Status DecodeAvroValueToBuilder(const ::avro::NodePtr& avro_node,
const std::optional<std::set<size_t>>& projection,
::avro::Decoder* decoder, arrow::ArrayBuilder* array_builder,
AvroDirectDecoder::DecodeContext* ctx) {
auto type = avro_node->type();
auto logical_type = avro_node->logicalType();
switch (type) {
case ::avro::AVRO_BOOL: {
auto* builder = arrow::internal::checked_cast<arrow::BooleanBuilder*>(array_builder);
bool value = decoder->decodeBool();
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
case ::avro::AVRO_INT: {
int32_t value = decoder->decodeInt();
auto arrow_type = array_builder->type();
switch (arrow_type->id()) {
case arrow::Type::INT8: {
auto* builder =
arrow::internal::checked_cast<arrow::Int8Builder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
case arrow::Type::INT16: {
auto* builder =
arrow::internal::checked_cast<arrow::Int16Builder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
case arrow::Type::INT32: {
auto* builder =
arrow::internal::checked_cast<arrow::Int32Builder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
case arrow::Type::DATE32: {
if (logical_type.type() != ::avro::LogicalType::Type::DATE) {
return Status::TypeError(
fmt::format("Unexpected avro type [{}] with arrow type [{}].", type,
arrow_type->ToString()));
}
auto* builder =
arrow::internal::checked_cast<arrow::Date32Builder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
default:
return Status::TypeError(
fmt::format("Unexpected avro type [{}] with arrow type [{}].", type,
arrow_type->ToString()));
}
}
case ::avro::AVRO_LONG: {
int64_t value = decoder->decodeLong();
switch (logical_type.type()) {
case ::avro::LogicalType::Type::NONE: {
auto* builder =
arrow::internal::checked_cast<arrow::Int64Builder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
case ::avro::LogicalType::Type::TIMESTAMP_MILLIS:
case ::avro::LogicalType::Type::TIMESTAMP_MICROS:
case ::avro::LogicalType::Type::TIMESTAMP_NANOS:
case ::avro::LogicalType::Type::LOCAL_TIMESTAMP_MILLIS:
case ::avro::LogicalType::Type::LOCAL_TIMESTAMP_MICROS:
case ::avro::LogicalType::Type::LOCAL_TIMESTAMP_NANOS: {
auto* builder =
arrow::internal::checked_cast<arrow::TimestampBuilder*>(array_builder);
auto ts_type =
arrow::internal::checked_cast<arrow::TimestampType*>(builder->type().get());
// for arrow second, we need to convert it from avro millisecond
if (ts_type->unit() == arrow::TimeUnit::type::SECOND) {
value /= DateTimeUtils::CONVERSION_FACTORS[DateTimeUtils::MILLISECOND];
}
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
default:
return Status::TypeError(
fmt::format("Unexpected avro type [{}] with arrow type [{}].", type,
array_builder->type()->ToString()));
}
}
case ::avro::AVRO_FLOAT: {
auto* builder = arrow::internal::checked_cast<arrow::FloatBuilder*>(array_builder);
float value = decoder->decodeFloat();
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
case ::avro::AVRO_DOUBLE: {
auto* builder = arrow::internal::checked_cast<arrow::DoubleBuilder*>(array_builder);
double value = decoder->decodeDouble();
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(value));
return Status::OK();
}
case ::avro::AVRO_STRING: {
auto* builder = arrow::internal::checked_cast<arrow::StringBuilder*>(array_builder);
decoder->decodeString(ctx->string_scratch);
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(ctx->string_scratch));
return Status::OK();
}
case ::avro::AVRO_BYTES: {
decoder->decodeBytes(ctx->bytes_scratch);
switch (logical_type.type()) {
case ::avro::LogicalType::Type::NONE: {
auto* builder =
arrow::internal::checked_cast<arrow::BinaryBuilder*>(array_builder);
PAIMON_RETURN_NOT_OK_FROM_ARROW(
builder->Append(ctx->bytes_scratch.data(),
static_cast<int32_t>(ctx->bytes_scratch.size())));
return Status::OK();
}
case ::avro::LogicalType::Type::DECIMAL: {
auto* builder =
arrow::internal::checked_cast<arrow::Decimal128Builder*>(array_builder);
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
arrow::Decimal128 decimal,
arrow::Decimal128::FromBigEndian(ctx->bytes_scratch.data(),
ctx->bytes_scratch.size()));
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append(decimal));
return Status::OK();
}
default:
return Status::TypeError(
fmt::format("Unexpected avro type [{}] with arrow type [{}].", type,
array_builder->type()->ToString()));
}
}
case ::avro::AVRO_RECORD: {
return DecodeStructToBuilder(avro_node, projection, decoder, array_builder, ctx);
}
case ::avro::AVRO_ARRAY: {
if (AvroUtils::HasMapLogicalType(avro_node)) {
return DecodeMapToBuilder(avro_node, decoder, array_builder, ctx);
} else {
return DecodeListToBuilder(avro_node, decoder, array_builder, ctx);
}
}
case ::avro::AVRO_MAP: {
return DecodeMapToBuilder(avro_node, decoder, array_builder, ctx);
}
default:
return Status::Invalid(fmt::format("Unsupported avro type: {}", type));
}
}
Status DecodeFieldToBuilder(const ::avro::NodePtr& avro_node,
const std::optional<std::set<size_t>>& projection,
::avro::Decoder* decoder, arrow::ArrayBuilder* array_builder,
AvroDirectDecoder::DecodeContext* ctx) {
if (avro_node->type() == ::avro::AVRO_UNION) {
const size_t branch_index = decoder->decodeUnionIndex();
// Validate branch index
const size_t num_branches = avro_node->leaves();
if (branch_index >= num_branches) {
return Status::Invalid(fmt::format("Union branch index {} out of range [0, {})",
branch_index, num_branches));
}
const auto& branch_node = avro_node->leafAt(branch_index);
if (branch_node->type() == ::avro::AVRO_NULL) {
decoder->decodeNull();
PAIMON_RETURN_NOT_OK_FROM_ARROW(array_builder->AppendNull());
return Status::OK();
} else {
return DecodeFieldToBuilder(branch_node, projection, decoder, array_builder, ctx);
}
}
return DecodeAvroValueToBuilder(avro_node, projection, decoder, array_builder, ctx);
}
} // namespace
Status AvroDirectDecoder::DecodeAvroToBuilder(const ::avro::NodePtr& avro_node,
const std::optional<std::set<size_t>>& projection,
::avro::Decoder* decoder,
arrow::ArrayBuilder* array_builder,
DecodeContext* ctx) {
return DecodeFieldToBuilder(avro_node, projection, decoder, array_builder, ctx);
}
} // namespace paimon::avro