blob: 632223a9d819ad61698f54b3aaef729a73430420 [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 <functional>
#include <memory>
#include <string>
#include <vector>
#include "arrow/api.h"
#include "gtest/gtest.h"
#include "paimon/common/data/variant/generic_variant.h"
#include "paimon/common/data/variant/variant_builder.h"
#include "paimon/common/data/variant/variant_defs.h"
#include "paimon/common/data/variant/variant_reassembler.h"
#include "paimon/common/data/variant/variant_schema.h"
#include "paimon/common/data/variant/variant_shredding_utils.h"
#include "paimon/common/data/variant/variant_shredding_writer.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
class VariantShreddingTest : public ::testing::Test {
public:
// Shreds the given JSON documents (nullptr = null variant) with the given shredding type,
// asserts the reassembled variants render back to the same JSON, and returns the shredded
// array for structural checks.
std::shared_ptr<arrow::StructArray> RoundTrip(
const std::shared_ptr<arrow::DataType>& shredding_type,
const std::vector<const char*>& jsons) {
EXPECT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> physical,
VariantShreddingUtils::VariantShreddingSchema(shredding_type));
EXPECT_OK_AND_ASSIGN(std::shared_ptr<VariantSchema> schema,
VariantShreddingUtils::BuildVariantSchema(physical));
EXPECT_OK_AND_ASSIGN(
std::unique_ptr<VariantShreddedColumnWriter> writer,
VariantShreddedColumnWriter::Create(schema, physical, arrow::default_memory_pool()));
std::vector<std::string> expected_jsons;
for (const char* json : jsons) {
if (json == nullptr) {
EXPECT_OK(writer->AppendNull());
expected_jsons.emplace_back();
continue;
}
EXPECT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant,
GenericVariant::FromJson(json, pool_));
EXPECT_OK_AND_ASSIGN(std::string expected_json, variant->ToJson());
expected_jsons.push_back(std::move(expected_json));
EXPECT_OK(writer->Append(*variant));
}
EXPECT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> shredded_array, writer->Finish());
auto shredded = std::static_pointer_cast<arrow::StructArray>(shredded_array);
EXPECT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> assembled_array,
VariantReassembler::AssembleVariantArray(
shredded, schema, pool_, arrow::default_memory_pool()));
auto assembled = std::static_pointer_cast<arrow::StructArray>(assembled_array);
EXPECT_EQ(assembled->length(), static_cast<int64_t>(jsons.size()));
auto value_column = std::static_pointer_cast<arrow::BinaryArray>(assembled->field(0));
auto metadata_column = std::static_pointer_cast<arrow::BinaryArray>(assembled->field(1));
for (size_t i = 0; i < jsons.size(); ++i) {
SCOPED_TRACE("row " + std::to_string(i));
if (jsons[i] == nullptr) {
EXPECT_TRUE(assembled->IsNull(i));
continue;
}
EXPECT_FALSE(assembled->IsNull(i));
EXPECT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant,
GenericVariant::Create(value_column->GetView(i),
metadata_column->GetView(i), pool_));
EXPECT_OK_AND_ASSIGN(std::string actual_json, variant->ToJson());
EXPECT_EQ(actual_json, expected_jsons[i]);
}
return shredded;
}
protected:
// The physical shredded arrow type for a logical shredding type.
std::shared_ptr<arrow::DataType> Physical(const std::shared_ptr<arrow::DataType>& logical) {
EXPECT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> physical,
VariantShreddingUtils::VariantShreddingSchema(logical));
return physical;
}
std::shared_ptr<GenericVariant> Json(const char* json) {
EXPECT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant,
GenericVariant::FromJson(json, pool_));
return variant;
}
// Builds a single variant using the direct append API, which can encode types (float, binary,
// date, timestamp) that JSON parsing never produces.
std::shared_ptr<GenericVariant> BuildVariant(
const std::function<Status(VariantBuilder&)>& append) {
VariantBuilder builder(/*allow_duplicate_keys=*/false);
Status st = append(builder);
EXPECT_TRUE(st.ok()) << st.ToString();
EXPECT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, builder.Build(pool_));
return variant;
}
// Shreds the given variants (nullptr = null row) against a physical shredded type, reassembles
// them, asserts each reassembled variant renders back to the same JSON, and returns the
// shredded array. Unlike `RoundTrip`, the physical type is provided directly so that typed
// columns unsupported by `VariantShreddingSchema` (date/timestamp) can be exercised.
std::shared_ptr<arrow::StructArray> ShredAndCheck(
const std::shared_ptr<arrow::DataType>& physical,
const std::vector<std::shared_ptr<GenericVariant>>& variants) {
EXPECT_OK_AND_ASSIGN(std::shared_ptr<VariantSchema> schema,
VariantShreddingUtils::BuildVariantSchema(physical));
EXPECT_OK_AND_ASSIGN(
std::unique_ptr<VariantShreddedColumnWriter> writer,
VariantShreddedColumnWriter::Create(schema, physical, arrow::default_memory_pool()));
std::vector<std::string> expected_jsons;
for (const auto& variant : variants) {
if (variant == nullptr) {
EXPECT_OK(writer->AppendNull());
expected_jsons.emplace_back();
continue;
}
EXPECT_OK_AND_ASSIGN(std::string expected_json, variant->ToJson());
expected_jsons.push_back(std::move(expected_json));
EXPECT_OK(writer->Append(*variant));
}
EXPECT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> shredded_array, writer->Finish());
auto shredded = std::static_pointer_cast<arrow::StructArray>(shredded_array);
EXPECT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> assembled_array,
VariantReassembler::AssembleVariantArray(
shredded, schema, pool_, arrow::default_memory_pool()));
auto assembled = std::static_pointer_cast<arrow::StructArray>(assembled_array);
auto value_column = std::static_pointer_cast<arrow::BinaryArray>(assembled->field(0));
auto metadata_column = std::static_pointer_cast<arrow::BinaryArray>(assembled->field(1));
for (size_t i = 0; i < variants.size(); ++i) {
SCOPED_TRACE("row " + std::to_string(i));
if (variants[i] == nullptr) {
EXPECT_TRUE(assembled->IsNull(i));
continue;
}
EXPECT_FALSE(assembled->IsNull(i));
EXPECT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant,
GenericVariant::Create(value_column->GetView(i),
metadata_column->GetView(i), pool_));
EXPECT_OK_AND_ASSIGN(std::string actual_json, variant->ToJson());
EXPECT_EQ(actual_json, expected_jsons[i]);
}
return shredded;
}
std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
};
TEST_F(VariantShreddingTest, ShreddingSchemaShape) {
auto shredding_type =
arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("b", arrow::utf8())});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> physical,
VariantShreddingUtils::VariantShreddingSchema(shredding_type));
// struct{metadata: binary not null, value: binary, typed_value: struct{a:
// struct{value, typed_value} not null, b: ... not null}}
auto expected = arrow::struct_(
{arrow::field("metadata", arrow::binary(), false),
arrow::field("value", arrow::binary(), true),
arrow::field(
"typed_value",
arrow::struct_(
{arrow::field("a",
arrow::struct_({arrow::field("value", arrow::binary(), true),
arrow::field("typed_value", arrow::int32(), true)}),
false),
arrow::field("b",
arrow::struct_({arrow::field("value", arrow::binary(), true),
arrow::field("typed_value", arrow::utf8(), true)}),
false)}),
true)});
ASSERT_TRUE(physical->Equals(*expected)) << physical->ToString();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<VariantSchema> schema,
VariantShreddingUtils::BuildVariantSchema(physical));
ASSERT_EQ(schema->top_level_metadata_idx, 0);
ASSERT_EQ(schema->variant_idx, 1);
ASSERT_EQ(schema->typed_idx, 2);
ASSERT_TRUE(schema->has_object_schema);
ASSERT_EQ(schema->object_schema.size(), 2);
ASSERT_FALSE(schema->IsUnshredded());
ASSERT_TRUE(VariantShreddingUtils::IsShreddedFileType(physical));
auto logical_variant = arrow::struct_({arrow::field("value", arrow::binary(), false),
arrow::field("metadata", arrow::binary(), false)});
auto untyped_physical_variant =
arrow::struct_({arrow::field("metadata", arrow::binary(), false),
arrow::field("value", arrow::binary(), false)});
ASSERT_FALSE(VariantShreddingUtils::IsShreddedFileType(logical_variant));
ASSERT_FALSE(VariantShreddingUtils::IsShreddedFileType(untyped_physical_variant));
ASSERT_FALSE(VariantShreddingUtils::IsUntypedPhysicalVariantType(logical_variant));
ASSERT_TRUE(VariantShreddingUtils::IsUntypedPhysicalVariantType(untyped_physical_variant));
// Invalid shredding types are rejected.
ASSERT_NOK(VariantShreddingUtils::VariantShreddingSchema(arrow::date32()));
ASSERT_NOK(
VariantShreddingUtils::VariantShreddingSchema(arrow::map(arrow::utf8(), arrow::int32())));
}
TEST_F(VariantShreddingTest, ShredObject) {
// Mirrors the Java GenericVariantTest#testShredding scenarios.
auto variant_json = R"({"a": 1, "b": "hello"})";
// Happy path: all fields shredded, no residual value.
{
auto shredded = RoundTrip(
arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("b", arrow::utf8())}),
{variant_json});
ASSERT_TRUE(shredded->field(1)->IsNull(0)); // top-level value (residual) is null
ASSERT_FALSE(shredded->field(2)->IsNull(0)); // typed_value is present
}
// Missing field "c" in the data: present in schema, both children null.
{
auto shredded = RoundTrip(
arrow::struct_({arrow::field("a", arrow::int32()), arrow::field("c", arrow::utf8()),
arrow::field("b", arrow::utf8())}),
{variant_json});
auto typed = std::static_pointer_cast<arrow::StructArray>(shredded->field(2));
auto c_group = std::static_pointer_cast<arrow::StructArray>(typed->field(1));
ASSERT_FALSE(c_group->IsNull(0));
ASSERT_TRUE(c_group->field(0)->IsNull(0));
ASSERT_TRUE(c_group->field(1)->IsNull(0));
}
// "a" is not present in the shredding schema: it goes to the residual value.
{
auto shredded = RoundTrip(
arrow::struct_({arrow::field("b", arrow::utf8()), arrow::field("c", arrow::utf8())}),
{variant_json});
auto value_column = std::static_pointer_cast<arrow::BinaryArray>(shredded->field(1));
ASSERT_FALSE(value_column->IsNull(0));
// The residual must equal the standalone encoding of {"a": 1}.
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> residual_expected,
GenericVariant::FromJson("{\"a\": 1}", pool_));
ASSERT_OK_AND_ASSIGN(std::string_view residual_value, residual_expected->Value());
ASSERT_EQ(value_column->GetView(0), residual_value);
}
}
TEST_F(VariantShreddingTest, ShredAllTypes) {
// Mirrors the Java GenericVariantTest#testShreddingAllTypes.
const char* json =
"{\n"
" \"c1\": \"Hello, World!\",\n"
" \"c2\": 12345678901234,\n"
" \"c3\": 1.0123456789012345678901234567890123456789,\n"
" \"c4\": 100.99,\n"
" \"c5\": true,\n"
" \"c6\": null,\n"
" \"c7\": {\"street\" : \"Main St\",\"city\" : \"Hangzhou\"},\n"
" \"c8\": [1, 2]\n"
"}\n";
auto shredding_type = arrow::struct_(
{arrow::field("c1", arrow::utf8()), arrow::field("c2", arrow::int64()),
arrow::field("c3", arrow::float64()), arrow::field("c4", arrow::decimal128(5, 2)),
arrow::field("c5", arrow::boolean()), arrow::field("c6", arrow::utf8()),
arrow::field("c7", arrow::struct_({arrow::field("street", arrow::utf8()),
arrow::field("city", arrow::utf8())})),
arrow::field("c8", arrow::list(arrow::int32()))});
auto shredded = RoundTrip(shredding_type, {json, nullptr, json});
// c6 is a variant null: it stays in the field's value column ("00"), typed_value is null.
auto typed = std::static_pointer_cast<arrow::StructArray>(shredded->field(2));
auto c6_group = std::static_pointer_cast<arrow::StructArray>(typed->field(5));
ASSERT_FALSE(c6_group->IsNull(0));
auto c6_value = std::static_pointer_cast<arrow::BinaryArray>(c6_group->field(0));
ASSERT_FALSE(c6_value->IsNull(0));
ASSERT_EQ(c6_value->GetView(0), std::string_view("\x00", 1));
ASSERT_TRUE(c6_group->field(1)->IsNull(0));
// Nothing was left over at the top level.
ASSERT_TRUE(shredded->field(1)->IsNull(0));
// No shredding at all: everything stays in the top-level value.
auto no_match_type = arrow::struct_({arrow::field("other", arrow::utf8())});
auto unshredded = RoundTrip(no_match_type, {json});
auto value_column = std::static_pointer_cast<arrow::BinaryArray>(unshredded->field(1));
ASSERT_FALSE(value_column->IsNull(0));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> expected,
GenericVariant::FromJson(json, pool_));
ASSERT_OK_AND_ASSIGN(std::string_view expected_value, expected->Value());
ASSERT_EQ(value_column->GetView(0), expected_value);
}
TEST_F(VariantShreddingTest, ShredScalarsAndMismatches) {
// Top-level scalar shredding with type mismatches falling back to the value column.
auto long_type = arrow::struct_({arrow::field("x", arrow::int64())});
RoundTrip(long_type,
{"{\"x\": 5}", R"({"x": "not a number"})", "{\"x\": 3.25}", "{\"x\": [1]}", "{}"});
// Decimal rescale: 100.99 fits decimal(9, 4) exactly (allowNumericScaleChanges).
auto decimal_type = arrow::struct_({arrow::field("x", arrow::decimal128(9, 4))});
RoundTrip(decimal_type,
{"{\"x\": 100.99}", "{\"x\": 42}", "{\"x\": 0.123456789}", "{\"x\": 100.00}"});
// A 38-digit unscaled value cannot rescale to scale 1 without overflowing the 128-bit
// decimal; it must fall back to the value column instead of being written corrupted.
auto wide_decimal_type = arrow::struct_({arrow::field("x", arrow::decimal128(38, 1))});
RoundTrip(wide_decimal_type,
{"{\"x\": 99999999999999999999999999999999999999}", "{\"x\": 1.5}"});
// Integer target from decimal that is numerically integral.
auto int_type = arrow::struct_({arrow::field("x", arrow::int32())});
RoundTrip(int_type, {"{\"x\": 5.0}", "{\"x\": 5.5}", "{\"x\": 123456789012345678}"});
auto array_type = arrow::struct_(
{arrow::field("arr", arrow::list(arrow::struct_({arrow::field("k", arrow::utf8())})))});
RoundTrip(array_type, {R"({"arr": [{"k": "v1"}, {"k": "v2", "extra": 1}, {"other": 2}]})",
"{\"arr\": [1, 2]}", R"({"arr": {"k": "v"}})"});
}
TEST_F(VariantShreddingTest, ScalarShreddingIntegerWidths) {
// int8/int16 targets: in-range longs shred into the narrow typed column, while out-of-range
// values fall back to the residual value column. Decimal-integral inputs take the
// decimal->integer shredding path.
ShredAndCheck(Physical(arrow::struct_({arrow::field("x", arrow::int8())})),
{Json(R"({"x": 5})"), Json(R"({"x": 200})"), Json(R"({"x": 5.0})")});
ShredAndCheck(Physical(arrow::struct_({arrow::field("x", arrow::int16())})),
{Json(R"({"x": 5})"), Json(R"({"x": 40000})"), Json(R"({"x": 5.0})")});
}
TEST_F(VariantShreddingTest, ScalarShreddingFloatAndBinary) {
// Float and binary variants are not producible from JSON, so build them directly. They shred
// into their typed columns and round-trip back to the same value.
ShredAndCheck(Physical(arrow::float32()),
{BuildVariant([](VariantBuilder& b) { return b.AppendFloat(1.5f); }), nullptr});
ShredAndCheck(Physical(arrow::binary()), {BuildVariant([](VariantBuilder& b) {
return b.AppendBinary(std::string_view("\x01\x02\x03", 3));
})});
}
TEST_F(VariantShreddingTest, ScalarShreddingDate) {
// date typed columns are produced by external engines; `VariantShreddingSchema` itself never
// emits them, so build the physical shredded type directly.
auto physical = arrow::struct_({arrow::field("metadata", arrow::binary(), false),
arrow::field("value", arrow::binary(), true),
arrow::field("typed_value", arrow::date32(), true)});
ShredAndCheck(physical,
{BuildVariant([](VariantBuilder& b) { return b.AppendDate(19000); }), nullptr});
}
TEST_F(VariantShreddingTest, TimestampSchemaParsing) {
auto make_physical = [](const std::shared_ptr<arrow::DataType>& ts) {
return arrow::struct_({arrow::field("metadata", arrow::binary(), false),
arrow::field("value", arrow::binary(), true),
arrow::field("typed_value", ts, true)});
};
// A microsecond timestamp with a timezone parses as TIMESTAMP_LTZ; without, TIMESTAMP_NTZ.
ASSERT_OK_AND_ASSIGN(std::shared_ptr<VariantSchema> ltz,
VariantShreddingUtils::BuildVariantSchema(
make_physical(arrow::timestamp(arrow::TimeUnit::MICRO, "UTC"))));
ASSERT_TRUE(ltz->scalar_schema.has_value());
ASSERT_EQ(ltz->scalar_schema->kind, VariantSchema::ScalarKind::kTimestampLtz);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<VariantSchema> ntz,
VariantShreddingUtils::BuildVariantSchema(
make_physical(arrow::timestamp(arrow::TimeUnit::MICRO))));
ASSERT_TRUE(ntz->scalar_schema.has_value());
ASSERT_EQ(ntz->scalar_schema->kind, VariantSchema::ScalarKind::kTimestampNtz);
// Non-microsecond timestamps cannot represent the variant's microsecond values, so they are
// rejected as an invalid shredding schema.
ASSERT_NOK(VariantShreddingUtils::BuildVariantSchema(
make_physical(arrow::timestamp(arrow::TimeUnit::MILLI, "UTC"))));
}
TEST_F(VariantShreddingTest, TimestampReassembly) {
// The writer never produces timestamp typed columns, but the reassembler must handle files
// written by engines that do. Build a shredded array with a populated timestamp typed_value
// and verify it reassembles into the same variant.
auto reassemble_one = [&](const std::shared_ptr<arrow::DataType>& ts_type,
const std::shared_ptr<GenericVariant>& reference, int64_t micros) {
auto physical = arrow::struct_({arrow::field("metadata", arrow::binary(), false),
arrow::field("value", arrow::binary(), true),
arrow::field("typed_value", ts_type, true)});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<VariantSchema> schema,
VariantShreddingUtils::BuildVariantSchema(physical));
std::string metadata(reference->Metadata());
ASSERT_OK_AND_ASSIGN(std::string expected_json, reference->ToJson());
arrow::BinaryBuilder meta_builder;
ASSERT_TRUE(meta_builder.Append(metadata).ok());
std::shared_ptr<arrow::Array> meta_array;
ASSERT_TRUE(meta_builder.Finish(&meta_array).ok());
arrow::BinaryBuilder value_builder;
ASSERT_TRUE(value_builder.AppendNull().ok());
std::shared_ptr<arrow::Array> value_array;
ASSERT_TRUE(value_builder.Finish(&value_array).ok());
arrow::TimestampBuilder ts_builder(ts_type, arrow::default_memory_pool());
ASSERT_TRUE(ts_builder.Append(micros).ok());
std::shared_ptr<arrow::Array> ts_array;
ASSERT_TRUE(ts_builder.Finish(&ts_array).ok());
auto made = arrow::StructArray::Make({meta_array, value_array, ts_array},
{"metadata", "value", "typed_value"});
ASSERT_TRUE(made.ok()) << made.status().ToString();
std::shared_ptr<arrow::StructArray> shredded = made.ValueOrDie();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> assembled_array,
VariantReassembler::AssembleVariantArray(
shredded, schema, pool_, arrow::default_memory_pool()));
auto assembled = std::static_pointer_cast<arrow::StructArray>(assembled_array);
auto value_column = std::static_pointer_cast<arrow::BinaryArray>(assembled->field(0));
auto metadata_column = std::static_pointer_cast<arrow::BinaryArray>(assembled->field(1));
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GenericVariant> variant,
GenericVariant::Create(value_column->GetView(0), metadata_column->GetView(0), pool_));
ASSERT_OK_AND_ASSIGN(std::string actual_json, variant->ToJson());
ASSERT_EQ(actual_json, expected_json);
};
int64_t micros = 1700000000000000;
reassemble_one(arrow::timestamp(arrow::TimeUnit::MICRO, "UTC"),
BuildVariant([&](VariantBuilder& b) { return b.AppendTimestamp(micros); }),
micros);
reassemble_one(arrow::timestamp(arrow::TimeUnit::MICRO),
BuildVariant([&](VariantBuilder& b) { return b.AppendTimestampNtz(micros); }),
micros);
}
TEST_F(VariantShreddingTest, ScalarSchemaToArrowType) {
using SK = VariantSchema::ScalarKind;
auto check = [](VariantSchema::ScalarType scalar,
const std::shared_ptr<arrow::DataType>& expected) {
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::DataType> type,
VariantShreddingUtils::ScalarSchemaToArrowType(scalar));
ASSERT_TRUE(type->Equals(*expected)) << type->ToString();
};
check({SK::kBoolean}, arrow::boolean());
check({SK::kByte}, arrow::int8());
check({SK::kShort}, arrow::int16());
check({SK::kInt}, arrow::int32());
check({SK::kLong}, arrow::int64());
check({SK::kFloat}, arrow::float32());
check({SK::kDouble}, arrow::float64());
check({SK::kString}, arrow::utf8());
check({SK::kBinary}, arrow::binary());
check({SK::kDecimal, 10, 2}, arrow::decimal128(10, 2));
check({SK::kDate}, arrow::date32());
check({SK::kTimestampLtz}, arrow::timestamp(arrow::TimeUnit::MICRO, "UTC"));
check({SK::kTimestampNtz}, arrow::timestamp(arrow::TimeUnit::MICRO));
// kUuid has no shredded arrow representation.
ASSERT_NOK(VariantShreddingUtils::ScalarSchemaToArrowType({SK::kUuid}));
}
TEST_F(VariantShreddingTest, InvalidShreddingSchemas) {
// Not a struct.
ASSERT_NOK(VariantShreddingUtils::BuildVariantSchema(arrow::int32()));
// Empty struct.
ASSERT_NOK(VariantShreddingUtils::BuildVariantSchema(arrow::struct_({})));
// The "value" column must be binary.
ASSERT_NOK(VariantShreddingUtils::BuildVariantSchema(
arrow::struct_({arrow::field("value", arrow::int32())})));
// Unknown field name.
ASSERT_NOK(VariantShreddingUtils::BuildVariantSchema(
arrow::struct_({arrow::field("bogus", arrow::binary())})));
// A top-level schema must carry a metadata column.
ASSERT_NOK(VariantShreddingUtils::BuildVariantSchema(
arrow::struct_({arrow::field("value", arrow::binary())})));
// Unsupported typed_value type.
ASSERT_NOK(VariantShreddingUtils::BuildVariantSchema(arrow::struct_(
{arrow::field("metadata", arrow::binary()), arrow::field("value", arrow::binary()),
arrow::field("typed_value", arrow::map(arrow::utf8(), arrow::int32()))})));
}
} // namespace paimon::test