| /* |
| * 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 |