| /* | |
| * 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/data/blob.h" | |
| #include "arrow/api.h" | |
| #include "arrow/c/bridge.h" | |
| #include "gtest/gtest.h" | |
| #include "paimon/fs/local/local_file_system.h" | |
| #include "paimon/memory/memory_pool.h" | |
| #include "paimon/testing/utils/testharness.h" | |
| namespace paimon::test { | |
| class BlobTest : public ::testing::Test { | |
| public: | |
| void SetUp() override { | |
| pool_ = GetDefaultPool(); | |
| dir_ = UniqueTestDirectory::Create(); | |
| ASSERT_TRUE(dir_); | |
| uri_ = dir_->Str() + "/file.blob"; | |
| file_system_ = std::make_shared<LocalFileSystem>(); | |
| ASSERT_OK(file_system_->WriteFile(uri_, "abcdefghijklmn", true)); | |
| } | |
| void TearDown() override {} | |
| private: | |
| std::shared_ptr<MemoryPool> pool_; | |
| std::unique_ptr<UniqueTestDirectory> dir_; | |
| std::shared_ptr<FileSystem> file_system_; | |
| std::string uri_; | |
| }; | |
| TEST_F(BlobTest, TestSimple) { | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<Blob> blob, Blob::FromPath(uri_)); | |
| auto serialized = blob->ToDescriptor(pool_); | |
| ASSERT_OK_AND_ASSIGN(auto restored_blob, | |
| Blob::FromDescriptor(serialized->data(), serialized->size())); | |
| ASSERT_EQ(*restored_blob->ToDescriptor(pool_), *serialized); | |
| ASSERT_OK_AND_ASSIGN(auto input_stream, restored_blob->NewInputStream(file_system_)); | |
| ASSERT_EQ(uri_, restored_blob->Uri()); | |
| std::string str(14, '\0'); | |
| ASSERT_OK(input_stream->Read(str.data(), str.size())); | |
| ASSERT_EQ("abcdefghijklmn", str); | |
| } | |
| TEST_F(BlobTest, TestInvalidParameters) { | |
| // Test null file system in NewInputStream | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<Blob> blob, Blob::FromPath(uri_)); | |
| ASSERT_NOK_WITH_MSG(blob->NewInputStream(nullptr), "file system is nullptr"); | |
| // Test invalid descriptor | |
| std::string invalid_bytes = "invalid_descriptor_bytes"; | |
| ASSERT_NOK(Blob::FromDescriptor(invalid_bytes.data(), invalid_bytes.size())); | |
| } | |
| TEST_F(BlobTest, TestRoundTripConsistency) { | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<Blob> original_blob, | |
| Blob::FromPath(uri_, /*offset=*/1, /*length=*/5)); | |
| auto first_descriptor = original_blob->ToDescriptor(pool_); | |
| ASSERT_OK_AND_ASSIGN(auto restored_blob, | |
| Blob::FromDescriptor(first_descriptor->data(), first_descriptor->size())); | |
| auto second_descriptor = restored_blob->ToDescriptor(pool_); | |
| ASSERT_EQ(*first_descriptor, *second_descriptor); | |
| // Verify scheme consistency | |
| ASSERT_EQ(original_blob->Uri(), restored_blob->Uri()); | |
| } | |
| TEST_F(BlobTest, TestInputStreamCreation) { | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<Blob> blob, Blob::FromPath(uri_)); | |
| ASSERT_OK_AND_ASSIGN(auto input_stream, blob->NewInputStream(file_system_)); | |
| ASSERT_TRUE(input_stream); | |
| // Test reading from the input stream | |
| std::string buffer(14, '\0'); | |
| ASSERT_OK_AND_ASSIGN(auto bytes_read, input_stream->Read(buffer.data(), buffer.size())); | |
| ASSERT_EQ(14, bytes_read); | |
| ASSERT_EQ("abcdefghijklmn", buffer); | |
| } | |
| TEST_F(BlobTest, TestBoundaryConditions) { | |
| // Test with empty path | |
| ASSERT_NOK_WITH_MSG(Blob::FromPath(""), "path is an empty string"); | |
| // Test with very large offset and length values | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<Blob> blob, | |
| Blob::FromPath(uri_, /*offset=*/INT64_MAX, /*length=*/1)); | |
| auto serialized = blob->ToDescriptor(pool_); | |
| ASSERT_OK_AND_ASSIGN(auto restored_blob, | |
| Blob::FromDescriptor(serialized->data(), serialized->size())); | |
| ASSERT_EQ(*restored_blob->ToDescriptor(pool_), *serialized); | |
| } | |
| TEST_F(BlobTest, TestNewInputStreamWithOffsetAndLength) { | |
| // Create blob with offset=2, length=6 (should read "cdefgh") | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<Blob> blob, | |
| Blob::FromPath(uri_, /*offset=*/2, /*length=*/6)); | |
| ASSERT_OK_AND_ASSIGN(auto input_stream, blob->NewInputStream(file_system_)); | |
| ASSERT_TRUE(input_stream); | |
| ASSERT_OK_AND_ASSIGN(int64_t length, input_stream->Length()); | |
| ASSERT_EQ(6, length); | |
| // Test reading with offset and length applied | |
| std::string buffer(6, '\0'); | |
| ASSERT_OK_AND_ASSIGN(auto bytes_read, input_stream->Read(buffer.data(), buffer.size())); | |
| ASSERT_EQ(6, bytes_read); | |
| ASSERT_EQ("cdefgh", buffer); | |
| } | |
| TEST_F(BlobTest, TestNewInputStreamWithDynamicLength) { | |
| // Create blob with offset=2, dynamic length (-1 means read to end) | |
| ASSERT_OK_AND_ASSIGN(std::unique_ptr<Blob> blob, | |
| Blob::FromPath(uri_, /*offset=*/2, /*length=*/-1)); | |
| ASSERT_OK_AND_ASSIGN(auto input_stream, blob->NewInputStream(file_system_)); | |
| ASSERT_TRUE(input_stream); | |
| ASSERT_OK_AND_ASSIGN(int64_t length, input_stream->Length()); | |
| ASSERT_EQ(12, length); | |
| // Test reading from offset to end (should read "cdefghijklmn") | |
| std::string buffer(12, '\0'); | |
| ASSERT_OK_AND_ASSIGN(auto bytes_read, input_stream->Read(buffer.data(), buffer.size())); | |
| ASSERT_EQ(12, bytes_read); | |
| ASSERT_EQ("cdefghijklmn", buffer); | |
| } | |
| TEST_F(BlobTest, TestArrowField) { | |
| for (bool nullable : {false, true}) { | |
| ASSERT_OK_AND_ASSIGN(auto schema, Blob::ArrowField("my_blob", nullable)); | |
| ASSERT_NE(schema, nullptr); | |
| auto field_result = arrow::ImportField(schema.get()); | |
| ASSERT_TRUE(field_result.ok()); | |
| auto field = field_result.ValueUnsafe(); | |
| ASSERT_EQ(field->name(), "my_blob"); | |
| ASSERT_EQ(field->type()->id(), arrow::Type::LARGE_BINARY); | |
| ASSERT_EQ(field->nullable(), nullable); | |
| ASSERT_TRUE(field->HasMetadata()); | |
| auto extension_type = field->metadata()->Get("paimon.extension.type"); | |
| ASSERT_TRUE(extension_type.ok()); | |
| ASSERT_EQ(extension_type.ValueUnsafe(), "paimon.type.blob"); | |
| } | |
| { | |
| std::unordered_map<std::string, std::string> custom_metadata = { | |
| {"custom_key", "custom_value"}}; | |
| ASSERT_OK_AND_ASSIGN(auto schema, | |
| Blob::ArrowField("meta_blob", /*nullable=*/false, custom_metadata)); | |
| auto field = arrow::ImportField(schema.get()).ValueUnsafe(); | |
| ASSERT_EQ(field->name(), "meta_blob"); | |
| ASSERT_FALSE(field->nullable()); | |
| ASSERT_TRUE(field->HasMetadata()); | |
| auto extension_type = field->metadata()->Get("paimon.extension.type"); | |
| ASSERT_TRUE(extension_type.ok()); | |
| ASSERT_EQ(extension_type.ValueUnsafe(), "paimon.type.blob"); | |
| auto custom_val = field->metadata()->Get("custom_key"); | |
| ASSERT_TRUE(custom_val.ok()); | |
| ASSERT_EQ(custom_val.ValueUnsafe(), "custom_value"); | |
| } | |
| } | |
| } // namespace paimon::test |