| /* |
| * 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/blob/blob_writer_builder.h" |
| |
| #include "arrow/api.h" |
| #include "arrow/c/bridge.h" |
| #include "gtest/gtest.h" |
| #include "paimon/common/data/blob_utils.h" |
| #include "paimon/common/utils/arrow/status_utils.h" |
| #include "paimon/data/blob.h" |
| #include "paimon/defs.h" |
| #include "paimon/fs/file_system.h" |
| #include "paimon/fs/local/local_file_system.h" |
| #include "paimon/testing/utils/test_helper.h" |
| #include "paimon/testing/utils/testharness.h" |
| |
| namespace paimon::blob::test { |
| class BlobWriterBuilderTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| dir_ = paimon::test::UniqueTestDirectory::Create(); |
| ASSERT_TRUE(dir_); |
| file_system_ = std::make_shared<LocalFileSystem>(); |
| ASSERT_OK_AND_ASSIGN(output_stream_, |
| file_system_->Create(dir_->Str() + "/file.blob", /*overwrite=*/true)); |
| struct_type_ = arrow::struct_({BlobUtils::ToArrowField("blob_col", true)}); |
| } |
| void TearDown() override {} |
| |
| private: |
| std::unique_ptr<paimon::test::UniqueTestDirectory> dir_; |
| std::shared_ptr<OutputStream> output_stream_; |
| std::shared_ptr<FileSystem> file_system_; |
| std::shared_ptr<arrow::DataType> struct_type_; |
| }; |
| |
| TEST_F(BlobWriterBuilderTest, TestSimple) { |
| BlobWriterBuilder builder(struct_type_, {}); |
| ASSERT_NOK_WITH_MSG(builder.Build(output_stream_, "none"), |
| "File system is nullptr. Please call WithFileSystem() first."); |
| |
| builder.WithFileSystem(file_system_); |
| ASSERT_OK(builder.Build(output_stream_, "none")); |
| } |
| |
| TEST_F(BlobWriterBuilderTest, TestWriteNullOptions) { |
| auto prepare_batch = [&](const std::shared_ptr<Blob>& blob, ArrowArray* c_array) -> Status { |
| PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> array, |
| paimon::test::TestHelper::MakeBlobDescriptorArray(struct_type_, blob, |
| GetDefaultPool())); |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, c_array)); |
| return Status::OK(); |
| }; |
| // Each writer gets its own output file so that a Finish() in one scenario cannot leak |
| // bytes into the next. |
| int32_t file_index = 0; |
| auto build_writer = [&](const std::map<std::string, std::string>& options) |
| -> Result<std::unique_ptr<FormatWriter>> { |
| PAIMON_ASSIGN_OR_RAISE( |
| std::shared_ptr<OutputStream> out, |
| file_system_->Create(dir_->Str() + "/file" + std::to_string(file_index++) + ".blob", |
| /*overwrite=*/true)); |
| BlobWriterBuilder builder(struct_type_, options); |
| builder.WithFileSystem(file_system_); |
| return builder.Build(out, "none"); |
| }; |
| |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Blob> missing_blob, |
| Blob::FromPath(dir_->Str() + "/not_exist_file", /*offset=*/0, |
| /*length=*/10)); |
| std::string data_file = dir_->Str() + "/data_file.bin"; |
| ASSERT_OK_AND_ASSIGN(auto data_file_stream, |
| file_system_->Create(data_file, /*overwrite=*/true)); |
| ASSERT_OK_AND_ASSIGN(int64_t written, data_file_stream->Write("blob data", 9)); |
| ASSERT_EQ(written, 9); |
| ASSERT_OK(data_file_stream->Flush()); |
| ASSERT_OK(data_file_stream->Close()); |
| // The offset beyond the end of the 9-byte file makes the descriptor a fetch failure. |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<Blob> bad_offset_blob, |
| Blob::FromPath(data_file, /*offset=*/100, /*length=*/10)); |
| |
| // Both options default to false. |
| { |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<FormatWriter> writer, build_writer({})); |
| ArrowArray c_array; |
| ASSERT_OK(prepare_batch(missing_blob, &c_array)); |
| ASSERT_NOK_WITH_MSG(writer->AddBatch(&c_array), "not exists"); |
| } |
| { |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<FormatWriter> writer, build_writer({})); |
| ArrowArray c_array; |
| ASSERT_OK(prepare_batch(bad_offset_blob, &c_array)); |
| ASSERT_NOK_WITH_MSG(writer->AddBatch(&c_array), "exceed total length"); |
| } |
| |
| { |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<FormatWriter> writer, |
| build_writer({{Options::BLOB_WRITE_NULL_ON_MISSING_FILE, "true"}})); |
| ArrowArray c_array; |
| ASSERT_OK(prepare_batch(missing_blob, &c_array)); |
| ASSERT_OK(writer->AddBatch(&c_array)); |
| ASSERT_OK(writer->Finish()); |
| } |
| |
| { |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<FormatWriter> writer, |
| build_writer({{Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE, "true"}})); |
| ArrowArray c_array; |
| ASSERT_OK(prepare_batch(bad_offset_blob, &c_array)); |
| ASSERT_OK(writer->AddBatch(&c_array)); |
| ASSERT_OK(writer->Finish()); |
| } |
| |
| // An explicit "false" exercises value parsing, not just the absent-key default. |
| { |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<FormatWriter> writer, |
| build_writer({{Options::BLOB_WRITE_NULL_ON_MISSING_FILE, "false"}, |
| {Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE, "false"}})); |
| ArrowArray c_array; |
| ASSERT_OK(prepare_batch(missing_blob, &c_array)); |
| ASSERT_NOK_WITH_MSG(writer->AddBatch(&c_array), "not exists"); |
| } |
| |
| { |
| ASSERT_NOK_WITH_MSG(build_writer({{Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE, "invalid"}}), |
| "convert key blob-write-null-on-fetch-failure"); |
| } |
| } |
| |
| } // namespace paimon::blob::test |