blob: 159a573dca7cdee79a81eed1a9ac9dfcb1c202d0 [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/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, 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