blob: 85467e48fd33c607930cee73f41101e5ce628da1 [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/shredding/map_shared_shredding_utils.h"
#include <set>
#include "arrow/type.h"
#include "arrow/util/key_value_metadata.h"
#include "gtest/gtest.h"
#include "paimon/core/core_options.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
// ---- IsShreddingKeyMap ----
TEST(MapSharedShreddingUtilsTest, IsShreddingKeyMap) {
ASSERT_TRUE(
MapSharedShreddingUtils::IsShreddingKeyMap(arrow::map(arrow::utf8(), arrow::int32())));
ASSERT_TRUE(
MapSharedShreddingUtils::IsShreddingKeyMap(arrow::map(arrow::utf8(), arrow::float64())));
// Nested value type (struct)
auto nested_value =
arrow::struct_({arrow::field("x", arrow::int32()), arrow::field("y", arrow::utf8())});
ASSERT_TRUE(
MapSharedShreddingUtils::IsShreddingKeyMap(arrow::map(arrow::utf8(), nested_value)));
ASSERT_FALSE(
MapSharedShreddingUtils::IsShreddingKeyMap(arrow::map(arrow::int32(), arrow::utf8())));
ASSERT_FALSE(MapSharedShreddingUtils::IsShreddingKeyMap(arrow::int32()));
ASSERT_FALSE(MapSharedShreddingUtils::IsShreddingKeyMap(arrow::list(arrow::utf8())));
}
// ---- DetectShreddingColumns ----
TEST(MapSharedShreddingUtilsTest, DetectShreddingColumnsBasic) {
auto schema = arrow::schema({
arrow::field("id", arrow::int32()),
arrow::field("tags", arrow::map(arrow::utf8(), arrow::utf8())),
arrow::field("metrics", arrow::map(arrow::utf8(), arrow::float64())),
arrow::field("name", arrow::utf8()),
});
ASSERT_OK_AND_ASSIGN(
CoreOptions options,
CoreOptions::FromMap({{"fields.tags.map.storage-layout", "shared-shredding"},
{"fields.metrics.map.storage-layout", "shared-shredding"}}));
ASSERT_OK_AND_ASSIGN(auto field_names,
MapSharedShreddingUtils::DetectShreddingColumns(schema, options));
ASSERT_EQ(field_names.size(), 2);
ASSERT_EQ(field_names[0], "tags");
ASSERT_EQ(field_names[1], "metrics");
}
TEST(MapSharedShreddingUtilsTest, DetectShreddingColumnsNoShredding) {
auto schema = arrow::schema({
arrow::field("id", arrow::int32()),
arrow::field("tags", arrow::map(arrow::utf8(), arrow::utf8())),
});
ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap({}));
ASSERT_OK_AND_ASSIGN(auto field_names,
MapSharedShreddingUtils::DetectShreddingColumns(schema, options));
ASSERT_TRUE(field_names.empty());
}
// ---- LogicalToPhysicalSchema ----
TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaBasic) {
auto schema = arrow::schema({
arrow::field("id", arrow::int32()),
arrow::field("tags", arrow::map(arrow::utf8(), arrow::utf8())),
arrow::field("name", arrow::utf8()),
});
std::map<std::string, int32_t> field_to_num_columns = {{"tags", 4}};
ASSERT_OK_AND_ASSIGN(auto physical_schema, MapSharedShreddingUtils::LogicalToPhysicalSchema(
schema, field_to_num_columns));
// Build expected schema for comparison
auto expected_struct = arrow::struct_({
arrow::field("__field_mapping", arrow::list(arrow::int32()), true),
arrow::field("__col_0", arrow::utf8(), true),
arrow::field("__col_1", arrow::utf8(), true),
arrow::field("__col_2", arrow::utf8(), true),
arrow::field("__col_3", arrow::utf8(), true),
arrow::field("__overflow", arrow::map(arrow::int32(), arrow::utf8()), true),
});
auto expected_schema = arrow::schema({
arrow::field("id", arrow::int32()),
arrow::field("tags", expected_struct, true),
arrow::field("name", arrow::utf8()),
});
ASSERT_TRUE(physical_schema->Equals(expected_schema));
}
TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaNestedValue) {
// MAP<STRING, STRUCT<a: int32, b: utf8>>
auto nested_value =
arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("b", arrow::utf8())});
auto map_type = arrow::map(arrow::utf8(), nested_value);
auto schema = arrow::schema({arrow::field("data", map_type)});
std::map<std::string, int32_t> field_to_num_columns = {{"data", 2}};
ASSERT_OK_AND_ASSIGN(auto physical_schema, MapSharedShreddingUtils::LogicalToPhysicalSchema(
schema, field_to_num_columns));
auto expected_struct = arrow::struct_({
arrow::field("__field_mapping", arrow::list(arrow::int32()), true),
arrow::field("__col_0", nested_value, true),
arrow::field("__col_1", nested_value, true),
arrow::field("__overflow", arrow::map(arrow::int32(), nested_value), true),
});
auto expected_schema = arrow::schema({arrow::field("data", expected_struct, true)});
ASSERT_TRUE(physical_schema->Equals(expected_schema));
}
TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaNullable) {
// MAP value is nullable
auto nullable_map = arrow::map(arrow::utf8(), arrow::field("item", arrow::int64(), true));
auto schema_nullable = arrow::schema({arrow::field("m", nullable_map)});
std::map<std::string, int32_t> col_map = {{"m", 2}};
ASSERT_OK_AND_ASSIGN(
auto physical, MapSharedShreddingUtils::LogicalToPhysicalSchema(schema_nullable, col_map));
auto struct_type = physical->field(0)->type();
ASSERT_TRUE(struct_type->field(0)->nullable());
ASSERT_TRUE(struct_type->field(1)->nullable());
ASSERT_TRUE(struct_type->field(2)->nullable());
// MAP value is non-nullable
auto non_nullable_map = arrow::map(arrow::utf8(), arrow::field("item", arrow::int64(), false));
auto schema_non_nullable = arrow::schema({arrow::field("m", non_nullable_map)});
ASSERT_OK_AND_ASSIGN(auto physical2, MapSharedShreddingUtils::LogicalToPhysicalSchema(
schema_non_nullable, col_map));
auto struct_type2 = physical2->field(0)->type();
ASSERT_FALSE(struct_type2->field(1)->nullable());
ASSERT_FALSE(struct_type2->field(2)->nullable());
}
TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaPreservesFieldMetadata) {
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append("paimon.field.id", "7");
metadata->Append("description", "original map field");
auto map_type = arrow::map(arrow::utf8(), arrow::int64());
auto schema = arrow::schema({arrow::field("m", map_type, false, metadata)});
std::map<std::string, int32_t> col_map = {{"m", 2}};
ASSERT_OK_AND_ASSIGN(auto physical_schema,
MapSharedShreddingUtils::LogicalToPhysicalSchema(schema, col_map));
ASSERT_FALSE(physical_schema->field(0)->nullable());
ASSERT_TRUE(physical_schema->field(0)->metadata()->Equals(*metadata));
}
TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaNoShreddingColumns) {
auto schema = arrow::schema({
arrow::field("id", arrow::int32()),
arrow::field("name", arrow::utf8()),
});
std::map<std::string, int32_t> empty_map;
ASSERT_OK_AND_ASSIGN(auto physical_schema,
MapSharedShreddingUtils::LogicalToPhysicalSchema(schema, empty_map));
ASSERT_TRUE(physical_schema->Equals(schema));
}
TEST(MapSharedShreddingUtilsTest, BuildSpecificPhysicalStructTypeWithOverflow) {
auto actual = MapSharedShreddingUtils::BuildSpecificPhysicalStructType(
arrow::int64(), /*physical_col_ids=*/{3, 1}, /*value_nullable=*/false,
/*include_overflow=*/true);
auto expected = arrow::struct_({
arrow::field("__field_mapping", arrow::list(arrow::int32()), true),
arrow::field("__col_1", arrow::int64(), false),
arrow::field("__col_3", arrow::int64(), false),
arrow::field("__overflow",
arrow::map(arrow::int32(), arrow::field("value", arrow::int64(), false)),
true),
});
ASSERT_TRUE(actual->Equals(*expected)) << "Expected:\n"
<< expected->ToString() << "\nActual:\n"
<< actual->ToString();
}
TEST(MapSharedShreddingUtilsTest, BuildSpecificPhysicalStructTypeWithoutOverflow) {
auto actual = MapSharedShreddingUtils::BuildSpecificPhysicalStructType(
arrow::utf8(), /*physical_col_ids=*/{3}, /*value_nullable=*/true,
/*include_overflow=*/false);
auto expected = arrow::struct_({
arrow::field("__field_mapping", arrow::list(arrow::int32()), true),
arrow::field("__col_3", arrow::utf8(), true),
});
ASSERT_TRUE(actual->Equals(*expected)) << "Expected:\n"
<< expected->ToString() << "\nActual:\n"
<< actual->ToString();
}
// ---- BuildColumnToNumColumns ----
TEST(MapSharedShreddingUtilsTest, BuildColumnToNumColumns) {
ASSERT_OK_AND_ASSIGN(
CoreOptions options,
CoreOptions::FromMap({{"fields.tags.map.shared-shredding.max-columns", "128"},
{"fields.metrics.map.shared-shredding.max-columns", "64"}}));
std::vector<std::string> shredding_field_names = {"tags", "metrics"};
ASSERT_OK_AND_ASSIGN(auto result, MapSharedShreddingUtils::BuildColumnToNumColumns(
shredding_field_names, options));
ASSERT_EQ(result.size(), 2);
ASSERT_EQ(result.at("tags"), 128);
ASSERT_EQ(result.at("metrics"), 64);
}
TEST(MapSharedShreddingUtilsTest, BuildColumnToNumColumnsDefault) {
// No explicit max-columns config -> default 256
ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap({}));
std::vector<std::string> shredding_field_names = {"tags"};
ASSERT_OK_AND_ASSIGN(auto result, MapSharedShreddingUtils::BuildColumnToNumColumns(
shredding_field_names, options));
ASSERT_EQ(result.at("tags"), 256);
}
// ---- SerializeMetadata / DeserializeMetadata roundtrip ----
TEST(MapSharedShreddingUtilsTest, MetadataRoundtripNoneCompression) {
MapSharedShreddingFieldMeta original;
original.name_to_id = {{"age", 0}, {"name", 1}};
original.field_to_columns = {{0, {0}}, {1, {1, 2}}};
original.overflow_field_set = {1, 5};
original.num_columns = 3;
original.max_row_width = 2;
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
ASSERT_OK(MapSharedShreddingUtils::SerializeMetadata(original, "none", metadata.get()));
// Verify raw KV strings
auto find_value = [&](const char* key) -> std::string {
int32_t idx = metadata->FindKey(key);
EXPECT_GE(idx, 0);
return metadata->value(idx);
};
ASSERT_EQ(find_value(MapShreddingDefine::kStorageLayout), "shared-shredding");
ASSERT_EQ(find_value(MapSharedShreddingDefine::kVersion), "1");
ASSERT_EQ(find_value(MapSharedShreddingDefine::kNumColumns), "3");
ASSERT_EQ(find_value(MapSharedShreddingDefine::kMaxRowWidth), "2");
std::string expected_dict = R"({"age":0,"name":1})";
ASSERT_EQ(find_value(MapSharedShreddingDefine::kFieldDict), expected_dict);
// field_dict_original_size should be the length of the JSON string
std::string field_dict_original_size =
find_value(MapSharedShreddingDefine::kFieldDictOriginalSize);
ASSERT_EQ(field_dict_original_size, std::to_string(expected_dict.size()));
std::string expected_field_to_columns = R"({"0":[0],"1":[1,2]})";
ASSERT_EQ(find_value(MapSharedShreddingDefine::kFieldColumns), expected_field_to_columns);
// overflow_set is a JSON array of sorted field_ids
ASSERT_EQ(find_value(MapSharedShreddingDefine::kOverflowSet), "[1,5]");
// Roundtrip verify
ASSERT_OK_AND_ASSIGN(auto deserialized, MapSharedShreddingUtils::DeserializeMetadata(metadata));
ASSERT_EQ(deserialized, original);
}
TEST(MapSharedShreddingUtilsTest, MetadataRoundtripCompression) {
MapSharedShreddingFieldMeta original;
original.name_to_id = {{"alpha", 0}, {"beta", 1}, {"gamma", 2}};
original.field_to_columns = {{0, {0, 1, 2}}, {1, {3}}, {2, {4, 5}}};
original.overflow_field_set = {2};
original.num_columns = 6;
original.max_row_width = 3;
auto verify_roundtrip = [&](const std::string& compression) {
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
ASSERT_OK(
MapSharedShreddingUtils::SerializeMetadata(original, compression, metadata.get()));
ASSERT_OK_AND_ASSIGN(auto deserialized,
MapSharedShreddingUtils::DeserializeMetadata(metadata));
ASSERT_EQ(deserialized, original);
};
verify_roundtrip("none");
verify_roundtrip("lz4");
verify_roundtrip("zstd");
}
TEST(MapSharedShreddingUtilsTest, MetadataRoundtripEmptyData) {
MapSharedShreddingFieldMeta original;
auto verify_roundtrip = [&](const std::string& compression) {
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
ASSERT_OK(
MapSharedShreddingUtils::SerializeMetadata(original, compression, metadata.get()));
ASSERT_OK_AND_ASSIGN(auto deserialized,
MapSharedShreddingUtils::DeserializeMetadata(metadata));
ASSERT_EQ(deserialized, original);
};
verify_roundtrip("none");
verify_roundtrip("lz4");
verify_roundtrip("zstd");
}
TEST(MapSharedShreddingUtilsTest, DeserializeLegacyMetadataWithoutCompression) {
MapSharedShreddingFieldMeta original;
original.name_to_id = {{"alpha", 0}, {"beta", 1}};
original.field_to_columns = {{0, {0}}, {1, {1}}};
original.overflow_field_set = {1};
original.num_columns = 2;
original.max_row_width = 2;
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
ASSERT_OK(MapSharedShreddingUtils::SerializeMetadata(
original, MapSharedShreddingDefine::kDefaultDictCompression, metadata.get()));
ASSERT_TRUE(metadata->Delete(MapSharedShreddingDefine::kFieldDictCompression).ok());
ASSERT_OK_AND_ASSIGN(auto deserialized, MapSharedShreddingUtils::DeserializeMetadata(metadata));
ASSERT_EQ(deserialized, original);
}
// ---- DeserializeMetadata error cases ----
TEST(MapSharedShreddingUtilsTest, DeserializeMetadataErrors) {
const std::string layout_error = "metadata is null or storage layout is not shared-shredding";
// nullptr
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::DeserializeMetadata(nullptr), layout_error);
// missing storage layout
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append("some_key", "some_value");
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::DeserializeMetadata(metadata), layout_error);
}
// wrong storage layout
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append(MapShreddingDefine::kStorageLayout, "default");
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::DeserializeMetadata(metadata), layout_error);
}
// missing version
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append(MapShreddingDefine::kStorageLayout,
MapShreddingDefine::kStorageLayoutSharedShredding);
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::DeserializeMetadata(metadata),
"missing shredding metadata key: paimon.map.shared-shredding.version");
}
// wrong version
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append(MapShreddingDefine::kStorageLayout,
MapShreddingDefine::kStorageLayoutSharedShredding);
metadata->Append(MapSharedShreddingDefine::kVersion, "999");
metadata->Append(MapSharedShreddingDefine::kFieldDictOriginalSize, "2");
metadata->Append(MapSharedShreddingDefine::kFieldDict, "{}");
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::DeserializeMetadata(metadata),
"unsupported shared-shredding metadata version: 999");
}
// missing field_dict
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append(MapShreddingDefine::kStorageLayout,
MapShreddingDefine::kStorageLayoutSharedShredding);
metadata->Append(MapSharedShreddingDefine::kVersion, "1");
metadata->Append(MapSharedShreddingDefine::kFieldDictOriginalSize, "2");
ASSERT_NOK_WITH_MSG(
MapSharedShreddingUtils::DeserializeMetadata(metadata),
"missing shredding metadata key: paimon.map.shared-shredding.field-dict");
}
}
// ---- HasShreddingMetadata ----
TEST(MapSharedShreddingUtilsTest, HasShreddingMetadata) {
ASSERT_FALSE(MapSharedShreddingUtils::HasShreddingMetadata(nullptr));
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append(MapShreddingDefine::kStorageLayout,
MapShreddingDefine::kStorageLayoutSharedShredding);
ASSERT_TRUE(MapSharedShreddingUtils::HasShreddingMetadata(metadata));
}
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
metadata->Append(MapShreddingDefine::kStorageLayout, "default");
ASSERT_FALSE(MapSharedShreddingUtils::HasShreddingMetadata(metadata));
}
{
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
ASSERT_FALSE(MapSharedShreddingUtils::HasShreddingMetadata(metadata));
}
}
// ---- PhysicalColumnName ----
TEST(MapSharedShreddingUtilsTest, PhysicalColumnName) {
ASSERT_EQ(MapSharedShreddingDefine::PhysicalColumnName(0), "__col_0");
ASSERT_EQ(MapSharedShreddingDefine::PhysicalColumnName(1), "__col_1");
ASSERT_EQ(MapSharedShreddingDefine::PhysicalColumnName(99), "__col_99");
}
// ---- GetPhysicalColumnIndices ----
// Normal: single physical column per field
TEST(MapSharedShreddingUtilsTest, GetPhysicalColumnIndicesSingleColumn) {
MapSharedShreddingFieldMeta meta;
meta.name_to_id = {{"age", 0}, {"name", 1}};
meta.field_to_columns = {{0, {2}}, {1, {5}}};
ASSERT_OK_AND_ASSIGN(auto cols_age,
MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "age"));
ASSERT_EQ(cols_age, (std::vector<int32_t>{2}));
ASSERT_OK_AND_ASSIGN(auto cols_name,
MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "name"));
ASSERT_EQ(cols_name, (std::vector<int32_t>{5}));
}
// Normal: multiple physical columns for one field
TEST(MapSharedShreddingUtilsTest, GetPhysicalColumnIndicesMultipleColumns) {
MapSharedShreddingFieldMeta meta;
meta.name_to_id = {{"tags", 0}};
meta.field_to_columns = {{0, {0, 3, 7}}};
ASSERT_OK_AND_ASSIGN(auto cols,
MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "tags"));
ASSERT_EQ(cols, (std::vector<int32_t>{0, 3, 7}));
}
// Normal: many fields each mapping to different physical columns
TEST(MapSharedShreddingUtilsTest, GetPhysicalColumnIndicesMultipleFields) {
MapSharedShreddingFieldMeta meta;
meta.name_to_id = {{"a", 0}, {"b", 1}, {"c", 2}};
meta.field_to_columns = {{0, {0, 1}}, {1, {2, 3, 4}}, {2, {5}}};
ASSERT_OK_AND_ASSIGN(auto cols_a, MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "a"));
ASSERT_EQ(cols_a, (std::vector<int32_t>{0, 1}));
ASSERT_OK_AND_ASSIGN(auto cols_b, MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "b"));
ASSERT_EQ(cols_b, (std::vector<int32_t>{2, 3, 4}));
ASSERT_OK_AND_ASSIGN(auto cols_c, MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "c"));
ASSERT_EQ(cols_c, (std::vector<int32_t>{5}));
}
// Error: field name not found in name_to_id
TEST(MapSharedShreddingUtilsTest, GetPhysicalColumnIndicesFieldNotFound) {
MapSharedShreddingFieldMeta meta;
meta.name_to_id = {{"age", 0}};
meta.field_to_columns = {{0, {1}}};
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "nonexistent"),
"cannot find field nonexistent in map shared shredding meta");
}
// Error: field name not found in empty meta
TEST(MapSharedShreddingUtilsTest, GetPhysicalColumnIndicesEmptyMeta) {
MapSharedShreddingFieldMeta meta;
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "any"),
"cannot find field any in map shared shredding meta");
}
// Error: field id exists in name_to_id but is missing from field_to_columns
TEST(MapSharedShreddingUtilsTest, GetPhysicalColumnIndicesFieldIdMissingInFieldToColumns) {
MapSharedShreddingFieldMeta meta;
// "score" -> field_id 42, but field_to_columns has no entry for 42
meta.name_to_id = {{"score", 42}};
meta.field_to_columns = {};
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::GetPhysicalColumnIndices(meta, "score"),
"cannot find field id 42 in field_to_columns in map shared shredding meta");
}
TEST(MapSharedShreddingUtilsTest, IsOverflowField) {
MapSharedShreddingFieldMeta meta;
meta.name_to_id = {{"a", 0}, {"b", 1}, {"c", 2}};
meta.overflow_field_set = {0, 2};
ASSERT_OK_AND_ASSIGN(bool a_overflow, MapSharedShreddingUtils::IsOverflowField(meta, "a"));
ASSERT_TRUE(a_overflow);
ASSERT_OK_AND_ASSIGN(bool b_overflow, MapSharedShreddingUtils::IsOverflowField(meta, "b"));
ASSERT_FALSE(b_overflow);
ASSERT_OK_AND_ASSIGN(bool c_overflow, MapSharedShreddingUtils::IsOverflowField(meta, "c"));
ASSERT_TRUE(c_overflow);
ASSERT_NOK_WITH_MSG(MapSharedShreddingUtils::IsOverflowField(meta, "missing"),
"cannot find field missing in map shared shredding meta");
}
} // namespace paimon::test