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 c662861 feat(common): introduce arrow utilities (#110)
c662861 is described below
commit c66286180e2d52e9b70b01d88e660db0d872c64a
Author: Zhang Jiawei <[email protected]>
AuthorDate: Thu Jun 25 10:27:33 2026 +0800
feat(common): introduce arrow utilities (#110)
---
.../utils/arrow/arrow_input_stream_adapter.cpp | 166 +++++++
.../utils/arrow/arrow_input_stream_adapter.h | 64 +++
.../utils/arrow/arrow_output_stream_adapter.cpp | 70 +++
.../utils/arrow/arrow_output_stream_adapter.h | 48 ++
.../utils/arrow/arrow_stream_adapter_test.cpp | 94 ++++
src/paimon/common/utils/arrow/arrow_utils.cpp | 179 ++++++++
src/paimon/common/utils/arrow/arrow_utils.h | 65 +++
src/paimon/common/utils/arrow/arrow_utils_test.cpp | 490 +++++++++++++++++++++
src/paimon/common/utils/arrow/mem_utils.cpp | 87 ++++
src/paimon/common/utils/arrow/mem_utils.h | 36 ++
src/paimon/common/utils/arrow/mem_utils_test.cpp | 77 ++++
.../common/utils/arrow/status_utils_test.cpp | 60 +++
12 files changed, 1436 insertions(+)
diff --git a/src/paimon/common/utils/arrow/arrow_input_stream_adapter.cpp
b/src/paimon/common/utils/arrow/arrow_input_stream_adapter.cpp
new file mode 100644
index 0000000..941c18d
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_input_stream_adapter.cpp
@@ -0,0 +1,166 @@
+/*
+ * 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_input_stream_adapter.h"
+
+#include <cstdint>
+#include <utility>
+
+#include "arrow/api.h"
+#include "fmt/format.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/math.h"
+#include "paimon/common/utils/options_utils.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/macros.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+namespace {
+
+template <typename To, typename From>
+arrow::Status ValidateArrowIoRange(From value, const char* name) {
+ if (!InRange<To>(value)) {
+ return arrow::Status::Invalid(fmt::format("{} value {} is out of bound
of type {}", name,
+ value,
OptionsUtils::GetTypeName<To>()));
+ }
+ return arrow::Status::OK();
+}
+
+} // namespace
+
+ArrowInputStreamAdapter::ArrowInputStreamAdapter(
+ const std::shared_ptr<paimon::InputStream>& input_stream,
+ const std::shared_ptr<arrow::MemoryPool>& pool, uint64_t file_size)
+ : input_stream_(input_stream), pool_(pool), file_size_(file_size) {}
+
+ArrowInputStreamAdapter::~ArrowInputStreamAdapter() {
+ [[maybe_unused]] auto status = DoClose();
+}
+
+arrow::Status ArrowInputStreamAdapter::Seek(int64_t position) {
+ return ToArrowStatus(input_stream_->Seek(position,
SeekOrigin::FS_SEEK_SET));
+}
+
+arrow::Result<int64_t> ArrowInputStreamAdapter::Read(int64_t nbytes, void*
out) {
+ ARROW_RETURN_NOT_OK(ValidateArrowIoRange<uint32_t>(nbytes, "nbytes"));
+ Result<int32_t> read_bytes =
+ input_stream_->Read(static_cast<char*>(out),
static_cast<uint32_t>(nbytes));
+ if (!read_bytes.ok()) {
+ return ToArrowStatus(read_bytes.status());
+ }
+ return read_bytes.value();
+}
+
+arrow::Result<std::shared_ptr<arrow::Buffer>>
ArrowInputStreamAdapter::Read(int64_t nbytes) {
+ ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ResizableBuffer> buffer,
+ arrow::AllocateResizableBuffer(nbytes, pool_.get()));
+ ARROW_ASSIGN_OR_RAISE(int64_t read_bytes, Read(nbytes,
buffer->mutable_data()));
+ if (read_bytes < nbytes) {
+ ARROW_RETURN_NOT_OK(buffer->Resize(read_bytes));
+ }
+ return std::shared_ptr<arrow::Buffer>(std::move(buffer));
+}
+
+arrow::Result<int64_t> ArrowInputStreamAdapter::ReadAt(int64_t position,
int64_t nbytes,
+ void* out) {
+ ARROW_RETURN_NOT_OK(ValidateArrowIoRange<uint64_t>(position, "position"));
+ ARROW_RETURN_NOT_OK(ValidateArrowIoRange<uint32_t>(nbytes, "nbytes"));
+ Result<int32_t> read_bytes = input_stream_->Read(
+ static_cast<char*>(out), static_cast<uint32_t>(nbytes),
static_cast<uint64_t>(position));
+ if (!read_bytes.ok()) {
+ return ToArrowStatus(read_bytes.status());
+ }
+ return read_bytes.value();
+}
+
+arrow::Result<std::shared_ptr<arrow::Buffer>>
ArrowInputStreamAdapter::ReadAt(int64_t position,
+
int64_t nbytes) {
+ ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ResizableBuffer> buffer,
+ arrow::AllocateResizableBuffer(nbytes, pool_.get()));
+ ARROW_ASSIGN_OR_RAISE(int64_t read_bytes, ReadAt(position, nbytes,
buffer->mutable_data()));
+ if (read_bytes < nbytes) {
+ ARROW_RETURN_NOT_OK(buffer->Resize(read_bytes));
+ }
+ return std::shared_ptr<arrow::Buffer>(std::move(buffer));
+}
+
+arrow::Future<std::shared_ptr<arrow::Buffer>>
ArrowInputStreamAdapter::ReadAsync(
+ const arrow::io::IOContext& io_context, int64_t position, int64_t nbytes) {
+ auto fut = arrow::Future<std::shared_ptr<arrow::Buffer>>::Make();
+ auto range_status = ValidateArrowIoRange<uint64_t>(position, "position");
+ if (!range_status.ok()) {
+ fut.MarkFinished(range_status);
+ return fut;
+ }
+ range_status = ValidateArrowIoRange<uint32_t>(nbytes, "nbytes");
+ if (!range_status.ok()) {
+ fut.MarkFinished(range_status);
+ return fut;
+ }
+
+ arrow::Result<std::shared_ptr<arrow::Buffer>> buffer_result =
+ arrow::AllocateResizableBuffer(nbytes, pool_.get());
+ if (PAIMON_UNLIKELY(!buffer_result.ok())) {
+ fut.MarkFinished(buffer_result.status());
+ return fut;
+ }
+ std::shared_ptr<arrow::Buffer> buffer =
std::move(buffer_result).ValueUnsafe();
+ input_stream_->ReadAsync(reinterpret_cast<char*>(buffer->mutable_data()),
+ static_cast<uint32_t>(nbytes),
static_cast<uint64_t>(position),
+ [fut, buffer](Status callback_status) mutable {
+ if (callback_status.ok()) {
+ fut.MarkFinished(std::move(buffer));
+ } else {
+
fut.MarkFinished(ToArrowStatus(callback_status));
+ }
+ });
+ return fut;
+}
+
+arrow::Result<int64_t> ArrowInputStreamAdapter::Tell() const {
+ Result<int64_t> position = input_stream_->GetPos();
+ if (!position.ok()) {
+ return ToArrowStatus(position.status());
+ }
+ return position.value();
+}
+
+arrow::Result<int64_t> ArrowInputStreamAdapter::GetSize() {
+ return static_cast<int64_t>(file_size_);
+}
+
+bool ArrowInputStreamAdapter::closed() const {
+ return closed_;
+}
+
+arrow::Status ArrowInputStreamAdapter::DoClose() {
+ if (!closed_) {
+ Status status = input_stream_->Close();
+ if (!status.ok()) {
+ return ToArrowStatus(status);
+ }
+ closed_ = true;
+ }
+ return arrow::Status::OK();
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/utils/arrow/arrow_input_stream_adapter.h
b/src/paimon/common/utils/arrow/arrow_input_stream_adapter.h
new file mode 100644
index 0000000..00ee164
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_input_stream_adapter.h
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+
+#include "arrow/api.h"
+#include "arrow/io/interfaces.h"
+#include "arrow/util/future.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class InputStream;
+
+class PAIMON_EXPORT ArrowInputStreamAdapter : public
arrow::io::RandomAccessFile {
+ public:
+ ArrowInputStreamAdapter(const std::shared_ptr<paimon::InputStream>&
input_stream,
+ const std::shared_ptr<arrow::MemoryPool>& pool,
uint64_t file_size);
+ ~ArrowInputStreamAdapter() override;
+
+ // NOTE: In paimon file system definition, position + nbytes should not
exceed file_size_.
+ arrow::Result<int64_t> Read(int64_t nbytes, void* out) override;
+ arrow::Result<std::shared_ptr<arrow::Buffer>> Read(int64_t nbytes)
override;
+ arrow::Result<int64_t> ReadAt(int64_t position, int64_t nbytes, void* out)
override;
+ arrow::Result<std::shared_ptr<arrow::Buffer>> ReadAt(int64_t position,
int64_t nbytes) override;
+ arrow::Future<std::shared_ptr<arrow::Buffer>> ReadAsync(const
arrow::io::IOContext& io_context,
+ int64_t position,
+ int64_t nbytes)
override;
+ arrow::Status Seek(int64_t position) override;
+ arrow::Result<int64_t> Tell() const override;
+ arrow::Result<int64_t> GetSize() override;
+ arrow::Status Close() override {
+ return DoClose();
+ }
+ bool closed() const override;
+
+ private:
+ arrow::Status DoClose();
+
+ std::shared_ptr<paimon::InputStream> input_stream_;
+ std::shared_ptr<arrow::MemoryPool> pool_;
+ uint64_t file_size_;
+ bool closed_ = false;
+};
+
+} // namespace paimon
diff --git a/src/paimon/common/utils/arrow/arrow_output_stream_adapter.cpp
b/src/paimon/common/utils/arrow/arrow_output_stream_adapter.cpp
new file mode 100644
index 0000000..c74c090
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_output_stream_adapter.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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_output_stream_adapter.h"
+
+#include "arrow/result.h"
+#include "fmt/format.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/math.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/result.h"
+
+namespace paimon {
+
+ArrowOutputStreamAdapter::ArrowOutputStreamAdapter(const
std::shared_ptr<paimon::OutputStream>& out)
+ : out_(out) {}
+
+arrow::Status ArrowOutputStreamAdapter::Close() {
+ // output stream close is called by paimon framework(such as single file
writer), no need to
+ // close here
+ closed_ = true;
+ return arrow::Status::OK();
+}
+
+arrow::Result<int64_t> ArrowOutputStreamAdapter::Tell() const {
+ paimon::Result<int64_t> pos = out_->GetPos();
+ if (!pos.ok()) {
+ return ToArrowStatus(pos.status());
+ }
+ return pos.value();
+}
+
+bool ArrowOutputStreamAdapter::closed() const {
+ return closed_;
+}
+
+arrow::Status ArrowOutputStreamAdapter::Write(const void* data, int64_t
nbytes) {
+ if (!InRange<uint32_t>(nbytes)) {
+ return arrow::Status::Invalid(
+ fmt::format("nbytes value {} is out of bound of uint32_t",
nbytes));
+ }
+ Result<int32_t> len =
+ out_->Write(static_cast<const char*>(data),
static_cast<uint32_t>(nbytes));
+ if (!len.ok()) {
+ return ToArrowStatus(len.status());
+ }
+ return arrow::Status::OK();
+}
+
+arrow::Status ArrowOutputStreamAdapter::Flush() {
+ return ToArrowStatus(out_->Flush());
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/utils/arrow/arrow_output_stream_adapter.h
b/src/paimon/common/utils/arrow/arrow_output_stream_adapter.h
new file mode 100644
index 0000000..75f791d
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_output_stream_adapter.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+
+#include "arrow/io/interfaces.h"
+#include "arrow/result.h"
+#include "arrow/status.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class OutputStream;
+
+class PAIMON_EXPORT ArrowOutputStreamAdapter : public arrow::io::OutputStream {
+ public:
+ explicit ArrowOutputStreamAdapter(const
std::shared_ptr<paimon::OutputStream>& out);
+
+ arrow::Status Close() override;
+ arrow::Result<int64_t> Tell() const override;
+ bool closed() const override;
+ arrow::Status Write(const void* data, int64_t nbytes) override;
+ arrow::Status Flush() override;
+
+ private:
+ std::shared_ptr<paimon::OutputStream> out_;
+ bool closed_ = false;
+};
+
+} // namespace paimon
diff --git a/src/paimon/common/utils/arrow/arrow_stream_adapter_test.cpp
b/src/paimon/common/utils/arrow/arrow_stream_adapter_test.cpp
new file mode 100644
index 0000000..04fba95
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_stream_adapter_test.cpp
@@ -0,0 +1,94 @@
+/*
+ * 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 <cstdint>
+#include <memory>
+#include <string>
+
+#include "arrow/api.h"
+#include "arrow/io/type_fwd.h"
+#include "arrow/util/future.h"
+#include "gtest/gtest.h"
+#include "paimon/common/utils/arrow/arrow_input_stream_adapter.h"
+#include "paimon/common/utils/arrow/arrow_output_stream_adapter.h"
+#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(ArrowStreamAdapterTest, TestInputAndOutputStream) {
+ auto test_root_dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(test_root_dir);
+ std::string test_root = test_root_dir->Str();
+ std::shared_ptr<FileSystem> file_system =
std::make_shared<LocalFileSystem>();
+ ASSERT_OK(file_system->Mkdirs(test_root));
+ std::string file_name = test_root + "/stream.arrow";
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<OutputStream> out,
+ file_system->Create(file_name, /*overwrite=*/true));
+ auto out_stream = std::make_unique<ArrowOutputStreamAdapter>(out);
+ ASSERT_EQ(out_stream->Tell().ValueOrDie(), 0);
+ ASSERT_FALSE(out_stream->closed());
+
+ std::string data = "hello";
+ ASSERT_TRUE(out_stream->Write(data.data(), data.length()).ok());
+ ASSERT_TRUE(out_stream->Flush().ok());
+ ASSERT_EQ(out_stream->Tell().ValueOrDie(), 5);
+ // noted that ArrowOutputStreamAdapter::Close() api do nothing except set
the closed_ flag.
+ ASSERT_TRUE(out_stream->Close().ok());
+ ASSERT_TRUE(out_stream->closed());
+ ASSERT_OK(out_stream->out_->Close());
+
+ // in stream
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in,
file_system->Open(file_name));
+ ASSERT_OK_AND_ASSIGN(uint64_t length, in->Length());
+ auto in_stream =
+ std::make_unique<ArrowInputStreamAdapter>(in,
GetArrowPool(GetDefaultPool()), length);
+ ASSERT_EQ(in_stream->GetSize().ValueOrDie(),
static_cast<int64_t>(data.length()));
+ ASSERT_EQ(in_stream->Tell().ValueOrDie(), 0);
+ ASSERT_FALSE(in_stream->closed());
+
+ char ret[10] = {};
+ int64_t read_len = in_stream->Read(data.length(), ret).ValueOrDie();
+ ASSERT_EQ(read_len, data.length());
+ ASSERT_EQ(std::string(ret, read_len), data);
+ ASSERT_EQ(in_stream->Tell().ValueOrDie(), 5);
+
+ ASSERT_TRUE(in_stream->Seek(/*position=*/3).ok());
+ std::shared_ptr<arrow::Buffer> buffer =
in_stream->Read(/*nbytes=*/2).ValueOrDie();
+ ASSERT_EQ(buffer->ToString(), "lo");
+
+ auto fut = in_stream->ReadAsync(arrow::io::default_io_context(),
/*position=*/0, /*nbytes=*/5);
+ auto buffer2 = fut.result().ValueOrDie();
+ ASSERT_EQ(data, buffer2->ToString());
+
+ auto buffer3 = in_stream->ReadAt(/*position=*/1,
/*nbytes=*/2).ValueOrDie();
+ ASSERT_EQ(buffer3->ToString(), "el");
+ int64_t read_len2 = in_stream->ReadAt(/*position=*/4, /*nbytes=*/1,
ret).ValueOrDie();
+ ASSERT_EQ(read_len2, 1);
+ ASSERT_EQ(std::string(ret, read_len2), "o");
+
+ ASSERT_TRUE(in_stream->Close().ok());
+ ASSERT_TRUE(in_stream->closed());
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/common/utils/arrow/arrow_utils.cpp
b/src/paimon/common/utils/arrow/arrow_utils.cpp
new file mode 100644
index 0000000..88623b3
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_utils.cpp
@@ -0,0 +1,179 @@
+/*
+ * 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/util/compression.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/string_utils.h"
+
+namespace paimon {
+Result<std::shared_ptr<arrow::Schema>> ArrowUtils::DataTypeToSchema(
+ const std::shared_ptr<arrow::DataType>& data_type) {
+ if (data_type->id() != arrow::Type::STRUCT) {
+ return Status::Invalid(
+ fmt::format("Expected struct data type, actual data type: {}",
data_type->ToString()));
+ }
+ const auto& struct_type =
std::static_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) {
+ auto struct_array =
arrow::internal::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 =
arrow::internal::checked_cast<arrow::DictionaryArray*>(array.get());
+ [[maybe_unused]] auto dict = dict_array->dictionary();
+ return;
+ }
+ case arrow::Type::type::STRUCT: {
+ auto* struct_array =
arrow::internal::checked_cast<arrow::StructArray*>(array.get());
+ for (const auto& field : struct_array->fields()) {
+ TraverseArray(field);
+ }
+ return;
+ }
+ case arrow::Type::type::MAP: {
+ auto* map_array =
arrow::internal::checked_cast<arrow::MapArray*>(array.get());
+ TraverseArray(map_array->keys());
+ TraverseArray(map_array->items());
+ return;
+ }
+ case arrow::Type::type::LIST: {
+ auto* list_array =
arrow::internal::checked_cast<arrow::ListArray*>(array.get());
+ TraverseArray(list_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;
+ }
+ 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 =
arrow::internal::checked_pointer_cast<arrow::StructType>(field->type());
+ auto struct_array =
arrow::internal::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 =
arrow::internal::checked_pointer_cast<arrow::ListType>(field->type());
+ auto list_array =
arrow::internal::checked_pointer_cast<arrow::ListArray>(data);
+ PAIMON_RETURN_NOT_OK(
+ InnerCheckNullabilityMatch(list_type->value_field(),
list_array->values()));
+ } else if (type->id() == arrow::Type::MAP) {
+ auto map_type =
arrow::internal::checked_pointer_cast<arrow::MapType>(field->type());
+ auto map_array =
arrow::internal::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 =
std::static_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<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
diff --git a/src/paimon/common/utils/arrow/arrow_utils.h
b/src/paimon/common/utils/arrow/arrow_utils.h
new file mode 100644
index 0000000..f6dea3d
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_utils.h
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/util/type_fwd.h"
+#include "fmt/format.h"
+#include "paimon/result.h"
+
+namespace paimon {
+
+class PAIMON_EXPORT ArrowUtils {
+ public:
+ ArrowUtils() = delete;
+ ~ArrowUtils() = delete;
+
+ static Result<std::shared_ptr<arrow::Schema>> DataTypeToSchema(
+ const std::shared_ptr<arrow::DataType>& data_type);
+
+ static Result<std::vector<int32_t>> CreateProjection(
+ const std::shared_ptr<arrow::Schema>& src_schema, const
arrow::FieldVector& target_fields);
+
+ static Status CheckNullabilityMatch(const std::shared_ptr<arrow::Schema>&
schema,
+ const std::shared_ptr<arrow::Array>&
data);
+
+ // For struct array, arrow is unsafe for fields() and field(); for dict
array, arrow is unsafe
+ // for dictionary(). Therefore, access array in advance before merge sort
and projection to
+ // avoid subsequent multi-threading problems.
+ static void TraverseArray(const std::shared_ptr<arrow::Array>& array);
+
+ static Result<std::shared_ptr<arrow::StructArray>>
RemoveFieldFromStructArray(
+ const std::shared_ptr<arrow::StructArray>& struct_array, const
std::string& field_name);
+
+ static bool EqualsIgnoreNullable(const std::shared_ptr<arrow::DataType>&
type,
+ const std::shared_ptr<arrow::DataType>&
other_type);
+
+ /// Normalize and resolve a compression string to an Arrow compression
type.
+ /// Handles "none" and empty string by mapping them to "uncompressed".
+ static Result<arrow::Compression::type> GetCompressionType(const
std::string& compression);
+
+ private:
+ static Status InnerCheckNullabilityMatch(const
std::shared_ptr<arrow::Field>& field,
+ const
std::shared_ptr<arrow::Array>& data);
+};
+
+} // namespace paimon
diff --git a/src/paimon/common/utils/arrow/arrow_utils_test.cpp
b/src/paimon/common/utils/arrow/arrow_utils_test.cpp
new file mode 100644
index 0000000..8908f67
--- /dev/null
+++ b/src/paimon/common/utils/arrow/arrow_utils_test.cpp
@@ -0,0 +1,490 @@
+/*
+ * 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/api.h"
+#include "arrow/ipc/api.h"
+#include "gtest/gtest.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/testing/utils/testharness.h"
+namespace paimon::test {
+
+TEST(ArrowUtilsTest, TestCreateProjection) {
+ arrow::FieldVector file_fields = {
+ arrow::field("k0", arrow::int32()), arrow::field("k1",
arrow::int32()),
+ arrow::field("p1", arrow::int32()), arrow::field("s1",
arrow::utf8()),
+ arrow::field("v0", arrow::float64()), arrow::field("v1",
arrow::boolean()),
+ arrow::field("s0", arrow::utf8())};
+ auto file_schema = arrow::schema(file_fields);
+
+ {
+ // normal case
+ arrow::FieldVector read_fields = {
+ arrow::field("k1", arrow::int32()), arrow::field("p1",
arrow::int32()),
+ arrow::field("s1", arrow::utf8()), arrow::field("v0",
arrow::float64()),
+ arrow::field("v1", arrow::boolean())};
+ auto read_schema = arrow::schema(read_fields);
+ ASSERT_OK_AND_ASSIGN(std::vector<int32_t> projection,
+ ArrowUtils::CreateProjection(file_schema,
read_schema->fields()));
+ std::vector<int32_t> expected_projection = {1, 2, 3, 4, 5};
+ ASSERT_EQ(projection, expected_projection);
+ }
+ {
+ // duplicate read field
+ arrow::FieldVector read_fields = {
+ arrow::field("k1", arrow::int32()), arrow::field("p1",
arrow::int32()),
+ arrow::field("s1", arrow::utf8()), arrow::field("v0",
arrow::float64()),
+ arrow::field("v0", arrow::float64()), arrow::field("v1",
arrow::boolean())};
+ auto read_schema = arrow::schema(read_fields);
+ ASSERT_OK_AND_ASSIGN(std::vector<int32_t> projection,
+ ArrowUtils::CreateProjection(file_schema,
read_schema->fields()));
+ std::vector<int32_t> expected_projection = {1, 2, 3, 4, 4, 5};
+ ASSERT_EQ(projection, expected_projection);
+ }
+ {
+ // duplicate read field, and sizeof(read_fields) > sizeof(file_fields)
+ arrow::FieldVector read_fields = {
+ arrow::field("k1", arrow::int32()), arrow::field("p1",
arrow::int32()),
+ arrow::field("s1", arrow::utf8()), arrow::field("v0",
arrow::float64()),
+ arrow::field("v0", arrow::float64()), arrow::field("v0",
arrow::float64()),
+ arrow::field("v0", arrow::float64()), arrow::field("v0",
arrow::float64()),
+ arrow::field("v1", arrow::boolean())};
+ auto read_schema = arrow::schema(read_fields);
+ ASSERT_OK_AND_ASSIGN(std::vector<int32_t> projection,
+ ArrowUtils::CreateProjection(file_schema,
read_schema->fields()));
+ std::vector<int32_t> expected_projection = {1, 2, 3, 4, 4, 4, 4, 4, 5};
+ ASSERT_EQ(projection, expected_projection);
+ }
+ {
+ // read field not found in src schema
+ arrow::FieldVector read_fields = {
+ arrow::field("k1", arrow::int32()), arrow::field("p1",
arrow::int32()),
+ arrow::field("s1", arrow::utf8()), arrow::field("v2",
arrow::float64()),
+ arrow::field("v1", arrow::boolean())};
+ auto read_schema = arrow::schema(read_fields);
+ ASSERT_NOK_WITH_MSG(ArrowUtils::CreateProjection(file_schema,
read_schema->fields()),
+ "Field 'v2' not found or duplicate in src schema");
+ }
+ {
+ // duplicate field in src schema
+ arrow::FieldVector file_fields_dup = {
+ arrow::field("k0", arrow::int32()), arrow::field("k1",
arrow::int32()),
+ arrow::field("p1", arrow::int32()), arrow::field("s1",
arrow::utf8()),
+ arrow::field("v0", arrow::float64()), arrow::field("v1",
arrow::boolean()),
+ arrow::field("v1", arrow::boolean()), arrow::field("s0",
arrow::utf8())};
+ auto file_schema_dup = arrow::schema(file_fields_dup);
+ arrow::FieldVector read_fields = {
+ arrow::field("k1", arrow::int32()), arrow::field("p1",
arrow::int32()),
+ arrow::field("s1", arrow::utf8()), arrow::field("v1",
arrow::float64()),
+ arrow::field("v1", arrow::boolean())};
+ auto read_schema = arrow::schema(read_fields);
+ ASSERT_NOK_WITH_MSG(ArrowUtils::CreateProjection(file_schema_dup,
read_schema->fields()),
+ "Field 'v1' not found or duplicate in src schema");
+ }
+ {
+ arrow::FieldVector read_fields = {
+ arrow::field("k1", arrow::int32()), arrow::field("p1",
arrow::int32()),
+ arrow::field("s1", arrow::utf8()), arrow::field("v0",
arrow::float64()),
+ arrow::field("v1", arrow::boolean())};
+ auto read_schema = arrow::schema(read_fields);
+ ASSERT_OK_AND_ASSIGN(std::vector<int32_t> projection,
+ ArrowUtils::CreateProjection(file_schema,
read_schema->fields()));
+ std::vector<int32_t> expected_projection = {1, 2, 3, 4, 5};
+ ASSERT_EQ(projection, expected_projection);
+ }
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchSimple) {
+ auto field = arrow::field("column1", arrow::int32(), /*nullable=*/false);
+ auto schema = arrow::schema({field});
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}),
R"([
+ [20],
+ [null],
+ [10]
+])")
+ .ValueOrDie();
+
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field column1 not nullable while
data have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}),
R"([
+ [20],
+ [10]
+])")
+ .ValueOrDie();
+
+ ASSERT_OK(ArrowUtils::CheckNullabilityMatch(schema, array));
+ }
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchWithStruct) {
+ auto child1 = arrow::field("child1", arrow::int32(), /*nullable=*/false);
+ auto child2 = arrow::field("child2", arrow::float64(), /*nullable=*/true);
+ auto struct_field =
+ arrow::field("parent", arrow::struct_({child1, child2}),
/*nullable=*/false);
+ auto schema = arrow::schema({struct_field});
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({struct_field}), R"([
+ [null]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field parent not nullable while
data have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({struct_field}), R"([
+ [[1, null]],
+ [[null, 10.0]]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field child1 not nullable while
data have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({struct_field}), R"([
+ [[1, null]],
+ [[2, 10.0]]
+])")
+ .ValueOrDie();
+ ASSERT_OK(ArrowUtils::CheckNullabilityMatch(schema, array));
+ }
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchWithList) {
+ auto value_field = arrow::field("value", arrow::int32(),
/*nullable=*/false);
+ auto list_field = arrow::field("list_column", arrow::list(value_field),
/*nullable=*/false);
+ auto schema = arrow::schema({list_field});
+
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({list_field}), R"([
+ [[1, 2, null, 4, 5]],
+ [null]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field list_column
not nullable while "
+ "data have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({list_field}), R"([
+ [[1, 2, null, 4, 5]]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field value not nullable while data
have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({list_field}), R"([
+ [[1, 2, 3, 4, 5]]
+])")
+ .ValueOrDie();
+ ASSERT_OK(ArrowUtils::CheckNullabilityMatch(schema, array));
+ }
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchWithMap) {
+ auto key_field = arrow::field("key", arrow::int32(), /*nullable=*/false);
+ auto value_field = arrow::field("value", arrow::int32(),
/*nullable=*/true);
+ auto map_type = std::make_shared<arrow::MapType>(key_field, value_field);
+ auto map_field = arrow::field("map_column", map_type, /*nullable=*/false);
+ auto schema = arrow::schema({map_field});
+
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({map_field}), R"([
+ [null]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field map_column
not nullable while "
+ "data have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({map_field}), R"([
+ [[[1, null]]]
+])")
+ .ValueOrDie();
+ ASSERT_OK(ArrowUtils::CheckNullabilityMatch(schema, array));
+ }
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchComplex) {
+ auto key_field = arrow::field("key", arrow::int32(), /*nullable=*/false);
+ auto value_field = arrow::field("value", arrow::int32(),
/*nullable=*/false);
+
+ auto inner_child1 =
+ arrow::field("inner1",
+ arrow::map(arrow::utf8(), arrow::field("inner_list",
arrow::list(value_field),
+
/*nullable=*/true)),
+ /*nullable=*/false);
+ auto inner_child2 = arrow::field(
+ "inner2",
+ arrow::map(arrow::utf8(), arrow::field("inner_map",
arrow::map(arrow::utf8(), value_field),
+ /*nullable=*/true)),
+ /*nullable=*/false);
+ auto inner_child3 = arrow::field(
+ "inner3",
+ arrow::map(arrow::utf8(),
+ arrow::field("inner_struct", arrow::struct_({key_field,
value_field}),
+ /*nullable=*/true)),
+ /*nullable=*/false);
+
+ auto schema = arrow::schema({inner_child1, inner_child2, inner_child3});
+ // test inner1
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({inner_child1, inner_child2, inner_child3}),
R"([
+[[["outer_key", [1, 2, 3, null]]], [["outer_key", [["key1", 1]]]],
[["outer_key", [100, 200]]]]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field value not nullable while data
have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({inner_child1, inner_child2, inner_child3}),
R"([
+[[["outer_key", [1, 2, 3]]], [["outer_key", [["key1", 1]]]], [["outer_key",
[100, 200]]]],
+[[["outer_key", null]], [["outer_key", [["key1", 1]]]], [["outer_key", [100,
200]]]],
+[null, [["outer_key", [["key1", 1]]]], [["outer_key", [100, 200]]]]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field inner1 not nullable while
data have null value");
+ }
+ // test inner2
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({inner_child1, inner_child2, inner_child3}),
R"([
+[[["outer_key", [1, 2, 3]]], [["outer_key", null]], [["outer_key", [100,
200]]]],
+[[["outer_key", null]], [["outer_key", [["key1", null]]]], [["outer_key",
[100, 200]]]]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field value not nullable while data
have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({inner_child1, inner_child2, inner_child3}),
R"([
+[[["outer_key", [1, 2, 3]]], [["outer_key", null]], [["outer_key", [100,
200]]]],
+[[["outer_key", [1, 2, 3]]], null, [["outer_key", [100, 200]]]]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field inner2 not nullable while
data have null value");
+ }
+ // test inner3
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({inner_child1, inner_child2, inner_child3}),
R"([
+[[["outer_key", [1, 2, 3]]], [["outer_key", null]], [["outer_key", null]]],
+[[["outer_key", null]], [["outer_key", [["key1", 2]]]], [["outer_key", [100,
null]]]]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field value not nullable while data
have null value");
+ }
+ {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({inner_child1, inner_child2, inner_child3}),
R"([
+[[["outer_key", [1, 2, 3]]], [["outer_key", null]], [["outer_key", null]]],
+[[["outer_key", null]], [["outer_key", [["key1", 2]]]], null]
+])")
+ .ValueOrDie();
+ ASSERT_NOK_WITH_MSG(
+ ArrowUtils::CheckNullabilityMatch(schema, array),
+ "CheckNullabilityMatch failed, field inner3 not nullable while
data have null value");
+ }
+}
+
+TEST(ArrowUtilsTest, TestRemoveFieldFromStructArrayFieldNotFound) {
+ auto struct_type =
+ arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("b",
arrow::utf8())});
+ auto src_array = arrow::ipc::internal::json::ArrayFromJSON(
+ struct_type,
R"([{"a":1,"b":"x"},{"a":2,"b":"y"},{"a":3,"b":"z"}])")
+ .ValueOrDie();
+ auto src_struct_array =
std::static_pointer_cast<arrow::StructArray>(src_array);
+
+ ASSERT_OK_AND_ASSIGN(auto result,
+
ArrowUtils::RemoveFieldFromStructArray(src_struct_array, "missing"));
+
+ ASSERT_TRUE(result->Equals(src_struct_array));
+ ASSERT_EQ(result->type()->num_fields(), 2);
+}
+
+TEST(ArrowUtilsTest, TestRemoveFieldFromStructArraySuccess) {
+ auto struct_type =
+ arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("b",
arrow::utf8()),
+ arrow::field("c", arrow::int64())});
+ auto src_array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ struct_type,
+
R"([{"a":1,"b":"x","c":10},{"a":2,"b":"y","c":20},{"a":3,"b":"z","c":30}])")
+ .ValueOrDie();
+ auto src_struct_array =
std::static_pointer_cast<arrow::StructArray>(src_array);
+
+ ASSERT_OK_AND_ASSIGN(auto result,
+
ArrowUtils::RemoveFieldFromStructArray(src_struct_array, "b"));
+
+ auto expected_type =
+ arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("c",
arrow::int64())});
+ auto expected_array = arrow::ipc::internal::json::ArrayFromJSON(
+ expected_type,
R"([{"a":1,"c":10},{"a":2,"c":20},{"a":3,"c":30}])")
+ .ValueOrDie();
+ auto expected_struct_array =
std::static_pointer_cast<arrow::StructArray>(expected_array);
+
+ ASSERT_EQ(result->type()->num_fields(), 2);
+ ASSERT_EQ(result->type()->field(0)->name(), "a");
+ ASSERT_EQ(result->type()->field(1)->name(), "c");
+ ASSERT_TRUE(result->Equals(expected_struct_array));
+}
+
+TEST(ArrowUtilsTest, TestEqualsIgnoreNullable) {
+ {
+ // test simple
+ ASSERT_FALSE(ArrowUtils::EqualsIgnoreNullable(arrow::int32(),
arrow::int64()));
+ ASSERT_TRUE(ArrowUtils::EqualsIgnoreNullable(arrow::int32(),
arrow::int32()));
+ }
+ {
+ // test struct
+ auto child1 = arrow::field("child1", arrow::int32(),
/*nullable=*/false);
+ auto child2 = arrow::field("child2", arrow::int32(),
/*nullable=*/false);
+ auto child3 = arrow::field("child1", arrow::int32(),
/*nullable=*/true);
+ auto struct_type1 = arrow::struct_({child1});
+ auto struct_type2 = arrow::struct_({child2});
+ auto struct_type3 = arrow::struct_({child3});
+ auto struct_type4 = arrow::struct_({child3, child1});
+ ASSERT_FALSE(ArrowUtils::EqualsIgnoreNullable(struct_type1,
struct_type2));
+ ASSERT_TRUE(ArrowUtils::EqualsIgnoreNullable(struct_type1,
struct_type3));
+ ASSERT_FALSE(ArrowUtils::EqualsIgnoreNullable(struct_type1,
struct_type4));
+ }
+ {
+ // test complex
+ auto key_field = arrow::field("key", arrow::int32(),
/*nullable=*/false);
+ auto value_field = arrow::field("value", arrow::int32(),
/*nullable=*/false);
+ auto inner_child1 = arrow::field(
+ "inner1",
+ arrow::map(arrow::utf8(), arrow::field("inner_list",
arrow::list(value_field),
+ /*nullable=*/true)),
+ /*nullable=*/false);
+ auto inner_child2 = arrow::field(
+ "inner2",
+ arrow::map(arrow::utf8(),
+ arrow::field("inner_map", arrow::map(arrow::utf8(),
value_field),
+ /*nullable=*/true)),
+ /*nullable=*/false);
+ auto inner_child3 = arrow::field(
+ "inner3",
+ arrow::map(arrow::utf8(),
+ arrow::field("inner_struct", arrow::struct_({key_field,
value_field}),
+ /*nullable=*/true)),
+ /*nullable=*/false);
+ auto struct_type1 = arrow::struct_({inner_child1, inner_child2,
inner_child3});
+
+ auto key_field_other = arrow::field("key", arrow::int32(),
/*nullable=*/true);
+ auto value_field_other = arrow::field("value", arrow::int32(),
/*nullable=*/true);
+ auto inner_child1_other = arrow::field(
+ "inner1",
+ arrow::map(arrow::utf8(), arrow::field("inner_list",
arrow::list(value_field_other),
+ /*nullable=*/false)),
+ /*nullable=*/true);
+ auto inner_child2_other = arrow::field(
+ "inner2",
+ arrow::map(arrow::utf8(),
+ arrow::field("inner_map", arrow::map(arrow::utf8(),
value_field_other),
+ /*nullable=*/false)),
+ /*nullable=*/true);
+ auto inner_child3_other = arrow::field(
+ "inner3",
+ arrow::map(
+ arrow::utf8(),
+ arrow::field("inner_struct", arrow::struct_({key_field_other,
value_field_other}),
+ /*nullable=*/false)),
+ /*nullable=*/true);
+ auto struct_type2 =
+ arrow::struct_({inner_child1_other, inner_child2_other,
inner_child3_other});
+ ASSERT_TRUE(ArrowUtils::EqualsIgnoreNullable(struct_type1,
struct_type2));
+ }
+}
+
+TEST(ArrowUtilsTest, TestGetCompressionType) {
+ {
+ ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType(""));
+ ASSERT_EQ(type, arrow::Compression::UNCOMPRESSED);
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto type,
ArrowUtils::GetCompressionType("none"));
+ ASSERT_EQ(type, arrow::Compression::UNCOMPRESSED);
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto type,
ArrowUtils::GetCompressionType("uncompressed"));
+ ASSERT_EQ(type, arrow::Compression::UNCOMPRESSED);
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto type,
ArrowUtils::GetCompressionType("zstd"));
+ ASSERT_EQ(type, arrow::Compression::ZSTD);
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto type,
ArrowUtils::GetCompressionType("ZSTD"));
+ ASSERT_EQ(type, arrow::Compression::ZSTD);
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("lz4"));
+ ASSERT_EQ(type, arrow::Compression::LZ4_FRAME);
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto type,
ArrowUtils::GetCompressionType("snappy"));
+ ASSERT_EQ(type, arrow::Compression::SNAPPY);
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto type,
ArrowUtils::GetCompressionType("gzip"));
+ ASSERT_EQ(type, arrow::Compression::GZIP);
+ }
+ {
+ ASSERT_NOK(ArrowUtils::GetCompressionType("invalid_codec"));
+ }
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/common/utils/arrow/mem_utils.cpp
b/src/paimon/common/utils/arrow/mem_utils.cpp
new file mode 100644
index 0000000..4ecb644
--- /dev/null
+++ b/src/paimon/common/utils/arrow/mem_utils.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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/mem_utils.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "arrow/memory_pool.h"
+#include "arrow/status.h"
+#include "paimon/memory/memory_pool.h"
+
+namespace paimon {
+
+class ArrowMemPoolAdaptor : public arrow::MemoryPool {
+ public:
+ explicit ArrowMemPoolAdaptor(const std::shared_ptr<paimon::MemoryPool>&
pool)
+ : pool_(*pool), life_holder_(pool) {}
+
+ arrow::Status Allocate(int64_t size, int64_t alignment, uint8_t** out)
override {
+ *out = reinterpret_cast<uint8_t*>(pool_.Malloc(size, alignment));
+ stats_.DidAllocateBytes(size);
+ return arrow::Status::OK();
+ }
+
+ arrow::Status Reallocate(int64_t old_size, int64_t new_size, int64_t
alignment,
+ uint8_t** ptr) override {
+ *ptr = reinterpret_cast<uint8_t*>(pool_.Realloc(*ptr, old_size,
new_size, alignment));
+ stats_.DidReallocateBytes(old_size, new_size);
+ return arrow::Status::OK();
+ }
+
+ void Free(uint8_t* buffer, int64_t size, int64_t alignment) override {
+ pool_.Free(buffer, size, alignment);
+ stats_.DidFreeBytes(size);
+ }
+
+ int64_t bytes_allocated() const override {
+ return stats_.bytes_allocated();
+ }
+
+ int64_t max_memory() const override {
+ return stats_.max_memory();
+ }
+
+ std::string backend_name() const override {
+ return "Paimon Pool";
+ }
+
+ /// The number of bytes that were allocated.
+ int64_t total_bytes_allocated() const override {
+ return stats_.total_bytes_allocated();
+ }
+
+ /// The number of allocations or reallocations that were requested.
+ int64_t num_allocations() const override {
+ return stats_.num_allocations();
+ }
+
+ private:
+ paimon::MemoryPool& pool_;
+ std::shared_ptr<paimon::MemoryPool> life_holder_;
+ arrow::internal::MemoryPoolStats stats_;
+};
+
+std::unique_ptr<arrow::MemoryPool> GetArrowPool(const
std::shared_ptr<MemoryPool>& pool) {
+ return std::make_unique<ArrowMemPoolAdaptor>(pool);
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/utils/arrow/mem_utils.h
b/src/paimon/common/utils/arrow/mem_utils.h
new file mode 100644
index 0000000..b59c403
--- /dev/null
+++ b/src/paimon/common/utils/arrow/mem_utils.h
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include "arrow/memory_pool.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class MemoryPool;
+
+PAIMON_EXPORT std::unique_ptr<arrow::MemoryPool> GetArrowPool(MemoryPool&
pool);
+
+PAIMON_EXPORT std::unique_ptr<arrow::MemoryPool> GetArrowPool(
+ const std::shared_ptr<MemoryPool>& pool);
+
+} // namespace paimon
diff --git a/src/paimon/common/utils/arrow/mem_utils_test.cpp
b/src/paimon/common/utils/arrow/mem_utils_test.cpp
new file mode 100644
index 0000000..d5d28a7
--- /dev/null
+++ b/src/paimon/common/utils/arrow/mem_utils_test.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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/mem_utils.h"
+
+#include "gtest/gtest.h"
+#include "paimon/memory/memory_pool.h"
+
+namespace paimon::test {
+
+TEST(MemUtilsTest, TestSimple) {
+ const int64_t alignment = 64;
+ auto pool = GetArrowPool(GetDefaultPool());
+ ASSERT_EQ("Paimon Pool", pool->backend_name());
+ ASSERT_EQ(0, pool->total_bytes_allocated());
+ ASSERT_EQ(0, pool->num_allocations());
+
+ uint8_t* ptr1 = nullptr;
+ ASSERT_TRUE(pool->Allocate(10, alignment, &ptr1).ok());
+ ASSERT_TRUE(ptr1);
+ ASSERT_EQ(10, pool->total_bytes_allocated());
+ ASSERT_EQ(10, pool->bytes_allocated());
+ ASSERT_EQ(10, pool->max_memory());
+ ASSERT_EQ(1, pool->num_allocations());
+
+ // test malloc and free
+ uint8_t* ptr2 = nullptr;
+ ASSERT_TRUE(pool->Allocate(20, alignment, &ptr2).ok());
+ ASSERT_TRUE(ptr2);
+ ASSERT_EQ(30, pool->bytes_allocated());
+ ASSERT_EQ(30, pool->max_memory());
+ pool->Free(ptr2, 20, alignment);
+ ASSERT_EQ(10, pool->bytes_allocated());
+ ASSERT_EQ(30, pool->max_memory());
+ ASSERT_EQ(2, pool->num_allocations());
+
+ // test realloc with nullptr
+ uint8_t* ptr3 = nullptr;
+ ASSERT_TRUE(pool->Reallocate(/*old_size=*/0, /*new_size=*/40, alignment,
&ptr3).ok());
+ ASSERT_TRUE(ptr3);
+ ASSERT_EQ(50, pool->bytes_allocated());
+ ASSERT_EQ(50, pool->max_memory());
+ ASSERT_EQ(3, pool->num_allocations());
+
+ uint8_t* ptr3_old = ptr3;
+ // test realloc with same size
+ ASSERT_TRUE(pool->Reallocate(/*old_size=*/40, /*new_size=*/40, alignment,
&ptr3).ok());
+ ASSERT_EQ(ptr3_old, ptr3);
+ ASSERT_EQ(50, pool->bytes_allocated());
+ ASSERT_EQ(50, pool->max_memory());
+ ASSERT_EQ(3, pool->num_allocations());
+
+ pool->Free(ptr1, 10, alignment);
+ pool->Free(ptr3, 40, alignment);
+ ASSERT_EQ(0, pool->bytes_allocated());
+ ASSERT_EQ(70, pool->total_bytes_allocated());
+ ASSERT_EQ(3, pool->num_allocations());
+ ASSERT_EQ(50, pool->max_memory());
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/common/utils/arrow/status_utils_test.cpp
b/src/paimon/common/utils/arrow/status_utils_test.cpp
new file mode 100644
index 0000000..b96c993
--- /dev/null
+++ b/src/paimon/common/utils/arrow/status_utils_test.cpp
@@ -0,0 +1,60 @@
+/*
+ * 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/status_utils.h"
+
+#include "gtest/gtest.h"
+
+namespace paimon::test {
+
+TEST(StatusUtilsTest, ToArrowStatus) {
+ ASSERT_EQ(arrow::Status::OK(), ToArrowStatus(Status::OK()));
+ std::string msg = "msg";
+ ASSERT_EQ(arrow::Status::OutOfMemory(msg),
ToArrowStatus(Status::OutOfMemory(msg)));
+ ASSERT_EQ(arrow::Status::KeyError(msg),
ToArrowStatus(Status::KeyError(msg)));
+ ASSERT_EQ(arrow::Status::TypeError(msg),
ToArrowStatus(Status::TypeError(msg)));
+ ASSERT_EQ(arrow::Status::Invalid(msg),
ToArrowStatus(Status::Invalid(msg)));
+ ASSERT_EQ(arrow::Status::IOError(msg),
ToArrowStatus(Status::IOError(msg)));
+ ASSERT_EQ(arrow::Status::CapacityError(msg),
ToArrowStatus(Status::CapacityError(msg)));
+ ASSERT_EQ(arrow::Status::IndexError(msg),
ToArrowStatus(Status::IndexError(msg)));
+ ASSERT_EQ(arrow::Status::UnknownError(msg),
ToArrowStatus(Status::UnknownError(msg)));
+ ASSERT_EQ(arrow::Status::NotImplemented(msg),
ToArrowStatus(Status::NotImplemented(msg)));
+ ASSERT_EQ(arrow::Status::SerializationError(msg),
+ ToArrowStatus(Status::SerializationError(msg)));
+ ASSERT_EQ(arrow::Status::Invalid(msg),
ToArrowStatus(Status::Invalid(msg)));
+}
+
+TEST(StatusUtilsTest, ToPaimonStatus) {
+ ASSERT_EQ(Status::OK(), ToPaimonStatus(arrow::Status::OK()));
+ std::string msg = "msg";
+ ASSERT_EQ(Status::OutOfMemory(msg),
ToPaimonStatus(arrow::Status::OutOfMemory(msg)));
+ ASSERT_EQ(Status::KeyError(msg),
ToPaimonStatus(arrow::Status::KeyError(msg)));
+ ASSERT_EQ(Status::TypeError(msg),
ToPaimonStatus(arrow::Status::TypeError(msg)));
+ ASSERT_EQ(Status::Invalid(msg),
ToPaimonStatus(arrow::Status::Invalid(msg)));
+ ASSERT_EQ(Status::IOError(msg),
ToPaimonStatus(arrow::Status::IOError(msg)));
+ ASSERT_EQ(Status::CapacityError(msg),
ToPaimonStatus(arrow::Status::CapacityError(msg)));
+ ASSERT_EQ(Status::IndexError(msg),
ToPaimonStatus(arrow::Status::IndexError(msg)));
+ ASSERT_EQ(Status::UnknownError(msg),
ToPaimonStatus(arrow::Status::UnknownError(msg)));
+ ASSERT_EQ(Status::NotImplemented(msg),
ToPaimonStatus(arrow::Status::NotImplemented(msg)));
+ ASSERT_EQ(Status::SerializationError(msg),
+ ToPaimonStatus(arrow::Status::SerializationError(msg)));
+ ASSERT_EQ(Status::Invalid(msg),
ToPaimonStatus(arrow::Status::Invalid(msg)));
+}
+
+} // namespace paimon::test