blob: f29e1d11e77e8d940e292ecad8d744baad8703f0 [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/common/utils/arrow/arrow_utils.h"
#include "arrow/array/array_base.h"
#include "arrow/array/array_nested.h"
#include "arrow/array/concatenate.h"
#include "arrow/array/util.h"
#include "arrow/buffer.h"
#include "arrow/type_traits.h"
#include "arrow/util/bit_util.h"
#include "arrow/util/bitmap_ops.h"
#include "arrow/util/compression.h"
#include "fmt/format.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/arrow/vector_utils.h"
#include "paimon/common/utils/checked_cast.h"
#include "paimon/common/utils/string_utils.h"
namespace paimon {
namespace {
bool HasNonZeroOffset(const std::shared_ptr<arrow::ArrayData>& data) {
if (data->offset != 0) {
return true;
}
for (const auto& child : data->child_data) {
if (HasNonZeroOffset(child)) {
return true;
}
}
return false;
}
Result<std::shared_ptr<arrow::ArrayData>> CopyToZeroOffset(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> copied,
arrow::Concatenate({arrow::MakeArray(data)}, pool));
return copied->data();
}
Result<std::shared_ptr<arrow::Buffer>> RebaseBitmap(const arrow::ArrayData& data,
const std::shared_ptr<arrow::Buffer>& bitmap,
arrow::MemoryPool* pool) {
if (data.offset % 8 == 0) {
return arrow::SliceBuffer(bitmap, data.offset / 8,
arrow::bit_util::BytesForBits(data.length));
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::Buffer> copied,
arrow::internal::CopyBitmap(pool, bitmap->data(), data.offset, data.length));
return copied;
}
Result<std::shared_ptr<arrow::Buffer>> RebaseValidityBitmap(const arrow::ArrayData& data,
arrow::MemoryPool* pool) {
const std::shared_ptr<arrow::Buffer>& bitmap = data.buffers[0];
if (bitmap == nullptr || data.null_count.load() == 0) {
return std::shared_ptr<arrow::Buffer>();
}
return RebaseBitmap(data, bitmap, pool);
}
struct RebasedOffsets {
std::shared_ptr<arrow::Buffer> buffer;
int64_t first_value = 0;
int64_t last_value = 0;
};
template <typename OffsetType>
Result<RebasedOffsets> RebaseOffsets(const arrow::ArrayData& data, arrow::MemoryPool* pool) {
const auto* offsets = data.GetValues<OffsetType>(1);
const OffsetType base = offsets[0];
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::unique_ptr<arrow::Buffer> buffer,
arrow::AllocateBuffer((data.length + 1) * static_cast<int64_t>(sizeof(OffsetType)), pool));
auto* rebased = reinterpret_cast<OffsetType*>(buffer->mutable_data());
for (int64_t i = 0; i <= data.length; i++) {
rebased[i] = offsets[i] - base;
}
return RebasedOffsets{std::shared_ptr<arrow::Buffer>(std::move(buffer)), base,
offsets[data.length]};
}
Result<std::shared_ptr<arrow::ArrayData>> RebaseToZeroOffset(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool);
/// Rebases a boolean array, whose values are a bitmap rather than byte addressable.
Result<std::shared_ptr<arrow::ArrayData>> RebaseBoolean(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
if (data->buffers.size() != 2 || data->buffers[1] == nullptr) {
return CopyToZeroOffset(data, pool);
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> validity,
RebaseValidityBitmap(*data, pool));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> values,
RebaseBitmap(*data, data->buffers[1], pool));
std::shared_ptr<arrow::ArrayData> rebased =
arrow::ArrayData::Make(data->type, data->length, data->null_count.load(), /*offset=*/0);
rebased->buffers = {std::move(validity), std::move(values)};
return rebased;
}
/// Rebases the {validity, offsets, values} layout of binary-like arrays.
template <typename OffsetType>
Result<std::shared_ptr<arrow::ArrayData>> RebaseBinaryLike(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
if (data->buffers.size() != 3 || data->buffers[1] == nullptr || data->buffers[2] == nullptr) {
return CopyToZeroOffset(data, pool);
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> validity,
RebaseValidityBitmap(*data, pool));
PAIMON_ASSIGN_OR_RAISE(RebasedOffsets offsets, RebaseOffsets<OffsetType>(*data, pool));
std::shared_ptr<arrow::ArrayData> rebased =
arrow::ArrayData::Make(data->type, data->length, data->null_count.load(), /*offset=*/0);
rebased->buffers = {std::move(validity), std::move(offsets.buffer),
arrow::SliceBuffer(data->buffers[2], offsets.first_value,
offsets.last_value - offsets.first_value)};
return rebased;
}
/// Rebases the {validity, offsets} plus single child layout of list, large list and map arrays.
template <typename OffsetType>
Result<std::shared_ptr<arrow::ArrayData>> RebaseListLike(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
if (data->buffers.size() != 2 || data->buffers[1] == nullptr || data->child_data.size() != 1) {
return CopyToZeroOffset(data, pool);
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> validity,
RebaseValidityBitmap(*data, pool));
PAIMON_ASSIGN_OR_RAISE(RebasedOffsets offsets, RebaseOffsets<OffsetType>(*data, pool));
// A contiguous slice of the parent always spans a contiguous range of the child.
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::ArrayData> child_slice,
data->child_data[0]->SliceSafe(offsets.first_value,
offsets.last_value - offsets.first_value));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ArrayData> child,
RebaseToZeroOffset(child_slice, pool));
std::shared_ptr<arrow::ArrayData> rebased =
arrow::ArrayData::Make(data->type, data->length, data->null_count.load(), /*offset=*/0);
rebased->buffers = {std::move(validity), std::move(offsets.buffer)};
rebased->child_data = {std::move(child)};
return rebased;
}
/// Rebases a fixed size list array, whose child holds `list_size` values per row.
Result<std::shared_ptr<arrow::ArrayData>> RebaseFixedSizeList(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
if (data->child_data.size() != 1) {
return CopyToZeroOffset(data, pool);
}
const int64_t list_size =
checked_cast<const arrow::FixedSizeListType&>(*data->type).list_size();
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> validity,
RebaseValidityBitmap(*data, pool));
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::ArrayData> child_slice,
data->child_data[0]->SliceSafe(data->offset * list_size, data->length * list_size));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ArrayData> child,
RebaseToZeroOffset(child_slice, pool));
std::shared_ptr<arrow::ArrayData> rebased =
arrow::ArrayData::Make(data->type, data->length, data->null_count.load(), /*offset=*/0);
rebased->buffers = {std::move(validity)};
rebased->child_data = {std::move(child)};
return rebased;
}
/// Rebases a struct array, whose slices keep full length children.
Result<std::shared_ptr<arrow::ArrayData>> RebaseStruct(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
if (data->buffers.empty()) {
return CopyToZeroOffset(data, pool);
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> validity,
RebaseValidityBitmap(*data, pool));
std::shared_ptr<arrow::ArrayData> rebased =
arrow::ArrayData::Make(data->type, data->length, data->null_count.load(), /*offset=*/0);
rebased->buffers = {std::move(validity)};
rebased->child_data.reserve(data->child_data.size());
for (const auto& child : data->child_data) {
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::ArrayData> child_slice,
child->SliceSafe(data->offset, data->length));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ArrayData> rebased_child,
RebaseToZeroOffset(child_slice, pool));
rebased->child_data.push_back(std::move(rebased_child));
}
return rebased;
}
/// Slices the single value buffer of a fixed width array. Returns nullptr when the layout is not
/// a plain byte addressable fixed width one.
Result<std::shared_ptr<arrow::ArrayData>> RebaseFixedWidth(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
// arrow::is_fixed_width() also covers dictionary types, whose dictionary is not in child_data.
if (!arrow::is_fixed_width(data->type->id()) || data->buffers.size() != 2 ||
data->buffers[1] == nullptr || !data->child_data.empty() || data->dictionary != nullptr) {
return std::shared_ptr<arrow::ArrayData>();
}
const int32_t bit_width = checked_cast<const arrow::FixedWidthType&>(*data->type).bit_width();
if (bit_width <= 0 || bit_width % 8 != 0) {
return std::shared_ptr<arrow::ArrayData>();
}
const int64_t byte_width = bit_width / 8;
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> validity,
RebaseValidityBitmap(*data, pool));
std::shared_ptr<arrow::ArrayData> rebased =
arrow::ArrayData::Make(data->type, data->length, data->null_count.load(), /*offset=*/0);
rebased->buffers = {
std::move(validity),
arrow::SliceBuffer(data->buffers[1], data->offset * byte_width, data->length * byte_width)};
return rebased;
}
/// Returns an ArrayData describing the same rows as `data` with a zero offset at every level.
/// Buffers are sliced rather than copied wherever the layout allows it, so the cost is
/// proportional to the number of rows instead of the number of value bytes.
Result<std::shared_ptr<arrow::ArrayData>> RebaseToZeroOffset(
const std::shared_ptr<arrow::ArrayData>& data, arrow::MemoryPool* pool) {
if (!HasNonZeroOffset(data)) {
return data;
}
// An empty array may not carry the buffers the layouts below slice.
if (data->length == 0 || data->buffers.empty()) {
return CopyToZeroOffset(data, pool);
}
switch (data->type->id()) {
case arrow::Type::BOOL:
return RebaseBoolean(data, pool);
case arrow::Type::STRING:
case arrow::Type::BINARY:
return RebaseBinaryLike<int32_t>(data, pool);
case arrow::Type::LARGE_STRING:
case arrow::Type::LARGE_BINARY:
return RebaseBinaryLike<int64_t>(data, pool);
case arrow::Type::LIST:
case arrow::Type::MAP:
return RebaseListLike<int32_t>(data, pool);
case arrow::Type::LARGE_LIST:
return RebaseListLike<int64_t>(data, pool);
case arrow::Type::FIXED_SIZE_LIST:
return RebaseFixedSizeList(data, pool);
case arrow::Type::STRUCT:
return RebaseStruct(data, pool);
case arrow::Type::DICTIONARY:
return CopyToZeroOffset(data, pool);
default:
break;
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ArrayData> rebased, RebaseFixedWidth(data, pool));
if (rebased != nullptr) {
return rebased;
}
return CopyToZeroOffset(data, pool);
}
} // namespace
const char* ArrowUtils::kArrowSchemaMetadataKey = "ARROW:schema";
Result<std::shared_ptr<arrow::Schema>> ArrowUtils::DataTypeToSchema(
const std::shared_ptr<arrow::DataType>& data_type) {
if (!data_type || data_type->id() != arrow::Type::STRUCT) {
return Status::Invalid(fmt::format("Expected struct data type, actual data type: {}",
data_type ? data_type->ToString() : "null"));
}
const auto& struct_type = checked_pointer_cast<arrow::StructType>(data_type);
return std::make_shared<arrow::Schema>(struct_type->fields());
}
Result<std::vector<int32_t>> ArrowUtils::CreateProjection(
const std::shared_ptr<arrow::Schema>& src_schema, const arrow::FieldVector& target_fields) {
std::vector<int32_t> target_to_src_mapping;
target_to_src_mapping.reserve(target_fields.size());
for (const auto& field : target_fields) {
auto src_field_idx = src_schema->GetFieldIndex(field->name());
if (src_field_idx < 0) {
return Status::Invalid(
fmt::format("Field '{}' not found or duplicate in src schema", field->name()));
}
target_to_src_mapping.push_back(src_field_idx);
}
return target_to_src_mapping;
}
Status ArrowUtils::CheckNullabilityMatch(const std::shared_ptr<arrow::Schema>& schema,
const std::shared_ptr<arrow::Array>& data) {
if (!schema || !data || data->type_id() != arrow::Type::STRUCT) {
return Status::Invalid("CheckNullabilityMatch requires a schema and a struct array");
}
auto struct_array = checked_pointer_cast<arrow::StructArray>(data);
if (struct_array->num_fields() != schema->num_fields()) {
return Status::Invalid(fmt::format(
"CheckNullabilityMatch failed, data field count {} mismatch schema field count {}",
struct_array->num_fields(), schema->num_fields()));
}
for (int32_t i = 0; i < schema->num_fields(); i++) {
PAIMON_RETURN_NOT_OK(InnerCheckNullabilityMatch(schema->field(i), struct_array->field(i)));
}
return Status::OK();
}
void ArrowUtils::TraverseArray(const std::shared_ptr<arrow::Array>& array) {
arrow::Type::type type = array->type()->id();
switch (type) {
case arrow::Type::type::DICTIONARY: {
auto* dict_array = checked_cast<arrow::DictionaryArray*>(array.get());
[[maybe_unused]] auto dict = dict_array->dictionary();
return;
}
case arrow::Type::type::STRUCT: {
auto* struct_array = checked_cast<arrow::StructArray*>(array.get());
for (const auto& field : struct_array->fields()) {
TraverseArray(field);
}
return;
}
case arrow::Type::type::MAP: {
auto* map_array = checked_cast<arrow::MapArray*>(array.get());
TraverseArray(map_array->keys());
TraverseArray(map_array->items());
return;
}
case arrow::Type::type::LIST: {
auto* list_array = checked_cast<arrow::ListArray*>(array.get());
TraverseArray(list_array->values());
return;
}
case arrow::Type::type::FIXED_SIZE_LIST: {
auto* vector_array = checked_cast<arrow::FixedSizeListArray*>(array.get());
TraverseArray(vector_array->values());
return;
}
default:
return;
}
}
bool ArrowUtils::EqualsIgnoreNullable(const std::shared_ptr<arrow::DataType>& type,
const std::shared_ptr<arrow::DataType>& other_type) {
if (type->id() != other_type->id() || type->num_fields() != other_type->num_fields()) {
return false;
}
if (type->id() == arrow::Type::FIXED_SIZE_LIST) {
const auto& vector_type = checked_cast<const arrow::FixedSizeListType&>(*type);
const auto& other_vector_type = checked_cast<const arrow::FixedSizeListType&>(*other_type);
if (vector_type.list_size() != other_vector_type.list_size()) {
return false;
}
}
for (int32_t i = 0; i < type->num_fields(); ++i) {
const auto& field = type->field(i);
const auto& other_field = other_type->field(i);
if (field->name() != other_field->name()) {
return false;
}
if (!EqualsIgnoreNullable(field->type(), other_field->type())) {
return false;
}
}
return true;
}
Status ArrowUtils::InnerCheckNullabilityMatch(const std::shared_ptr<arrow::Field>& field,
const std::shared_ptr<arrow::Array>& data) {
if (PAIMON_UNLIKELY(!field->nullable() && data->null_count() != 0)) {
return Status::Invalid(fmt::format(
"CheckNullabilityMatch failed, field {} not nullable while data have null value",
field->name()));
}
auto type = field->type();
if (type->id() == arrow::Type::STRUCT) {
auto struct_type = checked_pointer_cast<arrow::StructType>(field->type());
auto struct_array = checked_pointer_cast<arrow::StructArray>(data);
for (int32_t i = 0; i < struct_type->num_fields(); ++i) {
PAIMON_RETURN_NOT_OK(
InnerCheckNullabilityMatch(struct_type->field(i), struct_array->field(i)));
}
} else if (type->id() == arrow::Type::LIST) {
auto list_type = checked_pointer_cast<arrow::ListType>(field->type());
auto list_array = checked_pointer_cast<arrow::ListArray>(data);
PAIMON_RETURN_NOT_OK(
InnerCheckNullabilityMatch(list_type->value_field(), list_array->values()));
} else if (type->id() == arrow::Type::FIXED_SIZE_LIST) {
Status status = VectorUtils::ValidateVectorElements(*data);
if (!status.ok()) {
return Status::Invalid(
fmt::format("VECTOR field {} is invalid: {}", field->name(), status.message()));
}
} else if (type->id() == arrow::Type::MAP) {
auto map_type = checked_pointer_cast<arrow::MapType>(field->type());
auto map_array = checked_pointer_cast<arrow::MapArray>(data);
PAIMON_RETURN_NOT_OK(InnerCheckNullabilityMatch(map_type->key_field(), map_array->keys()));
PAIMON_RETURN_NOT_OK(
InnerCheckNullabilityMatch(map_type->item_field(), map_array->items()));
}
return Status::OK();
}
Result<std::shared_ptr<arrow::StructArray>> ArrowUtils::RemoveFieldFromStructArray(
const std::shared_ptr<arrow::StructArray>& struct_array, const std::string& field_name) {
auto struct_type = checked_pointer_cast<arrow::StructType>(struct_array->type());
int32_t field_idx = struct_type->GetFieldIndex(field_name);
if (field_idx == -1) {
return struct_array;
}
std::vector<std::shared_ptr<arrow::Array>> new_arrays;
std::vector<std::shared_ptr<arrow::Field>> new_fields;
for (int32_t i = 0; i < struct_type->num_fields(); ++i) {
if (i != field_idx) {
new_arrays.emplace_back(struct_array->field(i));
new_fields.emplace_back(struct_type->field(i));
}
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::StructArray> array,
arrow::StructArray::Make(new_arrays, new_fields, struct_array->null_bitmap(),
struct_array->null_count(), struct_array->offset()));
return array;
}
Result<std::shared_ptr<arrow::RecordBatch>> ArrowUtils::NormalizeRecordBatchOffsets(
const std::shared_ptr<arrow::RecordBatch>& record_batch, arrow::MemoryPool* pool) {
arrow::ArrayVector normalized_columns;
for (int32_t i = 0; i < record_batch->num_columns(); ++i) {
const std::shared_ptr<arrow::Array>& column = record_batch->column(i);
if (!HasNonZeroOffset(column->data())) {
continue;
}
if (normalized_columns.empty()) {
normalized_columns = record_batch->columns();
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ArrayData> normalized_data,
RebaseToZeroOffset(column->data(), pool));
normalized_columns[i] = arrow::MakeArray(normalized_data);
}
if (normalized_columns.empty()) {
return record_batch;
}
return arrow::RecordBatch::Make(record_batch->schema(), record_batch->num_rows(),
std::move(normalized_columns));
}
Result<arrow::Compression::type> ArrowUtils::GetCompressionType(const std::string& compression) {
std::string normalized = StringUtils::ToLowerCase(compression);
if (normalized.empty() || normalized == "none") {
normalized = "uncompressed";
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(arrow::Compression::type compression_type,
arrow::util::Codec::GetCompressionType(normalized));
return compression_type;
}
} // namespace paimon