blob: ef905fff38d6d4d784b06425530f4ac3c0adcd1f [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 "paimon/format/avro/avro_schema_converter.h"
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>
#include "arrow/util/checked_cast.h"
#include "avro/CustomAttributes.hh"
#include "avro/LogicalType.hh"
#include "avro/Node.hh"
#include "avro/Schema.hh"
#include "avro/Types.hh"
#include "avro/ValidSchema.hh"
#include "fmt/format.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/format/avro/avro_file_format_factory.h"
#include "paimon/format/avro/avro_utils.h"
#include "paimon/macros.h"
#include "paimon/status.h"
namespace paimon::avro {
/// Returns schema with nullable true.
::avro::Schema AvroSchemaConverter::NullableSchema(const ::avro::Schema& schema) {
assert(schema.type() != ::avro::AVRO_UNION);
::avro::UnionSchema union_schema;
union_schema.addType(::avro::NullSchema());
union_schema.addType(schema);
return union_schema;
}
void AvroSchemaConverter::AddRecordField(::avro::RecordSchema* record_schema,
const std::string& field_name,
const ::avro::Schema& field_schema) {
if (field_schema.type() == ::avro::Type::AVRO_UNION) {
::avro::CustomAttributes attrs;
attrs.addAttribute("default", "null", /*addQuotes=*/false);
record_schema->addField(field_name, field_schema, attrs);
} else {
record_schema->addField(field_name, field_schema);
}
}
Result<bool> AvroSchemaConverter::CheckUnionType(const ::avro::NodePtr& avro_node) {
auto type = avro_node->type();
if (type == ::avro::AVRO_UNION) {
if (avro_node->leaves() != 2) {
return Status::Invalid("not support avro union leaves not 2");
}
auto node = avro_node->leafAt(0);
if (node->type() != ::avro::AVRO_NULL) {
return Status::Invalid("not support avro union first leaf is not avro null");
}
return true;
}
return false;
}
Result<std::shared_ptr<arrow::DataType>> AvroSchemaConverter::AvroSchemaToArrowDataType(
const ::avro::ValidSchema& avro_schema) {
::avro::NodePtr root = avro_schema.root();
PAIMON_ASSIGN_OR_RAISE(bool is_union, CheckUnionType(root));
if (is_union) {
root = root->leafAt(1);
}
if (PAIMON_UNLIKELY(root->type() != ::avro::AVRO_RECORD)) {
return Status::Invalid("Avro schema root node is not a record type");
}
bool nullable = false;
return GetArrowType(root, &nullable);
}
Result<std::shared_ptr<arrow::Field>> AvroSchemaConverter::GetArrowField(
const std::string& name, const ::avro::NodePtr& avro_node) {
bool nullable = false;
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::DataType> arrow_type,
GetArrowType(avro_node, &nullable));
return arrow::field(name, std::move(arrow_type), nullable);
}
Result<std::shared_ptr<arrow::DataType>> AvroSchemaConverter::GetArrowType(
const ::avro::NodePtr& avro_node, bool* nullable) {
PAIMON_ASSIGN_OR_RAISE(bool is_union, CheckUnionType(avro_node));
if (is_union) {
*nullable = true;
return GetArrowType(avro_node->leafAt(1), nullable);
}
auto type = avro_node->type();
auto logical_type = avro_node->logicalType();
switch (logical_type.type()) {
case ::avro::LogicalType::Type::NONE:
break;
case ::avro::LogicalType::Type::DATE:
if (type != ::avro::AVRO_INT) {
return Status::TypeError("invalid avro date stored as ", toString(type));
}
return arrow::date32();
case ::avro::LogicalType::Type::DECIMAL:
if (type != ::avro::AVRO_BYTES) {
return Status::TypeError("invalid avro decimal stored as ", toString(type));
}
return arrow::decimal128(logical_type.precision(), logical_type.scale());
case ::avro::LogicalType::Type::TIMESTAMP_MILLIS: {
if (type != ::avro::AVRO_LONG) {
return Status::TypeError("invalid avro timestamp stored as ", toString(type));
}
return arrow::timestamp(arrow::TimeUnit::MILLI);
}
case ::avro::LogicalType::Type::TIMESTAMP_MICROS: {
if (type != ::avro::AVRO_LONG) {
return Status::TypeError("invalid avro timestamp stored as ", toString(type));
}
return arrow::timestamp(arrow::TimeUnit::MICRO);
}
case ::avro::LogicalType::Type::TIMESTAMP_NANOS: {
if (type != ::avro::AVRO_LONG) {
return Status::TypeError("invalid avro timestamp stored as ", toString(type));
}
return arrow::timestamp(arrow::TimeUnit::NANO);
}
case ::avro::LogicalType::Type::LOCAL_TIMESTAMP_MILLIS: {
if (type != ::avro::AVRO_LONG) {
return Status::TypeError("invalid avro timestamp stored as ", toString(type));
}
auto timezone = DateTimeUtils::GetLocalTimezoneName();
return arrow::timestamp(arrow::TimeUnit::MILLI, timezone);
}
case ::avro::LogicalType::Type::LOCAL_TIMESTAMP_MICROS: {
if (type != ::avro::AVRO_LONG) {
return Status::TypeError("invalid avro timestamp stored as ", toString(type));
}
auto timezone = DateTimeUtils::GetLocalTimezoneName();
return arrow::timestamp(arrow::TimeUnit::MICRO, timezone);
}
case ::avro::LogicalType::Type::LOCAL_TIMESTAMP_NANOS: {
if (type != ::avro::AVRO_LONG) {
return Status::TypeError("invalid avro timestamp stored as ", toString(type));
}
auto timezone = DateTimeUtils::GetLocalTimezoneName();
return arrow::timestamp(arrow::TimeUnit::NANO, timezone);
}
case ::avro::LogicalType::Type::CUSTOM: {
if (!AvroUtils::HasMapLogicalType(avro_node)) {
return Status::TypeError("invalid avro logical map type");
}
if (type != ::avro::AVRO_ARRAY) {
return Status::TypeError("invalid avro logical map stored as ", toString(type));
}
size_t subtype_count = avro_node->leaves();
if (subtype_count != 1) {
return Status::TypeError("invalid avro logical map type");
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> logical_map_field,
GetArrowField("item", avro_node->leafAt(0)));
auto logical_map_type = logical_map_field->type();
if (logical_map_type->id() != arrow::Type::STRUCT) {
return Status::TypeError("invalid avro logical map item type");
}
auto struct_type =
arrow::internal::checked_pointer_cast<arrow::StructType>(logical_map_type);
const auto& fields = struct_type->fields();
if (fields.size() != 2) {
return Status::TypeError("invalid avro logical map struct fields size");
}
auto key_field = fields[0]->WithNullable(false);
auto value_field = fields[1];
if (key_field->name() != "key" || value_field->name() != "value") {
return Status::TypeError("invalid avro logical map struct field names");
}
return std::make_shared<arrow::MapType>(std::move(key_field), std::move(value_field));
}
default:
return Status::Invalid("invalid avro logical type: ",
AvroUtils::ToString(logical_type));
}
size_t subtype_count = avro_node->leaves();
switch (type) {
case ::avro::AVRO_BOOL: {
return arrow::boolean();
}
case ::avro::AVRO_INT: {
return arrow::int32();
}
case ::avro::AVRO_LONG: {
return arrow::int64();
}
case ::avro::AVRO_FLOAT: {
return arrow::float32();
}
case ::avro::AVRO_DOUBLE: {
return arrow::float64();
}
case ::avro::AVRO_STRING: {
return arrow::utf8();
}
case ::avro::AVRO_BYTES: {
return arrow::binary();
}
case ::avro::AVRO_ARRAY: {
if (subtype_count != 1) {
return Status::TypeError("Invalid Avro List type");
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> child_field,
GetArrowField("item", avro_node->leafAt(0)));
return arrow::list(std::move(child_field));
}
case ::avro::AVRO_MAP: {
if (subtype_count != 2) {
return Status::TypeError("Invalid Avro Map type");
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> key_field,
GetArrowField("key", avro_node->leafAt(0)));
key_field = key_field->WithNullable(false);
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> value_field,
GetArrowField("value", avro_node->leafAt(1)));
return std::make_shared<arrow::MapType>(std::move(key_field), std::move(value_field));
}
case ::avro::AVRO_RECORD: {
arrow::FieldVector fields(subtype_count);
for (size_t child = 0; child < subtype_count; ++child) {
const auto& name = avro_node->nameAt(child);
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> child_field,
GetArrowField(name, avro_node->leafAt(child)));
fields[child] = std::move(child_field);
}
return arrow::struct_(std::move(fields));
}
default:
return Status::TypeError("Unknown Avro type kind: ", toString(type));
}
}
Result<::avro::Schema> AvroSchemaConverter::ArrowTypeToAvroSchema(
const std::shared_ptr<arrow::Field>& field, const std::string& row_name) {
bool nullable = field->nullable();
auto arrow_type = field->type();
switch (arrow_type->id()) {
case arrow::Type::BOOL:
return nullable ? NullableSchema(::avro::BoolSchema()) : ::avro::BoolSchema();
case arrow::Type::INT8:
case arrow::Type::INT16:
case arrow::Type::INT32:
return nullable ? NullableSchema(::avro::IntSchema()) : ::avro::IntSchema();
case arrow::Type::INT64:
return nullable ? NullableSchema(::avro::LongSchema()) : ::avro::LongSchema();
case arrow::Type::FLOAT:
return nullable ? NullableSchema(::avro::FloatSchema()) : ::avro::FloatSchema();
case arrow::Type::DOUBLE:
return nullable ? NullableSchema(::avro::DoubleSchema()) : ::avro::DoubleSchema();
case arrow::Type::STRING:
return nullable ? NullableSchema(::avro::StringSchema()) : ::avro::StringSchema();
case arrow::Type::BINARY:
return nullable ? NullableSchema(::avro::BytesSchema()) : ::avro::BytesSchema();
case arrow::Type::type::DATE32: {
::avro::IntSchema date_schema;
::avro::LogicalType date_type = ::avro::LogicalType(::avro::LogicalType::DATE);
date_schema.root()->setLogicalType(date_type);
return nullable ? NullableSchema(date_schema) : date_schema;
}
case arrow::Type::type::TIMESTAMP: {
const auto& arrow_timestamp_type =
arrow::internal::checked_pointer_cast<arrow::TimestampType>(arrow_type);
bool has_timezone = !arrow_timestamp_type->timezone().empty();
::avro::LongSchema timestamp_schema;
switch (arrow_timestamp_type->unit()) {
// Avro doesn't support seconds, convert to milliseconds
case arrow::TimeUnit::type::SECOND:
case arrow::TimeUnit::type::MILLI: {
::avro::LogicalType logical_type = ::avro::LogicalType(
has_timezone ? ::avro::LogicalType::LOCAL_TIMESTAMP_MILLIS
: ::avro::LogicalType::TIMESTAMP_MILLIS);
timestamp_schema.root()->setLogicalType(logical_type);
break;
}
case arrow::TimeUnit::type::MICRO: {
::avro::LogicalType logical_type = ::avro::LogicalType(
has_timezone ? ::avro::LogicalType::LOCAL_TIMESTAMP_MICROS
: ::avro::LogicalType::TIMESTAMP_MICROS);
timestamp_schema.root()->setLogicalType(logical_type);
break;
}
case arrow::TimeUnit::type::NANO: {
::avro::LogicalType logical_type = ::avro::LogicalType(
has_timezone ? ::avro::LogicalType::LOCAL_TIMESTAMP_NANOS
: ::avro::LogicalType::TIMESTAMP_NANOS);
timestamp_schema.root()->setLogicalType(logical_type);
break;
}
default:
return Status::Invalid("Unknown TimeUnit in TimestampType");
}
return nullable ? NullableSchema(timestamp_schema) : timestamp_schema;
}
case arrow::Type::type::DECIMAL128: {
const auto& arrow_decimal_type =
arrow::internal::checked_pointer_cast<arrow::Decimal128Type>(arrow_type);
::avro::BytesSchema decimal_schema;
::avro::LogicalType decimal_type = ::avro::LogicalType(::avro::LogicalType::DECIMAL);
decimal_type.setPrecision(arrow_decimal_type->precision());
decimal_type.setScale(arrow_decimal_type->scale());
decimal_schema.root()->setLogicalType(decimal_type);
return nullable ? NullableSchema(decimal_schema) : decimal_schema;
}
case arrow::Type::LIST: {
const auto& list_type =
arrow::internal::checked_pointer_cast<const arrow::ListType>(arrow_type);
const auto& value_field = list_type->value_field();
PAIMON_ASSIGN_OR_RAISE(::avro::Schema value_schema,
ArrowTypeToAvroSchema(value_field, row_name));
::avro::ArraySchema array_schema(value_schema);
return nullable ? NullableSchema(array_schema) : array_schema;
}
case arrow::Type::STRUCT: {
const auto& struct_type =
arrow::internal::checked_pointer_cast<const arrow::StructType>(arrow_type);
const auto& fields = struct_type->fields();
::avro::RecordSchema record_schema(row_name);
for (const auto& f : fields) {
PAIMON_ASSIGN_OR_RAISE(::avro::Schema field_schema,
ArrowTypeToAvroSchema(f, row_name + "_" + f->name()));
AddRecordField(&record_schema, f->name(), field_schema);
}
return nullable ? NullableSchema(record_schema) : record_schema;
}
case arrow::Type::MAP: {
const auto& map_type =
arrow::internal::checked_pointer_cast<const arrow::MapType>(arrow_type);
const auto& key_field = map_type->key_field();
const auto& item_field = map_type->item_field();
if (key_field->nullable()) {
return Status::Invalid("Avro Map key cannot be nullable");
}
if (key_field->type()->id() == arrow::Type::STRING) {
PAIMON_ASSIGN_OR_RAISE(::avro::Schema item_schema,
ArrowTypeToAvroSchema(item_field, row_name));
::avro::MapSchema map_schema(item_schema);
return nullable ? NullableSchema(map_schema) : map_schema;
} else {
// convert to list<record<key,value>>
PAIMON_ASSIGN_OR_RAISE(::avro::Schema key_schema,
ArrowTypeToAvroSchema(key_field, row_name + "_key"));
PAIMON_ASSIGN_OR_RAISE(::avro::Schema item_schema,
ArrowTypeToAvroSchema(item_field, row_name + "_value"));
::avro::LogicalType logical_map_type =
::avro::LogicalType(std::make_shared<MapLogicalType>());
::avro::RecordSchema record_schema(row_name);
AddRecordField(&record_schema, "key", key_schema);
AddRecordField(&record_schema, "value", item_schema);
::avro::ArraySchema logical_map_schema(record_schema);
logical_map_schema.root()->setLogicalType(logical_map_type);
return nullable ? NullableSchema(logical_map_schema) : logical_map_schema;
}
}
default:
return Status::Invalid(fmt::format("Not support arrow type '{}' convert to avro",
field->type()->ToString()));
}
}
Result<::avro::ValidSchema> AvroSchemaConverter::ArrowSchemaToAvroSchema(
const std::shared_ptr<arrow::Schema>& arrow_schema) {
// top level row name of avro record, the same as java paimon
static const std::string kTopLevelRowName = "org.apache.paimon.avro.generated.record";
::avro::RecordSchema record_schema(kTopLevelRowName);
for (const auto& field : arrow_schema->fields()) {
PAIMON_ASSIGN_OR_RAISE(
::avro::Schema field_schema,
ArrowTypeToAvroSchema(field, kTopLevelRowName + "_" + field->name()));
AddRecordField(&record_schema, field->name(), field_schema);
}
return ::avro::ValidSchema(record_schema);
}
} // namespace paimon::avro