| /* | |
| * 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/variant/variant_get.h" | |
| #include <functional> | |
| #include <string> | |
| #include "arrow/api.h" | |
| #include "gtest/gtest.h" | |
| #include "paimon/common/data/variant/variant_builder.h" | |
| #include "paimon/common/data/variant/variant_path_segment.h" | |
| #include "paimon/common/data/variant/variant_type_utils.h" | |
| #include "paimon/common/utils/arrow/mem_utils.h" | |
| #include "paimon/data/decimal.h" | |
| #include "paimon/data/timestamp.h" | |
| #include "paimon/testing/utils/testharness.h" | |
| namespace paimon::test { | |
| class VariantGetTest : public ::testing::Test { | |
| public: | |
| void SetUp() override { | |
| // The same document as the Java GenericVariantTest#testVariantGet. | |
| std::string json = | |
| "{\n" | |
| " \"object\": {\n" | |
| " \"name\": \"Apache Paimon\",\n" | |
| " \"age\": 2,\n" | |
| " \"address\": {\n" | |
| " \"street\": \"Main St\",\n" | |
| " \"city\": \"Hangzhou\"\n" | |
| " }\n" | |
| " },\n" | |
| " \"array\": [1, 2, 3, 4, 5],\n" | |
| " \"string\": \"Hello, World!\",\n" | |
| " \"long\": 12345678901234,\n" | |
| " \"double\": 1.0123456789012345678901234567890123456789,\n" | |
| " \"decimal\": 100.99,\n" | |
| " \"boolean1\": true,\n" | |
| " \"boolean2\": false,\n" | |
| " \"nullField\": null\n" | |
| "}\n"; | |
| ASSERT_OK_AND_ASSIGN(variant_, GenericVariant::FromJson(json, pool_)); | |
| cast_args_.fail_on_error = false; | |
| } | |
| std::optional<Literal> Get(const std::string& path, | |
| const std::shared_ptr<arrow::DataType>& target) { | |
| auto result = VariantGetExecutor::Get(variant_, path, target, cast_args_); | |
| EXPECT_TRUE(result.ok()) << result.status().ToString(); | |
| return result.value(); | |
| } | |
| std::string GetString(const std::string& path, const std::shared_ptr<arrow::DataType>& target) { | |
| std::optional<Literal> literal = Get(path, target); | |
| EXPECT_TRUE(literal.has_value()); | |
| return literal->ToString(); | |
| } | |
| Result<std::shared_ptr<arrow::Array>> GetAsArrow( | |
| const std::string& path, const std::shared_ptr<arrow::Field>& target_field) { | |
| return VariantGetExecutor::GetAsArrow(variant_, path, target_field, cast_args_, pool_, | |
| arrow_pool_); | |
| } | |
| // Builds a single-scalar variant using the direct append API, which can encode types that | |
| // JSON parsing never produces (uuid/date/float/binary/timestamp). | |
| std::shared_ptr<GenericVariant> BuildScalar( | |
| const std::function<Status(VariantBuilder&)>& append) { | |
| VariantBuilder builder(/*allow_duplicate_keys=*/false); | |
| Status st = append(builder); | |
| EXPECT_TRUE(st.ok()) << st.ToString(); | |
| auto result = builder.Build(pool_); | |
| EXPECT_TRUE(result.ok()) << result.status().ToString(); | |
| return result.value(); | |
| } | |
| protected: | |
| std::shared_ptr<MemoryPool> pool_ = GetDefaultPool(); | |
| std::shared_ptr<arrow::MemoryPool> arrow_pool_ = GetArrowPool(pool_); | |
| std::shared_ptr<GenericVariant> variant_; | |
| VariantCastArgs cast_args_; | |
| }; | |
| TEST_F(VariantGetTest, PathSegmentParse) { | |
| ASSERT_OK_AND_ASSIGN(auto segments, | |
| VariantPathSegment::Parse("$[\"object\"]['address'].city[3]")); | |
| ASSERT_EQ(segments.size(), 4); | |
| ASSERT_EQ(segments[0].kind, VariantPathSegment::Kind::kObjectExtraction); | |
| ASSERT_EQ(segments[0].key, "object"); | |
| ASSERT_EQ(segments[1].kind, VariantPathSegment::Kind::kObjectExtraction); | |
| ASSERT_EQ(segments[1].key, "address"); | |
| ASSERT_EQ(segments[2].kind, VariantPathSegment::Kind::kObjectExtraction); | |
| ASSERT_EQ(segments[2].key, "city"); | |
| ASSERT_EQ(segments[3].kind, VariantPathSegment::Kind::kArrayExtraction); | |
| ASSERT_EQ(segments[3].index, 3); | |
| ASSERT_OK_AND_ASSIGN(auto root_only, VariantPathSegment::Parse("$")); | |
| ASSERT_TRUE(root_only.empty()); | |
| // Java parity: the root `$` is located anywhere in the path (Java uses Matcher#find), so a | |
| // prefix before the first `$` is tolerated and ignored. | |
| ASSERT_OK_AND_ASSIGN(auto prefixed, VariantPathSegment::Parse("abc$.x")); | |
| ASSERT_EQ(prefixed.size(), 1); | |
| ASSERT_EQ(prefixed[0].kind, VariantPathSegment::Kind::kObjectExtraction); | |
| ASSERT_EQ(prefixed[0].key, "x"); | |
| ASSERT_NOK(VariantPathSegment::Parse("")); | |
| ASSERT_NOK(VariantPathSegment::Parse("no_root")); | |
| ASSERT_NOK(VariantPathSegment::Parse("$.")); | |
| ASSERT_NOK(VariantPathSegment::Parse("$[abc]")); | |
| ASSERT_NOK(VariantPathSegment::Parse("$['unterminated]")); | |
| } | |
| TEST_F(VariantGetTest, ScalarTargets) { | |
| ASSERT_EQ(GetString("$.string", arrow::utf8()), "Hello, World!"); | |
| auto long_value = Get("$.long", arrow::int64()); | |
| ASSERT_TRUE(long_value.has_value()); | |
| ASSERT_EQ(long_value->GetValue<int64_t>(), 12345678901234LL); | |
| ASSERT_EQ(GetString("$.long", arrow::utf8()), "12345678901234"); | |
| auto double_value = Get("$.double", arrow::float64()); | |
| ASSERT_TRUE(double_value.has_value()); | |
| ASSERT_DOUBLE_EQ(double_value->GetValue<double>(), 1.0123456789012346); | |
| auto decimal_value = Get("$.decimal", arrow::decimal128(5, 2)); | |
| ASSERT_TRUE(decimal_value.has_value()); | |
| ASSERT_EQ(decimal_value->GetValue<Decimal>().ToUnscaledLong(), 10099); | |
| ASSERT_EQ(GetString("$.decimal", arrow::utf8()), "100.99"); | |
| auto bool1 = Get("$.boolean1", arrow::boolean()); | |
| ASSERT_TRUE(bool1.has_value()); | |
| ASSERT_TRUE(bool1->GetValue<bool>()); | |
| auto bool2 = Get("$.boolean2", arrow::boolean()); | |
| ASSERT_TRUE(bool2.has_value()); | |
| ASSERT_FALSE(bool2->GetValue<bool>()); | |
| // Variant null maps to SQL NULL. | |
| ASSERT_FALSE(Get("$.nullField", arrow::boolean()).has_value()); | |
| auto elem = Get("$.array[3]", arrow::int64()); | |
| ASSERT_TRUE(elem.has_value()); | |
| ASSERT_EQ(elem->GetValue<int64_t>(), 4); | |
| } | |
| TEST_F(VariantGetTest, DecimalWithTrailingZerosCastsToScalarTargets) { | |
| ASSERT_OK_AND_ASSIGN(auto variant, GenericVariant::FromJson("100.00", pool_)); | |
| ASSERT_OK_AND_ASSIGN(auto literal, | |
| VariantGetExecutor::Get(variant, "$", arrow::int64(), cast_args_)); | |
| ASSERT_TRUE(literal.has_value()); | |
| ASSERT_EQ(literal->GetValue<int64_t>(), 100); | |
| ASSERT_OK_AND_ASSIGN(auto array, VariantGetExecutor::GetAsArrow( | |
| variant, "$", arrow::field("x", arrow::decimal128(10, 2)), | |
| cast_args_, pool_, arrow_pool_)); | |
| ASSERT_EQ(static_cast<const arrow::Decimal128Array&>(*array).FormatValue(0), "100.00"); | |
| } | |
| TEST_F(VariantGetTest, ContainerToJsonString) { | |
| ASSERT_EQ(GetString("$.object", arrow::utf8()), | |
| "{\"address\":{\"city\":\"Hangzhou\",\"street\":\"Main St\"},\"age\":2,\"name\":" | |
| "\"Apache Paimon\"}"); | |
| ASSERT_EQ(GetString("$.object.name", arrow::utf8()), "Apache Paimon"); | |
| ASSERT_EQ(GetString("$.object.address.street", arrow::utf8()), "Main St"); | |
| ASSERT_EQ(GetString("$[\"object\"]['address'].city", arrow::utf8()), "Hangzhou"); | |
| ASSERT_EQ(GetString("$.array", arrow::utf8()), "[1,2,3,4,5]"); | |
| } | |
| TEST_F(VariantGetTest, UnmatchedPathAndInvalidCast) { | |
| // A path that does not exist yields SQL NULL. | |
| ASSERT_FALSE(Get("$.missing", arrow::utf8()).has_value()); | |
| ASSERT_FALSE(Get("$.string[0]", arrow::utf8()).has_value()); | |
| ASSERT_FALSE(Get("$.array.key", arrow::utf8()).has_value()); | |
| // An invalid cast yields SQL NULL when fail_on_error is false. | |
| ASSERT_FALSE(Get("$.object", arrow::int64()).has_value()); | |
| // ... and an error when fail_on_error is true. | |
| cast_args_.fail_on_error = true; | |
| auto result = VariantGetExecutor::Get(variant_, "$.object", arrow::int64(), cast_args_); | |
| ASSERT_NOK(result); | |
| } | |
| TEST_F(VariantGetTest, NestedTargetsNotImplemented) { | |
| auto result = | |
| VariantGetExecutor::Get(variant_, "$.array", arrow::list(arrow::int32()), cast_args_); | |
| ASSERT_TRUE(result.status().IsNotImplemented()); | |
| } | |
| TEST_F(VariantGetTest, BinaryAndTimestampSources) { | |
| // Java-encoded data can carry binary/timestamp scalars that JSON parsing never produces. | |
| { | |
| VariantBuilder builder(/*allow_duplicate_keys=*/false); | |
| ASSERT_OK(builder.AppendBinary(std::string_view("\x01\x02\x03", 3))); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, builder.Build(pool_)); | |
| ASSERT_OK_AND_ASSIGN(std::optional<Literal> literal, | |
| VariantGetExecutor::Get(variant, "$", arrow::binary(), cast_args_)); | |
| ASSERT_TRUE(literal.has_value()); | |
| ASSERT_EQ(literal->GetValue<std::string>(), std::string("\x01\x02\x03", 3)); | |
| } | |
| { | |
| VariantBuilder builder(/*allow_duplicate_keys=*/false); | |
| ASSERT_OK(builder.AppendTimestamp(1700000000123456)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, builder.Build(pool_)); | |
| ASSERT_OK_AND_ASSIGN( | |
| std::optional<Literal> literal, | |
| VariantGetExecutor::Get(variant, "$", arrow::timestamp(arrow::TimeUnit::MICRO, "UTC"), | |
| cast_args_)); | |
| ASSERT_TRUE(literal.has_value()); | |
| ASSERT_EQ(literal->GetValue<Timestamp>().ToMicrosecond(), 1700000000123456); | |
| } | |
| { | |
| VariantBuilder builder(/*allow_duplicate_keys=*/false); | |
| ASSERT_OK(builder.AppendTimestampNtz(-1001)); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, builder.Build(pool_)); | |
| ASSERT_OK_AND_ASSIGN( | |
| std::optional<Literal> literal, | |
| VariantGetExecutor::Get(variant, "$", arrow::timestamp(arrow::TimeUnit::MICRO), | |
| cast_args_)); | |
| ASSERT_TRUE(literal.has_value()); | |
| // Negative epochs must floor, not truncate toward zero. | |
| ASSERT_EQ(literal->GetValue<Timestamp>().ToMicrosecond(), -1001); | |
| } | |
| } | |
| TEST_F(VariantGetTest, ExtractByPath) { | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> address, | |
| VariantGetExecutor::ExtractByPath(variant_, "$.object.address")); | |
| ASSERT_NE(address, nullptr); | |
| ASSERT_OK_AND_ASSIGN(std::string json, address->ToJson()); | |
| ASSERT_EQ(json, "{\"city\":\"Hangzhou\",\"street\":\"Main St\"}"); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> missing, | |
| VariantGetExecutor::ExtractByPath(variant_, "$.object.missing")); | |
| ASSERT_EQ(missing, nullptr); | |
| } | |
| TEST_F(VariantGetTest, CastToStructTarget) { | |
| auto target = arrow::field( | |
| "r", arrow::struct_( | |
| {arrow::field("name", arrow::utf8()), arrow::field("age", arrow::int64()), | |
| arrow::field("address", arrow::struct_({arrow::field("city", arrow::utf8())})), | |
| arrow::field("missing", arrow::utf8())})); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.object", target)); | |
| ASSERT_EQ(array->length(), 1); | |
| const auto& row = static_cast<const arrow::StructArray&>(*array); | |
| ASSERT_FALSE(row.IsNull(0)); | |
| ASSERT_EQ(static_cast<const arrow::StringArray&>(*row.field(0)).GetString(0), "Apache Paimon"); | |
| ASSERT_EQ(static_cast<const arrow::Int64Array&>(*row.field(1)).Value(0), 2); | |
| const auto& address = static_cast<const arrow::StructArray&>(*row.field(2)); | |
| ASSERT_EQ(static_cast<const arrow::StringArray&>(*address.field(0)).GetString(0), "Hangzhou"); | |
| // A target field absent from the variant object is null. | |
| ASSERT_TRUE(row.field(3)->IsNull(0)); | |
| // With fail_on_error == false a child that cannot cast becomes null while the parent row | |
| // stays non-null. | |
| auto mixed_target = arrow::field("r", arrow::struct_({arrow::field("string", arrow::int64()), | |
| arrow::field("long", arrow::int64())})); | |
| ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$", mixed_target)); | |
| const auto& mixed_row = static_cast<const arrow::StructArray&>(*array); | |
| ASSERT_FALSE(mixed_row.IsNull(0)); | |
| // "string" holds "Hello, World!", which cannot cast to int64. | |
| ASSERT_TRUE(mixed_row.field(0)->IsNull(0)); | |
| ASSERT_FALSE(mixed_row.field(1)->IsNull(0)); | |
| ASSERT_EQ(static_cast<const arrow::Int64Array&>(*mixed_row.field(1)).Value(0), 12345678901234); | |
| } | |
| TEST_F(VariantGetTest, CastToStructFromNonObject) { | |
| auto target = arrow::field("r", arrow::struct_({arrow::field("a", arrow::int64())})); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.array", target)); | |
| ASSERT_TRUE(array->IsNull(0)); | |
| cast_args_.fail_on_error = true; | |
| auto result = | |
| VariantGetExecutor::GetAsArrow(variant_, "$.array", target, cast_args_, pool_, arrow_pool_); | |
| ASSERT_NOK(result); | |
| } | |
| TEST_F(VariantGetTest, CastToMapTarget) { | |
| auto target = arrow::field("m", arrow::map(arrow::utf8(), arrow::utf8())); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, | |
| GetAsArrow("$.object.address", target)); | |
| const auto& map = static_cast<const arrow::MapArray&>(*array); | |
| ASSERT_EQ(map.value_length(0), 2); | |
| const auto& keys = static_cast<const arrow::StringArray&>(*map.keys()); | |
| const auto& items = static_cast<const arrow::StringArray&>(*map.items()); | |
| ASSERT_EQ(keys.GetString(0), "city"); | |
| ASSERT_EQ(items.GetString(0), "Hangzhou"); | |
| ASSERT_EQ(keys.GetString(1), "street"); | |
| ASSERT_EQ(items.GetString(1), "Main St"); | |
| // A map with a non-string key type is an invalid cast. | |
| auto bad_target = arrow::field("m", arrow::map(arrow::int64(), arrow::utf8())); | |
| ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.object.address", bad_target)); | |
| ASSERT_TRUE(array->IsNull(0)); | |
| } | |
| TEST_F(VariantGetTest, CastToListTarget) { | |
| auto target = arrow::field("l", arrow::list(arrow::int64())); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.array", target)); | |
| const auto& list = static_cast<const arrow::ListArray&>(*array); | |
| ASSERT_EQ(list.value_length(0), 5); | |
| const auto& values = static_cast<const arrow::Int64Array&>(*list.values()); | |
| for (int64_t i = 0; i < 5; ++i) { | |
| ASSERT_EQ(values.Value(i), i + 1); | |
| } | |
| auto list_of_structs = | |
| arrow::field("l", arrow::list(arrow::struct_({arrow::field("a", arrow::int64())}))); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> rows, | |
| GenericVariant::FromJson("[{\"a\": 1}, {\"a\": 2}]", pool_)); | |
| ASSERT_OK_AND_ASSIGN(array, VariantGetExecutor::GetAsArrow(rows, "$", list_of_structs, | |
| cast_args_, pool_, arrow_pool_)); | |
| const auto& struct_list = static_cast<const arrow::ListArray&>(*array); | |
| ASSERT_EQ(struct_list.value_length(0), 2); | |
| const auto& elements = static_cast<const arrow::StructArray&>(*struct_list.values()); | |
| ASSERT_EQ(static_cast<const arrow::Int64Array&>(*elements.field(0)).Value(0), 1); | |
| ASSERT_EQ(static_cast<const arrow::Int64Array&>(*elements.field(0)).Value(1), 2); | |
| } | |
| TEST_F(VariantGetTest, CastToVariantTarget) { | |
| // Re-encodes the extracted sub-variant against a fresh metadata dictionary. | |
| auto target = VariantTypeUtils::ToArrowField("v", /*nullable=*/true, {}); | |
| auto read_variant_json = [&](const arrow::Array& array) { | |
| const auto& row = static_cast<const arrow::StructArray&>(array); | |
| EXPECT_FALSE(row.IsNull(0)); | |
| std::string_view value = static_cast<const arrow::BinaryArray&>(*row.field(0)).GetView(0); | |
| std::string_view metadata = | |
| static_cast<const arrow::BinaryArray&>(*row.field(1)).GetView(0); | |
| EXPECT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> copied, | |
| GenericVariant::Create(value, metadata, pool_)); | |
| EXPECT_OK_AND_ASSIGN(std::string json, copied->ToJson()); | |
| return json; | |
| }; | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, | |
| GetAsArrow("$.object.address", target)); | |
| ASSERT_EQ(read_variant_json(*array), "{\"city\":\"Hangzhou\",\"street\":\"Main St\"}"); | |
| // A variant null cast to a VARIANT target stays an encoded variant null (a non-null row | |
| // whose value renders as JSON null); only scalar targets turn a variant null into SQL NULL. | |
| ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.nullField", target)); | |
| ASSERT_EQ(read_variant_json(*array), "null"); | |
| // An unmatched path is SQL NULL by contrast. | |
| ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.missing", target)); | |
| ASSERT_TRUE(array->IsNull(0)); | |
| } | |
| TEST_F(VariantGetTest, NestedTargetNullSemantics) { | |
| auto target = arrow::field("r", arrow::struct_({arrow::field("a", arrow::int64())})); | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.missing", target)); | |
| ASSERT_TRUE(array->IsNull(0)); | |
| // A variant null yields SQL NULL even with fail_on_error == true. | |
| cast_args_.fail_on_error = true; | |
| ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.nullField", target)); | |
| ASSERT_TRUE(array->IsNull(0)); | |
| } | |
| TEST_F(VariantGetTest, UuidSourceCastsToStringOnly) { | |
| std::string uuid_bytes(16, '\0'); | |
| for (int i = 0; i < 16; ++i) { | |
| uuid_bytes[i] = static_cast<char>(i); | |
| } | |
| std::shared_ptr<GenericVariant> variant = | |
| BuildScalar([&](VariantBuilder& b) { return b.AppendUuid(uuid_bytes); }); | |
| // A UUID has no Paimon type, so it can only be rendered as its canonical string. | |
| ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_string, | |
| VariantGetExecutor::Get(variant, "$", arrow::utf8(), cast_args_)); | |
| ASSERT_TRUE(as_string.has_value()); | |
| ASSERT_EQ(as_string->GetValue<std::string>(), "00010203-0405-0607-0809-0a0b0c0d0e0f"); | |
| // Any non-string target is an invalid cast, which is SQL NULL when fail_on_error is false. | |
| ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_long, | |
| VariantGetExecutor::Get(variant, "$", arrow::int64(), cast_args_)); | |
| ASSERT_FALSE(as_long.has_value()); | |
| cast_args_.fail_on_error = true; | |
| ASSERT_NOK(VariantGetExecutor::Get(variant, "$", arrow::int64(), cast_args_)); | |
| } | |
| TEST_F(VariantGetTest, FloatingPointToStringMatchesJava) { | |
| // Doubles and floats are stringified via the Java formatting, not the arrow cast. | |
| ASSERT_EQ(GetString("$.double", arrow::utf8()), "1.0123456789012346"); | |
| std::shared_ptr<GenericVariant> f = | |
| BuildScalar([](VariantBuilder& b) { return b.AppendFloat(1.5f); }); | |
| ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_string, | |
| VariantGetExecutor::Get(f, "$", arrow::utf8(), cast_args_)); | |
| ASSERT_TRUE(as_string.has_value()); | |
| ASSERT_EQ(as_string->GetValue<std::string>(), "1.5"); | |
| ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_float, | |
| VariantGetExecutor::Get(f, "$", arrow::float32(), cast_args_)); | |
| ASSERT_TRUE(as_float.has_value()); | |
| ASSERT_EQ(as_float->GetValue<float>(), 1.5f); | |
| } | |
| TEST_F(VariantGetTest, DateSource) { | |
| std::shared_ptr<GenericVariant> d = | |
| BuildScalar([](VariantBuilder& b) { return b.AppendDate(19000); }); | |
| ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_date, | |
| VariantGetExecutor::Get(d, "$", arrow::date32(), cast_args_)); | |
| ASSERT_TRUE(as_date.has_value()); | |
| ASSERT_EQ(as_date->GetValue<int32_t>(), 19000); | |
| } | |
| TEST_F(VariantGetTest, MissingCastExecutorYieldsNull) { | |
| // No binary->int64 cast executor exists, so the cast is treated as invalid (SQL NULL). | |
| std::shared_ptr<GenericVariant> bin = BuildScalar( | |
| [](VariantBuilder& b) { return b.AppendBinary(std::string_view("\x01\x02", 2)); }); | |
| ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_long, | |
| VariantGetExecutor::Get(bin, "$", arrow::int64(), cast_args_)); | |
| ASSERT_FALSE(as_long.has_value()); | |
| } | |
| TEST_F(VariantGetTest, ScalarArrowBuilders) { | |
| auto build_array = [&](const std::shared_ptr<GenericVariant>& variant, const std::string& path, | |
| const std::shared_ptr<arrow::DataType>& type) { | |
| auto result = VariantGetExecutor::GetAsArrow(variant, path, arrow::field("x", type), | |
| cast_args_, pool_, arrow_pool_); | |
| EXPECT_TRUE(result.ok()) << result.status().ToString(); | |
| return result.value(); | |
| }; | |
| std::shared_ptr<arrow::Array> boolean = build_array(variant_, "$.boolean1", arrow::boolean()); | |
| ASSERT_TRUE(static_cast<const arrow::BooleanArray&>(*boolean).Value(0)); | |
| // INT8/INT16/INT32 each require a narrowing cast from the int64 source. | |
| std::shared_ptr<arrow::Array> int8 = build_array(variant_, "$.object.age", arrow::int8()); | |
| ASSERT_EQ(static_cast<const arrow::Int8Array&>(*int8).Value(0), 2); | |
| std::shared_ptr<arrow::Array> int16 = build_array(variant_, "$.object.age", arrow::int16()); | |
| ASSERT_EQ(static_cast<const arrow::Int16Array&>(*int16).Value(0), 2); | |
| std::shared_ptr<arrow::Array> int32 = build_array(variant_, "$.object.age", arrow::int32()); | |
| ASSERT_EQ(static_cast<const arrow::Int32Array&>(*int32).Value(0), 2); | |
| std::shared_ptr<arrow::Array> float64 = build_array(variant_, "$.double", arrow::float64()); | |
| ASSERT_DOUBLE_EQ(static_cast<const arrow::DoubleArray&>(*float64).Value(0), 1.0123456789012346); | |
| std::shared_ptr<GenericVariant> f = | |
| BuildScalar([](VariantBuilder& b) { return b.AppendFloat(1.5f); }); | |
| std::shared_ptr<arrow::Array> float32 = build_array(f, "$", arrow::float32()); | |
| ASSERT_EQ(static_cast<const arrow::FloatArray&>(*float32).Value(0), 1.5f); | |
| std::shared_ptr<arrow::Array> decimal = | |
| build_array(variant_, "$.decimal", arrow::decimal128(5, 2)); | |
| ASSERT_EQ(static_cast<const arrow::Decimal128Array&>(*decimal).FormatValue(0), "100.99"); | |
| std::shared_ptr<GenericVariant> bin = BuildScalar( | |
| [](VariantBuilder& b) { return b.AppendBinary(std::string_view("\x01\x02\x03", 3)); }); | |
| std::shared_ptr<arrow::Array> binary = build_array(bin, "$", arrow::binary()); | |
| ASSERT_EQ(static_cast<const arrow::BinaryArray&>(*binary).GetView(0), | |
| std::string_view("\x01\x02\x03", 3)); | |
| std::shared_ptr<GenericVariant> d = | |
| BuildScalar([](VariantBuilder& b) { return b.AppendDate(19000); }); | |
| std::shared_ptr<arrow::Array> date = build_array(d, "$", arrow::date32()); | |
| ASSERT_EQ(static_cast<const arrow::Date32Array&>(*date).Value(0), 19000); | |
| // Only the microsecond unit matches the variant's internal timestamp representation; other | |
| // units require a timestamp->timestamp literal cast that is intentionally unsupported. | |
| std::shared_ptr<GenericVariant> ts = | |
| BuildScalar([](VariantBuilder& b) { return b.AppendTimestamp(1700000000123456); }); | |
| std::shared_ptr<arrow::Array> timestamp = | |
| build_array(ts, "$", arrow::timestamp(arrow::TimeUnit::MICRO, "UTC")); | |
| ASSERT_EQ(static_cast<const arrow::TimestampArray&>(*timestamp).Value(0), 1700000000123456); | |
| } | |
| TEST_F(VariantGetTest, ListFromNonArrayIsNull) { | |
| auto target = arrow::field("l", arrow::list(arrow::int64())); | |
| // An object cannot cast to a list: SQL NULL when fail_on_error is false ... | |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.object", target)); | |
| ASSERT_TRUE(array->IsNull(0)); | |
| // ... and an error when fail_on_error is true. | |
| cast_args_.fail_on_error = true; | |
| auto result = VariantGetExecutor::GetAsArrow(variant_, "$.object", target, cast_args_, pool_, | |
| arrow_pool_); | |
| ASSERT_NOK(result); | |
| } | |
| } // namespace paimon::test |