blob: d699d55518f2f50a8646f338c206f9a4f06d063f [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/data/blob_utils.h"
#include "arrow/api.h"
#include "arrow/c/bridge.h"
#include "gtest/gtest.h"
#include "paimon/catalog/identifier.h"
#include "paimon/common/data/blob_defs.h"
#include "paimon/common/data/blob_descriptor.h"
#include "paimon/common/data/blob_view_struct.h"
#include "paimon/common/types/data_field.h"
#include "paimon/data/blob.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
class BlobUtilsTest : public ::testing::Test {
public:
std::shared_ptr<arrow::KeyValueMetadata> CreateBlobMetadata() {
std::unordered_map<std::string, std::string> blob_metadata_map = {
{BlobDefs::kExtensionTypeKey, BlobDefs::kExtensionTypeValue}};
return std::make_shared<arrow::KeyValueMetadata>(blob_metadata_map);
}
private:
std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
};
TEST_F(BlobUtilsTest, IsBlobMetadata) {
auto correct_metadata = CreateBlobMetadata();
ASSERT_TRUE(BlobUtils::IsBlobMetadata(correct_metadata));
ASSERT_FALSE(BlobUtils::IsBlobMetadata(nullptr));
std::unordered_map<std::string, std::string> wrong_metadata_map = {
{BlobDefs::kExtensionTypeKey, "paimon.type.varchar"}};
auto wrong_metadata = std::make_shared<arrow::KeyValueMetadata>(wrong_metadata_map);
ASSERT_FALSE(BlobUtils::IsBlobMetadata(wrong_metadata));
std::unordered_map<std::string, std::string> no_extension_metadata_map = {
{"other_key", BlobDefs::kExtensionTypeValue}};
auto no_extension_metadata =
std::make_shared<arrow::KeyValueMetadata>(no_extension_metadata_map);
ASSERT_FALSE(BlobUtils::IsBlobMetadata(no_extension_metadata));
}
TEST_F(BlobUtilsTest, IsBlobField) {
std::shared_ptr<arrow::Field> blob_field = BlobUtils::ToArrowField("f1", true);
ASSERT_TRUE(BlobUtils::IsBlobField(blob_field));
auto int_field = arrow::field("i_int", arrow::int32());
ASSERT_FALSE(BlobUtils::IsBlobField(int_field));
auto binary_field_no_meta = arrow::field("b_no_meta", arrow::large_binary());
ASSERT_FALSE(BlobUtils::IsBlobField(binary_field_no_meta));
auto wrong_meta = std::make_shared<arrow::KeyValueMetadata>(
std::unordered_map<std::string, std::string>{{"other_key", "value"}});
auto binary_field_wrong_meta =
arrow::field("b_wrong_meta", arrow::large_binary(), false, wrong_meta);
ASSERT_FALSE(BlobUtils::IsBlobField(binary_field_wrong_meta));
}
TEST_F(BlobUtilsTest, SeparateBlobSchema) {
auto int_field = arrow::field("f1_int", arrow::int32());
auto string_field = arrow::field("f2_string", arrow::utf8());
std::shared_ptr<arrow::Field> blob_field_1 = BlobUtils::ToArrowField("f3_blob_1", true);
{
std::shared_ptr<arrow::Schema> original_schema =
arrow::schema({int_field, string_field, blob_field_1});
BlobUtils::SeparatedSchemas schemas =
BlobUtils::SeparateBlobSchema(original_schema, /*inline_fields=*/{});
std::shared_ptr<arrow::Schema> expected_main_schema =
arrow::schema({int_field, string_field});
ASSERT_TRUE(schemas.main_schema->Equals(*expected_main_schema));
std::shared_ptr<arrow::Schema> expected_blob_schema = arrow::schema({blob_field_1});
ASSERT_TRUE(schemas.blob_schema->Equals(*expected_blob_schema));
}
{
std::shared_ptr<arrow::Schema> no_blob_schema = arrow::schema({int_field, string_field});
BlobUtils::SeparatedSchemas no_blob_schemas =
BlobUtils::SeparateBlobSchema(no_blob_schema, /*inline_fields=*/{});
ASSERT_TRUE(no_blob_schemas.main_schema->Equals(*no_blob_schema));
ASSERT_EQ(no_blob_schemas.blob_schema->num_fields(), 0);
}
{
std::shared_ptr<arrow::Schema> only_blob_schema = arrow::schema({blob_field_1});
BlobUtils::SeparatedSchemas only_blob_schemas =
BlobUtils::SeparateBlobSchema(only_blob_schema, /*inline_fields=*/{});
ASSERT_TRUE(only_blob_schemas.blob_schema->Equals(*only_blob_schema));
ASSERT_EQ(only_blob_schemas.main_schema->num_fields(), 0);
}
{
// Inline blob field stays in main_schema instead of going to blob_schema
auto blob_field_2 = BlobUtils::ToArrowField("f4_blob_2", false);
std::shared_ptr<arrow::Schema> schema =
arrow::schema({int_field, blob_field_1, blob_field_2, string_field});
BlobUtils::SeparatedSchemas schemas =
BlobUtils::SeparateBlobSchema(schema, /*inline_fields=*/{"f3_blob_1"});
// f3_blob_1 is inline -> stays in main; f4_blob_2 goes to blob
std::shared_ptr<arrow::Schema> expected_main =
arrow::schema({int_field, blob_field_1, string_field});
ASSERT_TRUE(schemas.main_schema->Equals(*expected_main));
std::shared_ptr<arrow::Schema> expected_blob = arrow::schema({blob_field_2});
ASSERT_TRUE(schemas.blob_schema->Equals(*expected_blob));
}
{
// All blob fields are inline -> blob_schema is empty
std::shared_ptr<arrow::Schema> schema =
arrow::schema({int_field, blob_field_1, string_field});
BlobUtils::SeparatedSchemas schemas =
BlobUtils::SeparateBlobSchema(schema, /*inline_fields=*/{"f3_blob_1"});
ASSERT_TRUE(schemas.main_schema->Equals(*schema));
ASSERT_EQ(schemas.blob_schema->num_fields(), 0);
}
}
TEST_F(BlobUtilsTest, SeparateBlobArray) {
auto int_field = arrow::field("f1_int", arrow::int32());
std::shared_ptr<arrow::Field> blob_field = BlobUtils::ToArrowField("f2_blob", false);
auto string_field = arrow::field("f3_string", arrow::utf8());
auto schema = arrow::schema({int_field, blob_field, string_field});
arrow::Int32Builder int_builder;
ASSERT_TRUE(int_builder.AppendValues({1, 2, 3}).ok());
auto int_array = int_builder.Finish().ValueOrDie();
arrow::StringBuilder string_builder;
ASSERT_TRUE(string_builder.AppendValues({"a", "b", "c"}).ok());
auto string_array = string_builder.Finish().ValueOrDie();
arrow::LargeBinaryBuilder blob_builder;
ASSERT_TRUE(blob_builder.Append("1", 1).ok());
ASSERT_TRUE(blob_builder.Append("2", 1).ok());
ASSERT_TRUE(blob_builder.Append("3", 1).ok());
auto blob_array_data = blob_builder.Finish().ValueOrDie();
auto raw_struct_array =
arrow::StructArray::Make({int_array, blob_array_data, string_array}, schema->fields())
.ValueOrDie();
std::shared_ptr<arrow::StructArray> struct_array =
std::static_pointer_cast<arrow::StructArray>(raw_struct_array);
ASSERT_OK_AND_ASSIGN(auto separated,
BlobUtils::SeparateBlobArray(struct_array, /*inline_fields=*/{}));
std::shared_ptr<arrow::DataType> expected_main_type = arrow::struct_({int_field, string_field});
ASSERT_TRUE(separated.main_array->type()->Equals(*expected_main_type));
ASSERT_EQ(separated.main_array->num_fields(), 2);
ASSERT_TRUE(separated.main_array->field(0)->Equals(*int_array));
ASSERT_TRUE(separated.main_array->field(1)->Equals(*string_array));
std::shared_ptr<arrow::DataType> expected_blob_type = arrow::struct_({blob_field});
ASSERT_TRUE(separated.blob_array->type()->Equals(*expected_blob_type));
ASSERT_EQ(separated.blob_array->num_fields(), 1);
ASSERT_TRUE(separated.blob_array->field(0)->Equals(*blob_array_data));
// All blob fields are inline -> should return error (no blob field to separate)
ASSERT_NOK_WITH_MSG(
BlobUtils::SeparateBlobArray(struct_array, /*inline_fields=*/{"f2_blob"}),
"SeparateBlobArray expects at least one non-inline blob field, but got none.");
// All fields are blob with no inline -> no main array is needed
auto all_blob_struct = arrow::StructArray::Make({blob_array_data}, {blob_field}).ValueOrDie();
auto all_blob_sa = std::dynamic_pointer_cast<arrow::StructArray>(all_blob_struct);
ASSERT_OK_AND_ASSIGN(auto all_blob_separated,
BlobUtils::SeparateBlobArray(all_blob_sa, /*inline_fields=*/{}));
ASSERT_EQ(nullptr, all_blob_separated.main_array);
ASSERT_TRUE(all_blob_separated.blob_array->Equals(*all_blob_sa));
}
TEST_F(BlobUtilsTest, SeparateBlobArrayWithPartialInline) {
auto int_field = arrow::field("f1_int", arrow::int32());
std::shared_ptr<arrow::Field> blob_field_1 = BlobUtils::ToArrowField("f2_blob_1", false);
std::shared_ptr<arrow::Field> blob_field_2 = BlobUtils::ToArrowField("f3_blob_2", true);
auto schema = arrow::schema({int_field, blob_field_1, blob_field_2});
arrow::Int32Builder int_builder;
ASSERT_TRUE(int_builder.AppendValues({1, 2}).ok());
auto int_array = int_builder.Finish().ValueOrDie();
arrow::LargeBinaryBuilder blob_builder_1;
ASSERT_TRUE(blob_builder_1.Append("a", 1).ok());
ASSERT_TRUE(blob_builder_1.Append("b", 1).ok());
auto blob_array_1 = blob_builder_1.Finish().ValueOrDie();
arrow::LargeBinaryBuilder blob_builder_2;
ASSERT_TRUE(blob_builder_2.Append("x", 1).ok());
ASSERT_TRUE(blob_builder_2.AppendNull().ok());
auto blob_array_2 = blob_builder_2.Finish().ValueOrDie();
auto raw_struct_array =
arrow::StructArray::Make({int_array, blob_array_1, blob_array_2}, schema->fields())
.ValueOrDie();
auto struct_array = std::static_pointer_cast<arrow::StructArray>(raw_struct_array);
// f2_blob_1 is inline, f3_blob_2 goes to blob
ASSERT_OK_AND_ASSIGN(auto separated, BlobUtils::SeparateBlobArray(
struct_array, /*inline_fields=*/{"f2_blob_1"}));
std::shared_ptr<arrow::DataType> expected_main_type = arrow::struct_({int_field, blob_field_1});
ASSERT_TRUE(separated.main_array->type()->Equals(*expected_main_type));
ASSERT_EQ(separated.main_array->num_fields(), 2);
ASSERT_TRUE(separated.main_array->field(0)->Equals(*int_array));
ASSERT_TRUE(separated.main_array->field(1)->Equals(*blob_array_1));
std::shared_ptr<arrow::DataType> expected_blob_type = arrow::struct_({blob_field_2});
ASSERT_TRUE(separated.blob_array->type()->Equals(*expected_blob_type));
ASSERT_EQ(separated.blob_array->num_fields(), 1);
ASSERT_TRUE(separated.blob_array->field(0)->Equals(*blob_array_2));
}
TEST_F(BlobUtilsTest, ValidateInlineBlobDescriptorsEmptyFields) {
// Empty inline_descriptor_fields -> always OK
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append("random_data").ok());
auto array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({array}, {BlobUtils::ToArrowField("b0")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {}, "blob-descriptor-field"));
}
TEST_F(BlobUtilsTest, ValidateInlineBlobDescriptorsFieldNotPresent) {
// Field not in struct_array -> skip, OK
arrow::Int32Builder int_builder;
ASSERT_TRUE(int_builder.Append(42).ok());
auto int_array = int_builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({int_array}, {arrow::field("f0", arrow::int32())}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
// "b0" does not exist in the struct -> should pass
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {"b0"}, "blob-descriptor-field"));
}
TEST_F(BlobUtilsTest, ValidateInlineBlobDescriptorsWithValidDescriptor) {
// Valid BlobDescriptor bytes -> OK
ASSERT_OK_AND_ASSIGN(auto descriptor, BlobDescriptor::Create("file:///tmp/test.bin", 0, 100));
auto serialized = descriptor->Serialize(pool_);
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append(serialized->data(), serialized->size()).ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("b0")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {"b0"}, "blob-descriptor-field"));
}
TEST_F(BlobUtilsTest, ValidateInlineBlobDescriptorsWithNullValue) {
// Null values in blob column -> skip, OK
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.AppendNull().ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("b0")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {"b0"}, "blob-descriptor-field"));
}
TEST_F(BlobUtilsTest, ValidateInlineBlobDescriptorsWithRawBytes) {
// Raw bytes (not a descriptor) -> error
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append("not_a_descriptor_just_raw_data").ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("b0")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_NOK_WITH_MSG(BlobUtils::ValidateBlobInlineFields(sa, {"b0"}, "blob-descriptor-field"),
"BLOB inline field b0 require values to be set as corresponding type.");
}
TEST_F(BlobUtilsTest, ValidateInlineBlobDescriptorsMixedValidAndInvalid) {
// First row is valid descriptor, second row is raw bytes -> error on row 1
ASSERT_OK_AND_ASSIGN(auto descriptor, BlobDescriptor::Create("file:///tmp/test.bin", 0, 100));
auto serialized = descriptor->Serialize(pool_);
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append(serialized->data(), serialized->size()).ok());
ASSERT_TRUE(builder.Append("raw_bytes_not_descriptor").ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("b0")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_NOK_WITH_MSG(BlobUtils::ValidateBlobInlineFields(sa, {"b0"}, "blob-descriptor-field"),
"BLOB inline field b0 require values to be set as corresponding type.");
}
TEST_F(BlobUtilsTest, ValidateInlineBlobDescriptorsMultipleFields) {
// Two inline fields: b0 is valid, b1 has raw bytes -> error on b1
ASSERT_OK_AND_ASSIGN(auto descriptor, BlobDescriptor::Create("file:///tmp/test.bin", 0, 100));
auto serialized = descriptor->Serialize(pool_);
arrow::LargeBinaryBuilder b0_builder;
ASSERT_TRUE(b0_builder.Append(serialized->data(), serialized->size()).ok());
auto b0_array = b0_builder.Finish().ValueOrDie();
arrow::LargeBinaryBuilder b1_builder;
ASSERT_TRUE(b1_builder.Append("invalid_raw_data").ok());
auto b1_array = b1_builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({b0_array, b1_array},
{BlobUtils::ToArrowField("b0"), BlobUtils::ToArrowField("b1")})
.ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_NOK_WITH_MSG(
BlobUtils::ValidateBlobInlineFields(sa, {"b0", "b1"}, "blob-descriptor-field"),
"BLOB inline field b1 require values to be set as corresponding type.");
}
TEST_F(BlobUtilsTest, ValidateBlobViewFieldsEmptyFields) {
// Empty view_fields -> always OK
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append("random_data").ok());
auto array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({array}, {BlobUtils::ToArrowField("view")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {}, "blob-view-field"));
}
TEST_F(BlobUtilsTest, ValidateBlobViewFieldsFieldNotPresent) {
// Field not in struct_array -> skip, OK
arrow::Int32Builder int_builder;
ASSERT_TRUE(int_builder.Append(42).ok());
auto int_array = int_builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({int_array}, {arrow::field("f0", arrow::int32())}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {"view"}, "blob-view-field"));
}
TEST_F(BlobUtilsTest, ValidateBlobViewFieldsWithValidViewStruct) {
// A BlobViewStruct value is accepted for a view field.
BlobViewStruct view_struct(Identifier("db", "tbl"), /*field_id=*/2, /*row_id=*/5);
auto serialized = view_struct.Serialize(pool_);
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append(serialized->data(), serialized->size()).ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("view")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {"view"}, "blob-view-field"));
}
TEST_F(BlobUtilsTest, ValidateBlobViewFieldsWithNullValue) {
// Null values in view column -> skip, OK
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.AppendNull().ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("view")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_OK(BlobUtils::ValidateBlobInlineFields(sa, {"view"}, "blob-view-field"));
}
TEST_F(BlobUtilsTest, ValidateBlobViewFieldsWithRawBytes) {
// Raw bytes -> error
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append("raw_bytes_not_view").ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("view")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_NOK_WITH_MSG(BlobUtils::ValidateBlobInlineFields(sa, {"view"}, "blob-view-field"),
"BLOB inline field view require values to be set as corresponding type.");
}
TEST_F(BlobUtilsTest, ValidateBlobViewFieldsRejectsBlobDescriptor) {
// A BlobDescriptor value is NOT accepted for a view field.
auto pool = GetDefaultPool();
ASSERT_OK_AND_ASSIGN(auto descriptor, BlobDescriptor::Create("file:///tmp/test.bin", 0, 100));
auto serialized = descriptor->Serialize(pool);
arrow::LargeBinaryBuilder builder;
ASSERT_TRUE(builder.Append(serialized->data(), serialized->size()).ok());
auto blob_array = builder.Finish().ValueOrDie();
auto struct_array =
arrow::StructArray::Make({blob_array}, {BlobUtils::ToArrowField("view")}).ValueOrDie();
auto sa = std::dynamic_pointer_cast<arrow::StructArray>(struct_array);
ASSERT_NOK_WITH_MSG(BlobUtils::ValidateBlobInlineFields(sa, {"view"}, "blob-view-field"),
"BLOB inline field view require values to be set as corresponding type.");
}
TEST_F(BlobUtilsTest, TestConvertBlobInlineDataFields) {
// Schema with a blob field (large_binary with blob metadata) and normal fields.
auto blob_field = BlobUtils::ToArrowField("blob_col", /*nullable=*/true);
std::vector<DataField> data_fields = {DataField(0, arrow::field("int_col", arrow::int32())),
DataField(1, blob_field),
DataField(2, arrow::field("str_col", arrow::utf8()))};
// Without inline fields — blob_col stays as large_binary
{
auto result = BlobUtils::ConvertBlobInlineDataFields(data_fields, {});
ASSERT_EQ(result.size(), 3);
ASSERT_EQ(result[1].ArrowField()->type()->id(), arrow::Type::LARGE_BINARY);
}
// With inline fields — blob_col should be converted from large_binary to binary
{
auto result = BlobUtils::ConvertBlobInlineDataFields(data_fields, {"blob_col"});
ASSERT_EQ(result.size(), 3);
ASSERT_EQ(result[1].ArrowField()->type()->id(), arrow::Type::BINARY);
ASSERT_EQ(result[1].Name(), "blob_col");
ASSERT_EQ(result[1].Nullable(), true);
// Other fields unchanged
ASSERT_EQ(result[0].ArrowField()->type()->id(), arrow::Type::INT32);
ASSERT_EQ(result[2].ArrowField()->type()->id(), arrow::Type::STRING);
}
// Non-matching inline field name — no conversion should happen
{
auto result = BlobUtils::ConvertBlobInlineDataFields(data_fields, {"non_existent_field"});
ASSERT_EQ(result[1].ArrowField()->type()->id(), arrow::Type::LARGE_BINARY);
}
}
} // namespace paimon::test