This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 990580f feat(format): introduce avro direct encoding (#94)
990580f is described below
commit 990580f55a345ba5c3666ecd2147811899547735
Author: Zhang Jiawei <[email protected]>
AuthorDate: Mon Jun 22 17:33:11 2026 +0800
feat(format): introduce avro direct encoding (#94)
---
LICENSE | 15 +
NOTICE | 3 +
src/paimon/format/avro/avro_direct_decoder.cpp | 442 +++++++++++++
src/paimon/format/avro/avro_direct_decoder.h | 63 ++
src/paimon/format/avro/avro_direct_encoder.cpp | 376 +++++++++++
src/paimon/format/avro/avro_direct_encoder.h | 58 ++
.../avro/avro_direct_encoder_decoder_test.cpp | 687 +++++++++++++++++++++
7 files changed, 1644 insertions(+)
diff --git a/LICENSE b/LICENSE
index 175f5e7..2665c1e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -213,6 +213,21 @@ License: https://www.apache.org/licenses/LICENSE-2.0
--------------------------------------------------------------------------------
+This product includes code from Apache Iceberg C++.
+
+* Avro direct decoder/encoder:
+ * src/paimon/format/avro/avro_direct_decoder.cpp
+ * src/paimon/format/avro/avro_direct_decoder.h
+ * src/paimon/format/avro/avro_direct_encoder.cpp
+ * src/paimon/format/avro/avro_direct_encoder.h
+* Avro input stream in src/paimon/format/avro/avro_direct_decoder.cpp
+
+Copyright: 2024-2025 The Apache Software Foundation.
+Home page: https://iceberg.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
This product includes code from RocksDB.
* endian utility in src/paimon/common/utils/math.h
diff --git a/NOTICE b/NOTICE
index 3be42fb..b98bb34 100644
--- a/NOTICE
+++ b/NOTICE
@@ -10,6 +10,9 @@ Copyright 2014-present Alibaba Inc.
Apache Arrow
Copyright 2016-2024 The Apache Software Foundation
+Apache Iceberg C++
+Copyright 2024-2025 The Apache Software Foundation
+
Apache ORC
Copyright 2013 and onwards The Apache Software Foundation
diff --git a/src/paimon/format/avro/avro_direct_decoder.cpp
b/src/paimon/format/avro/avro_direct_decoder.cpp
new file mode 100644
index 0000000..b5bbd1d
--- /dev/null
+++ b/src/paimon/format/avro/avro_direct_decoder.cpp
@@ -0,0 +1,442 @@
+/*
+ * 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/main/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
diff --git a/src/paimon/format/avro/avro_direct_decoder.h
b/src/paimon/format/avro/avro_direct_decoder.h
new file mode 100644
index 0000000..a5bb7cd
--- /dev/null
+++ b/src/paimon/format/avro/avro_direct_decoder.h
@@ -0,0 +1,63 @@
+/*
+ * 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/main/src/iceberg/avro/avro_direct_decoder_internal.h
+
+#pragma once
+
+#include <set>
+
+#include "arrow/array/builder_base.h"
+#include "avro/Decoder.hh"
+#include "avro/Node.hh"
+#include "paimon/status.h"
+
+namespace paimon::avro {
+
+class AvroDirectDecoder {
+ public:
+ /// Context for reusing scratch buffers during Avro decoding
+ ///
+ /// Avoids frequent small allocations by reusing temporary buffers across
multiple decode
+ /// operations. This is particularly important for string, binary, and
decimal data types.
+ struct DecodeContext {
+ // Scratch buffer for string decoding (reused across rows)
+ std::string string_scratch;
+ // Scratch buffer for binary/decimal data (reused across rows)
+ std::vector<uint8_t> bytes_scratch;
+ };
+
+ /// Directly decode Avro data to Arrow array builders without GenericDatum
+ ///
+ /// Eliminates the GenericDatum intermediate layer by directly calling
Avro decoder
+ /// methods and immediately appending to Arrow builders.
+ ///
+ /// @param avro_node The Avro schema node for the data being decoded
+ /// @param decoder The Avro decoder positioned at the data to read
+ /// @param array_builder The Arrow array builder to append decoded data to
+ /// @param ctx Decode context for reusing scratch buffers
+ /// @return Status indicating success, or an error status
+ static Status DecodeAvroToBuilder(const ::avro::NodePtr& avro_node,
+ const std::optional<std::set<size_t>>&
projection,
+ ::avro::Decoder* decoder,
arrow::ArrayBuilder* array_builder,
+ DecodeContext* ctx);
+};
+
+} // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_direct_encoder.cpp
b/src/paimon/format/avro/avro_direct_encoder.cpp
new file mode 100644
index 0000000..d8ace9f
--- /dev/null
+++ b/src/paimon/format/avro/avro_direct_encoder.cpp
@@ -0,0 +1,376 @@
+/*
+ * 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/main/src/iceberg/avro/avro_direct_encoder.cc
+
+#include "paimon/format/avro/avro_direct_encoder.h"
+
+#include <algorithm>
+#include <cstring>
+
+#include "arrow/api.h"
+#include "arrow/type.h"
+#include "arrow/util/checked_cast.h"
+#include "fmt/format.h"
+#include "paimon/common/utils/date_time_utils.h"
+#include "paimon/format/avro/avro_utils.h"
+#include "paimon/result.h"
+
+namespace paimon::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) {
+ if (PAIMON_UNLIKELY(union_node->leaves() != 2)) {
+ return Status::Invalid(
+ fmt::format("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 Status::Invalid(
+ "Unexpected: In paimon, we expect the null branch to be the first
branch in a union.");
+ }
+ return Status::Invalid("Union must have exactly one null branch");
+}
+
+} // namespace
+
+Status AvroDirectEncoder::EncodeArrowToAvro(const ::avro::NodePtr& avro_node,
+ const arrow::Array& array, int64_t
row_index,
+ ::avro::Encoder* encoder,
EncodeContext* ctx) {
+ if (PAIMON_UNLIKELY(row_index < 0 || row_index >= array.length())) {
+ return Status::Invalid(
+ fmt::format("Row index {} out of bounds {}", row_index,
array.length()));
+ }
+
+ const bool is_null = array.IsNull(row_index);
+
+ if (avro_node->type() == ::avro::AVRO_UNION) {
+ PAIMON_ASSIGN_OR_RAISE(UnionBranches branches,
ValidateUnion(avro_node));
+
+ if (is_null) {
+ encoder->encodeUnionIndex(branches.null_index);
+ encoder->encodeNull();
+ return Status::OK();
+ }
+
+ encoder->encodeUnionIndex(branches.value_index);
+ return EncodeArrowToAvro(branches.value_node, array, row_index,
encoder, ctx);
+ }
+
+ if (is_null) {
+ return Status::Invalid("Null value in non-nullable field");
+ }
+
+ switch (avro_node->type()) {
+ case ::avro::AVRO_BOOL: {
+ const auto& bool_array =
+ arrow::internal::checked_cast<const
arrow::BooleanArray&>(array);
+ encoder->encodeBool(bool_array.Value(row_index));
+ return Status::OK();
+ }
+
+ case ::avro::AVRO_INT: {
+ // AVRO_INT can represent: int8, int16, int32, date (days since
epoch)
+ switch (array.type()->id()) {
+ case arrow::Type::INT8: {
+ const auto& int8_array =
+ arrow::internal::checked_cast<const
arrow::Int8Array&>(array);
+ encoder->encodeInt(int8_array.Value(row_index));
+ return Status::OK();
+ }
+ case arrow::Type::INT16: {
+ const auto& int16_array =
+ arrow::internal::checked_cast<const
arrow::Int16Array&>(array);
+ encoder->encodeInt(int16_array.Value(row_index));
+ return Status::OK();
+ }
+
+ case arrow::Type::INT32: {
+ const auto& int32_array =
+ arrow::internal::checked_cast<const
arrow::Int32Array&>(array);
+ encoder->encodeInt(int32_array.Value(row_index));
+ return Status::OK();
+ }
+ case arrow::Type::DATE32: {
+ const auto& date_array =
+ arrow::internal::checked_cast<const
arrow::Date32Array&>(array);
+ encoder->encodeInt(date_array.Value(row_index));
+ return Status::OK();
+ }
+ default:
+ return Status::Invalid(
+ fmt::format("AVRO_INT expects Int8Array or Int16Array
or Int32Array or "
+ "Date32Array, got {}",
+ array.type()->ToString()));
+ }
+ }
+
+ case ::avro::AVRO_LONG: {
+ // AVRO_LONG can represent: int64, timestamp
+ switch (array.type()->id()) {
+ case arrow::Type::INT64: {
+ const auto& int64_array =
+ arrow::internal::checked_cast<const
arrow::Int64Array&>(array);
+ encoder->encodeLong(int64_array.Value(row_index));
+ return Status::OK();
+ }
+ case arrow::Type::TIMESTAMP: {
+ const auto& timestamp_array =
+ arrow::internal::checked_cast<const
arrow::TimestampArray&>(array);
+ int64_t timestamp = timestamp_array.Value(row_index);
+
+ auto ts_type =
+
arrow::internal::checked_pointer_cast<arrow::TimestampType>(array.type());
+ arrow::TimeUnit::type unit = ts_type->unit();
+ const auto& logical_type = avro_node->logicalType().type();
+
+ // NOTE: Java Avro only support TIMESTAMP_MILLIS &&
TIMESTAMP_MICROS
+ if (((logical_type ==
::avro::LogicalType::TIMESTAMP_MILLIS ||
+ logical_type ==
::avro::LogicalType::LOCAL_TIMESTAMP_MILLIS) &&
+ unit == arrow::TimeUnit::MILLI) ||
+ ((logical_type ==
::avro::LogicalType::TIMESTAMP_MICROS ||
+ logical_type ==
::avro::LogicalType::LOCAL_TIMESTAMP_MICROS) &&
+ unit == arrow::TimeUnit::MICRO) ||
+ ((logical_type == ::avro::LogicalType::TIMESTAMP_NANOS
||
+ logical_type ==
::avro::LogicalType::LOCAL_TIMESTAMP_NANOS) &&
+ unit == arrow::TimeUnit::NANO)) {
+ encoder->encodeLong(timestamp);
+ } else if ((logical_type ==
::avro::LogicalType::TIMESTAMP_MILLIS ||
+ logical_type ==
::avro::LogicalType::LOCAL_TIMESTAMP_MILLIS) &&
+ unit == arrow::TimeUnit::SECOND) {
+ // for arrow second, we need to convert it to avro
millisecond
+ encoder->encodeLong(
+ timestamp *
+
DateTimeUtils::CONVERSION_FACTORS[DateTimeUtils::MILLISECOND]);
+ } else {
+ return Status::Invalid(
+ fmt::format("Unsupported timestamp type with avro
logical type {} and "
+ "arrow time unit {}.",
+
AvroUtils::ToString(avro_node->logicalType()),
+
DateTimeUtils::GetArrowTimeUnitStr(unit)));
+ }
+ return Status::OK();
+ }
+ default:
+ return Status::Invalid(
+ fmt::format("AVRO_LONG expects Int64Array, or
TimestampArray, got {}",
+ array.type()->ToString()));
+ }
+ }
+
+ case ::avro::AVRO_FLOAT: {
+ const auto& float_array =
+ arrow::internal::checked_cast<const arrow::FloatArray&>(array);
+ encoder->encodeFloat(float_array.Value(row_index));
+ return Status::OK();
+ }
+
+ case ::avro::AVRO_DOUBLE: {
+ const auto& double_array =
+ arrow::internal::checked_cast<const
arrow::DoubleArray&>(array);
+ encoder->encodeDouble(double_array.Value(row_index));
+ return Status::OK();
+ }
+
+ case ::avro::AVRO_STRING: {
+ const auto& string_array =
+ arrow::internal::checked_cast<const
arrow::StringArray&>(array);
+ std::string_view value = string_array.GetView(row_index);
+ encoder->encodeString(std::string(value));
+ return Status::OK();
+ }
+
+ case ::avro::AVRO_BYTES: {
+ // Handle DECIMAL
+ if (avro_node->logicalType().type() ==
::avro::LogicalType::DECIMAL) {
+ const auto& decimal_array =
+ arrow::internal::checked_cast<const
arrow::Decimal128Array&>(array);
+ std::string_view decimal_value =
decimal_array.GetView(row_index);
+ ctx->assign(decimal_value.begin(), decimal_value.end());
+ // Arrow Decimal128 bytes are in little-endian order, Avro
requires big-endian
+ std::reverse(ctx->begin(), ctx->end());
+ encoder->encodeBytes(ctx->data(), ctx->size());
+ return Status::OK();
+ }
+
+ // Handle regular BYTES
+ const auto& binary_array =
+ arrow::internal::checked_cast<const
arrow::BinaryArray&>(array);
+ std::string_view value = binary_array.GetView(row_index);
+ encoder->encodeBytes(reinterpret_cast<const
uint8_t*>(value.data()), value.size());
+ return Status::OK();
+ }
+
+ case ::avro::AVRO_RECORD: {
+ if (PAIMON_UNLIKELY(array.type()->id() != arrow::Type::STRUCT)) {
+ return Status::Invalid(fmt::format("AVRO_RECORD expects
StructArray, got {}",
+ array.type()->ToString()));
+ }
+
+ const auto& struct_array =
+ arrow::internal::checked_cast<const
arrow::StructArray&>(array);
+ const size_t num_fields = avro_node->leaves();
+
+ if (PAIMON_UNLIKELY(struct_array.num_fields() !=
static_cast<int>(num_fields))) {
+ return Status::Invalid(fmt::format(
+ "Field count mismatch: Arrow struct has {} fields, Avro
node has {} fields",
+ struct_array.num_fields(), 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));
+
+ PAIMON_RETURN_NOT_OK(
+ EncodeArrowToAvro(field_node, *field_array, row_index,
encoder, ctx));
+ }
+ return Status::OK();
+ }
+
+ 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 =
+ arrow::internal::checked_cast<const
arrow::ListArray&>(array);
+
+ 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();
+
+ for (int64_t i = start; i < end; ++i) {
+ encoder->startItem();
+ PAIMON_RETURN_NOT_OK(
+ EncodeArrowToAvro(element_node, *values, i,
encoder, ctx));
+ }
+ }
+ encoder->arrayEnd();
+ return Status::OK();
+ } else if (array.type()->id() == arrow::Type::MAP &&
+ AvroUtils::HasMapLogicalType(avro_node)) {
+ // Handle MapArray (for Avro maps with non-string keys)
+ if (PAIMON_UNLIKELY(element_node->type() !=
::avro::AVRO_RECORD ||
+ element_node->leaves() != 2)) {
+ return Status::Invalid(
+ fmt::format("Expected AVRO_RECORD for map key-value
pair, got {}",
+ AvroUtils::ToString(element_node)));
+ }
+
+ const auto& map_array =
+ arrow::internal::checked_cast<const
arrow::MapArray&>(array);
+
+ 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();
+
+ // 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();
+ PAIMON_RETURN_NOT_OK(EncodeArrowToAvro(key_node,
*keys, i, encoder, ctx));
+ PAIMON_RETURN_NOT_OK(
+ EncodeArrowToAvro(value_node, *values, i, encoder,
ctx));
+ }
+ }
+ encoder->arrayEnd();
+ return Status::OK();
+ }
+
+ return Status::Invalid(fmt::format(
+ "AVRO_ARRAY must map to ListArray or MapArray, got {}",
array.type()->ToString()));
+ }
+
+ case ::avro::AVRO_MAP: {
+ if (PAIMON_UNLIKELY(array.type()->id() != arrow::Type::MAP)) {
+ return Status::Invalid(
+ fmt::format("AVRO_MAP expects MapArray, got {}",
array.type()->ToString()));
+ }
+ const auto& map_array = arrow::internal::checked_cast<const
arrow::MapArray&>(array);
+
+ 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_node = avro_node->leafAt(1);
+
+ if (PAIMON_UNLIKELY(keys->type()->id() !=
arrow::Type::STRING)) {
+ return Status::Invalid(fmt::format("AVRO_MAP keys must be
StringArray, got {}",
+
keys->type()->ToString()));
+ }
+
+ for (int64_t i = start; i < end; ++i) {
+ encoder->startItem();
+ const auto& string_array =
+ arrow::internal::checked_cast<const
arrow::StringArray&>(*keys);
+ std::string_view key_value = string_array.GetView(i);
+ encoder->encodeString(std::string(key_value));
+
+ PAIMON_RETURN_NOT_OK(EncodeArrowToAvro(value_node,
*values, i, encoder, ctx));
+ }
+ }
+ encoder->mapEnd();
+ return Status::OK();
+ }
+
+ case ::avro::AVRO_NULL:
+ case ::avro::AVRO_UNION:
+ // Already handled above
+ return Status::Invalid(fmt::format("Unexpected Avro type handling:
{}",
+
::avro::toString(avro_node->type())));
+ default:
+ return Status::Invalid(
+ fmt::format("Unsupported Avro type: {}",
::avro::toString(avro_node->type())));
+ }
+}
+
+} // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_direct_encoder.h
b/src/paimon/format/avro/avro_direct_encoder.h
new file mode 100644
index 0000000..6d0f995
--- /dev/null
+++ b/src/paimon/format/avro/avro_direct_encoder.h
@@ -0,0 +1,58 @@
+/*
+ * 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/main/src/iceberg/avro/avro_direct_encoder_internal.h
+
+#pragma once
+
+#include <vector>
+
+#include "arrow/api.h"
+#include "avro/Encoder.hh"
+#include "avro/Node.hh"
+#include "paimon/status.h"
+
+namespace paimon::avro {
+
+class AvroDirectEncoder {
+ public:
+ /// Context for reusing scratch buffers during Avro encoding
+ ///
+ /// Avoids frequent small allocations by reusing temporary buffers across
multiple encode
+ /// operations. This is particularly important for binary/decimal types
(reused across rows).
+ using EncodeContext = std::vector<uint8_t>;
+
+ /// Directly encode Arrow data to Avro without GenericDatum
+ ///
+ /// Eliminates the GenericDatum intermediate layer by directly calling
Avro encoder
+ /// methods from Arrow arrays.
+ ///
+ /// @param avro_node The Avro schema node for the data being encoded
+ /// @param array The Arrow array containing the data to encode
+ /// @param row_index The index of the row to encode within the array
+ /// @param encoder The Avro encoder to write data to
+ /// @param ctx Encode context for reusing scratch buffers
+ /// @return Status indicating success, or an error status
+ static Status EncodeArrowToAvro(const ::avro::NodePtr& avro_node, const
arrow::Array& array,
+ int64_t row_index, ::avro::Encoder*
encoder,
+ EncodeContext* ctx);
+};
+
+} // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_direct_encoder_decoder_test.cpp
b/src/paimon/format/avro/avro_direct_encoder_decoder_test.cpp
new file mode 100644
index 0000000..bb5bd3c
--- /dev/null
+++ b/src/paimon/format/avro/avro_direct_encoder_decoder_test.cpp
@@ -0,0 +1,687 @@
+/*
+ * 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 <memory>
+#include <string>
+
+#include "arrow/api.h"
+#include "arrow/ipc/api.h"
+#include "avro/Compiler.hh"
+#include "avro/Decoder.hh"
+#include "avro/Encoder.hh"
+#include "avro/Stream.hh"
+#include "avro/ValidSchema.hh"
+#include "gtest/gtest.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/format/avro/avro_direct_decoder.h"
+#include "paimon/format/avro/avro_direct_encoder.h"
+#include "paimon/format/avro/avro_schema_converter.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::avro::test {
+
+class AvroDirectEncoderDecoderTest : public ::testing::Test {
+ public:
+ void SetUp() override {}
+ void TearDown() override {}
+
+ Result<std::unique_ptr<::avro::OutputStream>> EncodeData(
+ const ::avro::NodePtr& avro_node, const std::shared_ptr<arrow::Array>&
input_array) {
+ auto output_stream = ::avro::memoryOutputStream();
+ auto encoder = ::avro::binaryEncoder();
+ encoder->init(*output_stream);
+
+ for (int64_t i = 0; i < input_array->length(); ++i) {
+
PAIMON_RETURN_NOT_OK(AvroDirectEncoder::EncodeArrowToAvro(avro_node,
*input_array, i,
+
encoder.get(), &encode_ctx_));
+ }
+ return output_stream;
+ }
+
+ Result<std::shared_ptr<arrow::Array>> DecodeWithEncodedData(
+ const ::avro::NodePtr& avro_node,
std::unique_ptr<::avro::OutputStream>&& encoded_data,
+ const std::optional<std::set<size_t>>& projection, int32_t
expected_count,
+ arrow::ArrayBuilder* builder) {
+ auto input_stream = ::avro::memoryInputStream(*encoded_data);
+ auto decoder = ::avro::binaryDecoder();
+ decoder->init(*input_stream);
+
+ for (int32_t i = 0; i < expected_count; ++i) {
+ PAIMON_RETURN_NOT_OK(AvroDirectDecoder::DecodeAvroToBuilder(
+ avro_node, projection, decoder.get(), builder, &decode_ctx_));
+ }
+
+ std::shared_ptr<arrow::Array> decoded_array;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Finish(&decoded_array));
+ EXPECT_EQ(decoded_array->length(), expected_count);
+ return decoded_array;
+ }
+
+ void CheckResult(const std::string& schema_json,
+ const std::shared_ptr<arrow::Array>& input_array,
+ arrow::ArrayBuilder* builder) {
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+
+ ASSERT_OK_AND_ASSIGN(auto encoded_data, EncodeData(avro_schema.root(),
input_array));
+ ASSERT_OK_AND_ASSIGN(
+ auto decoded_array,
+ DecodeWithEncodedData(avro_schema.root(), std::move(encoded_data),
+ /*projection=*/std::nullopt,
input_array->length(), builder));
+ ASSERT_TRUE(decoded_array->Equals(*input_array));
+ }
+
+ Result<std::shared_ptr<arrow::Array>> GetProjectedArray(
+ const std::shared_ptr<arrow::StructArray>& input_array,
+ const std::set<size_t>& projection) {
+ auto struct_type = input_array->struct_type();
+ arrow::FieldVector projected_fields;
+ projected_fields.reserve(projection.size());
+ arrow::ArrayVector projected_field_arrays;
+ projected_field_arrays.reserve(projection.size());
+ for (size_t index : projection) {
+ if (index >= static_cast<size_t>(struct_type->num_fields())) {
+ return Status::Invalid(
+ fmt::format("Projection index {} out of range for struct
with {} fields", index,
+ struct_type->num_fields()));
+ }
+ projected_fields.push_back(struct_type->field(index));
+ projected_field_arrays.push_back(input_array->field(index));
+ }
+ auto projected_struct_type = arrow::struct_(projected_fields);
+
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ auto projected_array,
+ arrow::StructArray::Make(projected_field_arrays,
projected_fields));
+ return projected_array;
+ }
+
+ void CheckResultWithProjection(const std::shared_ptr<arrow::Array>&
src_array,
+ const std::set<size_t>& projection) {
+ auto src_struct_array =
std::dynamic_pointer_cast<arrow::StructArray>(src_array);
+ ASSERT_OK_AND_ASSIGN(auto avro_schema,
+ AvroSchemaConverter::ArrowSchemaToAvroSchema(
+
arrow::schema(src_struct_array->struct_type()->fields())));
+ ASSERT_OK_AND_ASSIGN(auto encoded_data, EncodeData(avro_schema.root(),
src_array));
+
+ ASSERT_OK_AND_ASSIGN(auto projected_array,
GetProjectedArray(src_struct_array, projection));
+ auto decoded_array_builder =
arrow::MakeBuilder(projected_array->type()).ValueOrDie();
+ ASSERT_OK_AND_ASSIGN(
+ auto decoded_array,
+ DecodeWithEncodedData(avro_schema.root(), std::move(encoded_data),
projection,
+ src_array->length(),
decoded_array_builder.get()));
+ ASSERT_TRUE(decoded_array->Equals(*projected_array));
+ }
+
+ protected:
+ AvroDirectEncoder::EncodeContext encode_ctx_;
+ AvroDirectDecoder::DecodeContext decode_ctx_;
+};
+
+TEST_F(AvroDirectEncoderDecoderTest, TestBooleanType) {
+ std::string schema_json = R"({"type": "boolean"})";
+ arrow::BooleanBuilder builder;
+ ASSERT_TRUE(builder.Append(true).ok());
+ ASSERT_TRUE(builder.Append(false).ok());
+ ASSERT_TRUE(builder.Append(true).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestIntegerTypes) {
+ // Test INT8
+ {
+ std::string schema_json = R"({"type": "int"})";
+ arrow::Int8Builder builder;
+ ASSERT_TRUE(builder.Append(1).ok());
+ ASSERT_TRUE(builder.Append(-128).ok());
+ ASSERT_TRUE(builder.Append(127).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+ }
+
+ // Test INT32
+ {
+ std::string schema_json = R"({"type": "int"})";
+ arrow::Int32Builder builder;
+ ASSERT_TRUE(builder.Append(42).ok());
+ ASSERT_TRUE(builder.Append(-2147483648).ok());
+ ASSERT_TRUE(builder.Append(2147483647).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+ }
+
+ // Test INT64
+ {
+ std::string schema_json = R"({"type": "long"})";
+ arrow::Int64Builder builder;
+ ASSERT_TRUE(builder.Append(123456789L).ok());
+ ASSERT_TRUE(builder.Append(-9223372036854775807L).ok());
+ ASSERT_TRUE(builder.Append(9223372036854775807L).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+ }
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestFloatingPointTypes) {
+ // Test FLOAT
+ {
+ std::string schema_json = R"({"type": "float"})";
+ arrow::FloatBuilder builder;
+ ASSERT_TRUE(builder.Append(3.14f).ok());
+ ASSERT_TRUE(builder.Append(-2.71f).ok());
+ ASSERT_TRUE(builder.Append(0.0f).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+ }
+
+ // Test DOUBLE
+ {
+ std::string schema_json = R"({"type": "double"})";
+ arrow::DoubleBuilder builder;
+ ASSERT_TRUE(builder.Append(3.141592653589793).ok());
+ ASSERT_TRUE(builder.Append(-2.718281828459045).ok());
+ ASSERT_TRUE(builder.Append(0.0).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+ }
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestStringType) {
+ std::string schema_json = R"({"type": "string"})";
+ arrow::StringBuilder builder;
+ ASSERT_TRUE(builder.Append("hello").ok());
+ ASSERT_TRUE(builder.Append("world").ok());
+ ASSERT_TRUE(builder.Append("").ok());
+ ASSERT_TRUE(builder.Append("测试中文").ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestBytesType) {
+ std::string schema_json = R"({"type": "bytes"})";
+ arrow::BinaryBuilder builder;
+ ASSERT_TRUE(builder.Append("binary_data").ok());
+ ASSERT_TRUE(builder.Append(std::string("\x00\x01\x02\x03", 4)).ok());
+ ASSERT_TRUE(builder.Append("").ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestDate32Type) {
+ std::string schema_json = R"({"type": "int", "logicalType": "date"})";
+ arrow::Date32Builder builder;
+ ASSERT_TRUE(builder.Append(18628).ok()); // 2021-01-01
+ ASSERT_TRUE(builder.Append(0).ok()); // 1970-01-01
+ ASSERT_TRUE(builder.Append(-1).ok()); // 1969-12-31
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestTimestampType) {
+ // Test timestamp-millis
+ {
+ std::string schema_json = R"({"type": "long", "logicalType":
"timestamp-millis"})";
+ arrow::TimestampBuilder
builder(arrow::timestamp(arrow::TimeUnit::MILLI),
+ arrow::default_memory_pool());
+ ASSERT_TRUE(builder.Append(1609459200123L).ok()); // 2021-01-01
00:00:00.123
+ ASSERT_TRUE(builder.Append(0L).ok()); // 1970-01-01
00:00:00
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+ }
+
+ // Test timestamp-micros
+ {
+ std::string schema_json = R"({"type": "long", "logicalType":
"timestamp-micros"})";
+ arrow::TimestampBuilder
builder(arrow::timestamp(arrow::TimeUnit::MICRO),
+ arrow::default_memory_pool());
+ ASSERT_TRUE(builder.Append(1609459200123123L).ok()); // 2021-01-01
00:00:00.123123
+ ASSERT_TRUE(builder.Append(0L).ok()); // 1970-01-01
00:00:00
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+ }
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestInvalidTimestampType) {
+ std::string schema_json = R"({"type": "long", "logicalType":
"timestamp-millis"})";
+ arrow::TimestampBuilder builder(arrow::timestamp(arrow::TimeUnit::NANO),
+ arrow::default_memory_pool());
+ ASSERT_TRUE(builder.Append(1609459200123L).ok()); // 2021-01-01
00:00:00.123
+ ASSERT_TRUE(builder.Append(0L).ok()); // 1970-01-01 00:00:00
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+ ASSERT_NOK_WITH_MSG(EncodeData(avro_schema.root(), input_array),
+ "Unsupported timestamp type with avro logical type
\"logicalType\": "
+ "\"timestamp-millis\" and arrow time unit
NANOSECOND.");
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestUnionType) {
+ // Test nullable int (union of null and int)
+ std::string schema_json = R"(["null", "int"])";
+ arrow::Int32Builder builder;
+ ASSERT_TRUE(builder.Append(42).ok());
+ ASSERT_TRUE(builder.AppendNull().ok());
+ ASSERT_TRUE(builder.Append(100).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestRecordType) {
+ std::string schema_json = R"({
+ "type": "record",
+ "name": "TestRecord",
+ "fields": [
+ {"name": "id", "type": "int"},
+ {"name": "name", "type": "string"},
+ {"name": "active", "type": "boolean"}
+ ]
+ })";
+
+ // Create struct array
+ auto int_field = arrow::field("id", arrow::int32());
+ auto string_field = arrow::field("name", arrow::utf8());
+ auto bool_field = arrow::field("active", arrow::boolean());
+ auto struct_type = arrow::struct_({int_field, string_field, bool_field});
+
+ arrow::StructBuilder struct_builder(
+ struct_type, arrow::default_memory_pool(),
+ {std::make_shared<arrow::Int32Builder>(),
std::make_shared<arrow::StringBuilder>(),
+ std::make_shared<arrow::BooleanBuilder>()});
+
+ auto int_builder =
static_cast<arrow::Int32Builder*>(struct_builder.field_builder(0));
+ auto string_builder =
static_cast<arrow::StringBuilder*>(struct_builder.field_builder(1));
+ auto bool_builder =
static_cast<arrow::BooleanBuilder*>(struct_builder.field_builder(2));
+
+ // Add first record
+ ASSERT_TRUE(struct_builder.Append().ok());
+ ASSERT_TRUE(int_builder->Append(1).ok());
+ ASSERT_TRUE(string_builder->Append("Alice").ok());
+ ASSERT_TRUE(bool_builder->Append(true).ok());
+
+ // Add second record
+ ASSERT_TRUE(struct_builder.Append().ok());
+ ASSERT_TRUE(int_builder->Append(2).ok());
+ ASSERT_TRUE(string_builder->Append("Bob").ok());
+ ASSERT_TRUE(bool_builder->Append(false).ok());
+
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(struct_builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &struct_builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestDecodeWithProjection) {
+ arrow::FieldVector fields = {
+ arrow::field("f0", arrow::boolean()),
+ arrow::field("f1", arrow::int8()),
+ arrow::field("f2", arrow::int16()),
+ arrow::field("f3", arrow::int32()),
+ arrow::field("f4", arrow::int64()),
+ arrow::field("f5", arrow::float32()),
+ arrow::field("f6", arrow::float64()),
+ arrow::field("f7", arrow::utf8()),
+ arrow::field("f8", arrow::binary()),
+ arrow::field("f9", arrow::map(arrow::float64(), arrow::float64())),
+ arrow::field("f10", arrow::map(arrow::utf8(), arrow::utf8())),
+ arrow::field("f11", arrow::list(arrow::float32())),
+ arrow::field("f12", arrow::struct_({arrow::field("f0",
arrow::boolean()),
+ arrow::field("f1",
arrow::int64())})),
+ arrow::field("f13", arrow::timestamp(arrow::TimeUnit::MICRO)),
+ arrow::field("f14", arrow::date32()),
+ arrow::field("f15", arrow::decimal128(2, 2)),
+ arrow::field("f16", arrow::decimal128(10, 10)),
+ arrow::field("f17", arrow::decimal128(19, 19))};
+
+ std::shared_ptr<arrow::Array> src_array =
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
+ [true, 127, 32767, 2147483647, 9999999999999, 1234.56,
1234567890.0987654321, "aa", "qq", [[1.1,10.1],[2.2,20.2]],
[["key1","val1"],["key2","val2"]], [0.1, 0.2], [true, null], "1970-01-01
00:02:03.123123", 2456, "0.22", "0.1234567890", "0.1234567890987654321"],
+ [false, -128, -32768, -2147483648, -9999999999999, -1234.56,
-1234567890.0987654321, null, "ww", [[1.11,10.11],[2.22,20.22]],
[["key11","val11"],["key22","val22"]], [-0.1, -0.2, null, 0.3, 0.4], [null, 2],
"1970-01-01 00:16:39.999999", null, "-0.22", "-0.1234567890", null],
+ [null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null]
+ ])")
+ .ValueOrDie();
+
+ // no skip
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17});
+ // skip bool
+ CheckResultWithProjection(src_array,
+ {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17});
+ // skip int
+ CheckResultWithProjection(src_array, {0, 1, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17});
+ // skip long
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17});
+ // skip float
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17});
+ // skip double
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17});
+ // skip string
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17});
+ // skip binary
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14,
15, 16, 17});
+ // skip map
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14,
15, 16, 17});
+ // skip array-based map
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14,
15, 16, 17});
+ // skip list
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14,
15, 16, 17});
+ // skip struct
+ CheckResultWithProjection(src_array,
+ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14,
15, 16, 17});
+ // skip others
+ CheckResultWithProjection(src_array, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12});
+ // skip null and union is already tested in above test cases
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestArrayType) {
+ std::string schema_json = R"({
+ "type": "array",
+ "items": "int"
+ })";
+
+ // Create list array
+ arrow::ListBuilder list_builder(arrow::default_memory_pool(),
+ std::make_shared<arrow::Int32Builder>());
+ auto int_builder =
static_cast<arrow::Int32Builder*>(list_builder.value_builder());
+
+ // First list: [1, 2, 3]
+ ASSERT_TRUE(list_builder.Append().ok());
+ ASSERT_TRUE(int_builder->Append(1).ok());
+ ASSERT_TRUE(int_builder->Append(2).ok());
+ ASSERT_TRUE(int_builder->Append(3).ok());
+
+ // Second list: []
+ ASSERT_TRUE(list_builder.Append().ok());
+
+ // Third list: [42]
+ ASSERT_TRUE(list_builder.Append().ok());
+ ASSERT_TRUE(int_builder->Append(42).ok());
+
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(list_builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &list_builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestMapType) {
+ std::string schema_json = R"({
+ "type": "map",
+ "values": "string"
+ })";
+
+ // Create map array
+ arrow::MapBuilder map_builder(arrow::default_memory_pool(),
+ std::make_shared<arrow::StringBuilder>(),
+ std::make_shared<arrow::StringBuilder>());
+ auto key_builder =
static_cast<arrow::StringBuilder*>(map_builder.key_builder());
+ auto value_builder =
static_cast<arrow::StringBuilder*>(map_builder.item_builder());
+
+ // First map: {"key1": "value1", "key2": "value2"}
+ ASSERT_TRUE(map_builder.Append().ok());
+ ASSERT_TRUE(key_builder->Append("key1").ok());
+ ASSERT_TRUE(value_builder->Append("value1").ok());
+ ASSERT_TRUE(key_builder->Append("key2").ok());
+ ASSERT_TRUE(value_builder->Append("value2").ok());
+
+ // Second map: {}
+ ASSERT_TRUE(map_builder.Append().ok());
+
+ // Third map: {"single": "entry"}
+ ASSERT_TRUE(map_builder.Append().ok());
+ ASSERT_TRUE(key_builder->Append("single").ok());
+ ASSERT_TRUE(value_builder->Append("entry").ok());
+
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(map_builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &map_builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestArrayBasedMapType) {
+ std::string schema_json = R"({
+ "type" : "array",
+ "items" : {
+ "type" : "record",
+ "name" : "record_f1",
+ "fields" : [ {
+ "name" : "key",
+ "type" : "int"
+ }, {
+ "name" : "value",
+ "type" : "string"
+ } ]
+ },
+ "logicalType" : "map"
+ })";
+
+ // Create map array
+ arrow::MapBuilder map_builder(arrow::default_memory_pool(),
+ std::make_shared<arrow::Int32Builder>(),
+ std::make_shared<arrow::StringBuilder>());
+ auto key_builder =
static_cast<arrow::Int32Builder*>(map_builder.key_builder());
+ auto value_builder =
static_cast<arrow::StringBuilder*>(map_builder.item_builder());
+
+ // First map: {111: "value1", 222: "value2"}
+ ASSERT_TRUE(map_builder.Append().ok());
+ ASSERT_TRUE(key_builder->Append(111).ok());
+ ASSERT_TRUE(value_builder->Append("value1").ok());
+ ASSERT_TRUE(key_builder->Append(222).ok());
+ ASSERT_TRUE(value_builder->Append("value2").ok());
+
+ // Second map: {}
+ ASSERT_TRUE(map_builder.Append().ok());
+
+ // Third map: {333: "entry"}
+ ASSERT_TRUE(map_builder.Append().ok());
+ ASSERT_TRUE(key_builder->Append(333).ok());
+ ASSERT_TRUE(value_builder->Append("entry").ok());
+
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(map_builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &map_builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestDecimalType) {
+ std::string schema_json = R"({
+ "type": "bytes",
+ "logicalType": "decimal",
+ "precision": 10,
+ "scale": 2
+ })";
+
+ // Create decimal array
+ auto decimal_type = arrow::decimal128(10, 2);
+ arrow::Decimal128Builder builder(decimal_type);
+
+ ASSERT_TRUE(builder.Append(arrow::Decimal128("123.45")).ok());
+ ASSERT_TRUE(builder.Append(arrow::Decimal128("-67.89")).ok());
+ ASSERT_TRUE(builder.Append(arrow::Decimal128("0.00")).ok());
+
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+ CheckResult(schema_json, input_array, &builder);
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestEncoderErrorCases) {
+ std::string schema_json = R"({"type": "int"})";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+ auto output_stream = ::avro::memoryOutputStream();
+ auto encoder = ::avro::binaryEncoder();
+ encoder->init(*output_stream);
+
+ {
+ // Test out of bounds row index
+ arrow::Int32Builder builder;
+ ASSERT_TRUE(builder.Append(42).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+
+
ASSERT_NOK_WITH_MSG(AvroDirectEncoder::EncodeArrowToAvro(avro_schema.root(),
*input_array,
+ -1,
encoder.get(), &encode_ctx_),
+ "Row index -1 out of bounds 1");
+
ASSERT_NOK_WITH_MSG(AvroDirectEncoder::EncodeArrowToAvro(avro_schema.root(),
*input_array,
+ 1,
encoder.get(), &encode_ctx_),
+ "Row index 1 out of bounds 1");
+ }
+ {
+ // Test null value in non-nullable field
+ arrow::Int32Builder nullable_builder;
+ ASSERT_TRUE(nullable_builder.AppendNull().ok());
+ std::shared_ptr<arrow::Array> nullable_array;
+ ASSERT_TRUE(nullable_builder.Finish(&nullable_array).ok());
+
+ ASSERT_NOK_WITH_MSG(
+ AvroDirectEncoder::EncodeArrowToAvro(avro_schema.root(),
*nullable_array, 0,
+ encoder.get(), &encode_ctx_),
+ "Null value in non-nullable field");
+ }
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestDecoderErrorCases) {
+ std::string schema_json = R"(["null", "int"])";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+
+ // Test with invalid union branch index, branch index 2, but union only
has 2 branches (0,1)
+ std::vector<uint8_t> invalid_data = {0x04};
+ auto input_stream = ::avro::memoryInputStream(invalid_data.data(),
invalid_data.size());
+ auto decoder = ::avro::binaryDecoder();
+ decoder->init(*input_stream);
+
+ arrow::Int32Builder builder;
+ ASSERT_NOK_WITH_MSG(
+ AvroDirectDecoder::DecodeAvroToBuilder(avro_schema.root(),
std::nullopt, decoder.get(),
+ &builder, &decode_ctx_),
+ "Union branch index 2 out of range [0, 2)");
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestInvalidUnionType) {
+ auto run = [&](const std::string& schema_json, const std::string&
error_msg) {
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+ auto output_stream = ::avro::memoryOutputStream();
+ auto encoder = ::avro::binaryEncoder();
+ encoder->init(*output_stream);
+
+ arrow::Int32Builder builder;
+ ASSERT_TRUE(builder.Append(42).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+
+
ASSERT_NOK_WITH_MSG(AvroDirectEncoder::EncodeArrowToAvro(avro_schema.root(),
*input_array,
+ 0,
encoder.get(), &encode_ctx_),
+ error_msg);
+ };
+ // Test union with more than 2 branches
+ run(R"(["null", "int", "string"])", "Union must have exactly 2 branches,
got 3");
+ // Test union with null branch not first
+ run(R"(["int", "null"])",
+ "Unexpected: In paimon, we expect the null branch to be the first
branch in a union.");
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestInvalidMapType) {
+ std::string schema_json = R"({
+ "type": "map",
+ "values": "string"
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+
+ arrow::MapBuilder map_builder(arrow::default_memory_pool(),
+ std::make_shared<arrow::Int32Builder>(),
+ std::make_shared<arrow::StringBuilder>());
+ auto key_builder =
static_cast<arrow::Int32Builder*>(map_builder.key_builder());
+ auto value_builder =
static_cast<arrow::StringBuilder*>(map_builder.item_builder());
+ ASSERT_TRUE(map_builder.Append().ok());
+ ASSERT_TRUE(key_builder->Append(1).ok());
+ ASSERT_TRUE(value_builder->Append("value1").ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(map_builder.Finish(&input_array).ok());
+
+ ASSERT_NOK_WITH_MSG(EncodeData(avro_schema.root(), input_array),
+ "AVRO_MAP keys must be StringArray, got int32");
+}
+
+TEST_F(AvroDirectEncoderDecoderTest, TestInvalidArrayBasedMapType) {
+ std::string schema_json = R"({
+ "type" : "array",
+ "items" : {
+ "type" : "record",
+ "name" : "record_f1",
+ "fields" : [ {
+ "name" : "key",
+ "type" : "int"
+ }, {
+ "name" : "value",
+ "type" : "string"
+ }, {
+ "name" : "metadata",
+ "type" : "string"
+ } ]
+ },
+ "logicalType" : "map"
+ })";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+
+ arrow::MapBuilder map_builder(arrow::default_memory_pool(),
+ std::make_shared<arrow::Int32Builder>(),
+ std::make_shared<arrow::StringBuilder>());
+ ASSERT_TRUE(map_builder.Append().ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(map_builder.Finish(&input_array).ok());
+
+ ASSERT_NOK_WITH_MSG(EncodeData(avro_schema.root(), input_array),
+ "Expected AVRO_RECORD for map key-value pair");
+}
+
+#ifndef NDEBUG
+TEST_F(AvroDirectEncoderDecoderTest, TestTypeMismatch) {
+ // Test string schema with int array (The type-mismatch issue should not
occur, so we only
+ // perform type conversion checks in debug mode.)
+ std::string schema_json = R"({"type": "string"})";
+ auto avro_schema = ::avro::compileJsonSchemaFromString(schema_json);
+ auto output_stream = ::avro::memoryOutputStream();
+ auto encoder = ::avro::binaryEncoder();
+ encoder->init(*output_stream);
+
+ arrow::Int32Builder builder;
+ ASSERT_TRUE(builder.Append(42).ok());
+ std::shared_ptr<arrow::Array> input_array;
+ ASSERT_TRUE(builder.Finish(&input_array).ok());
+
+ ASSERT_THROW(auto status = AvroDirectEncoder::EncodeArrowToAvro(
+ avro_schema.root(), *input_array, 0, encoder.get(),
&encode_ctx_),
+ std::bad_cast);
+}
+#endif
+
+} // namespace paimon::avro::test