blob: cd2947304fff4e07691d2130e48ec41b1b10bb0e [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 "exprs/variant-functions.h"
#include <algorithm>
#include <cstring>
#include <sstream>
#include <vector>
#include "gutil/strings/substitute.h"
#include "runtime/mem-pool.h"
#include "runtime/mem-tracker.h"
#include "runtime/variant-value.h"
#include "testutil/gtest-util.h"
#include "udf/udf-internal.h"
#include "common/names.h"
using impala_udf::FunctionContext;
namespace impala {
// ------------------------- variant-blob builders ---------------------------
// (Mirrors the encoding used in be/src/util/variant-util-test.cc, with 1-byte field ids
// and offsets only.)
static vector<uint8_t> BuildMetadata(const vector<string>& field_names) {
vector<uint8_t> buf;
uint8_t header = 0x01 | (1 << 4) | (0 << 6); // version=1, sorted, offset_size=1
buf.push_back(header);
buf.push_back(static_cast<uint8_t>(field_names.size()));
uint32_t offset = 0;
for (size_t i = 0; i <= field_names.size(); ++i) {
buf.push_back(static_cast<uint8_t>(offset));
if (i < field_names.size()) offset += field_names[i].size();
}
for (const string& name : field_names) {
for (char c : name) buf.push_back(static_cast<uint8_t>(c));
}
return buf;
}
static vector<uint8_t> BuildNull() { return {0x00}; }
static vector<uint8_t> BuildBoolean(bool val) {
uint8_t type_info = val ? 1 : 2;
return {static_cast<uint8_t>(type_info << 2)};
}
// Primitive header byte = physical-type-id << 2 (basic_type PRIMITIVE=0 in bits 0-1).
template <typename T>
static vector<uint8_t> BuildPrimitive(uint8_t phys_type_id, T val) {
vector<uint8_t> buf = {static_cast<uint8_t>(phys_type_id << 2)};
uint8_t bytes[sizeof(T)];
memcpy(bytes, &val, sizeof(T));
buf.insert(buf.end(), bytes, bytes + sizeof(T));
return buf;
}
static vector<uint8_t> BuildInt8(int8_t val) { return BuildPrimitive<int8_t>(3, val); }
static vector<uint8_t> BuildInt16(int16_t val) { return BuildPrimitive<int16_t>(4, val); }
static vector<uint8_t> BuildInt32(int32_t val) { return BuildPrimitive<int32_t>(5, val); }
static vector<uint8_t> BuildInt64(int64_t val) { return BuildPrimitive<int64_t>(6, val); }
static vector<uint8_t> BuildDouble(double val) { return BuildPrimitive<double>(7, val); }
static vector<uint8_t> BuildFloat(float val) { return BuildPrimitive<float>(14, val); }
// DATE (physical type 11) stores a signed int32 day count since the epoch.
static vector<uint8_t> BuildDate(int32_t days) {
return BuildPrimitive<int32_t>(11, days);
}
// Types the serializer cannot render. TIME (17), TIMESTAMPTZ (12) and TIMESTAMPTZ_NANOS
// (18) store an int64; UUID (20) stores 16 raw bytes.
static vector<uint8_t> BuildTime(int64_t micros) {
return BuildPrimitive<int64_t>(17, micros);
}
static vector<uint8_t> BuildTimestampTz(int64_t micros) {
return BuildPrimitive<int64_t>(12, micros);
}
static vector<uint8_t> BuildTimestampTzNanos(int64_t nanos) {
return BuildPrimitive<int64_t>(18, nanos);
}
static vector<uint8_t> BuildUuid() {
vector<uint8_t> buf = {static_cast<uint8_t>(20 << 2)};
for (uint8_t i = 0; i < 16; ++i) buf.push_back(i);
return buf;
}
static vector<uint8_t> BuildShortString(const string& s) {
uint8_t header = 0x01 | (static_cast<uint8_t>(s.size()) << 2);
vector<uint8_t> buf = {header};
for (char c : s) buf.push_back(static_cast<uint8_t>(c));
return buf;
}
// Lays out 'values' in the order given by 'storage_order', a permutation of
// [0, values.size()) (empty means the identity). Sets '*offsets' to where each value
// starts, followed by the total size, and appends the value data to '*data'.
static void LayoutValues(const vector<vector<uint8_t>>& values,
const vector<int>& storage_order, vector<uint32_t>* offsets, vector<uint8_t>* data) {
vector<int> order = storage_order;
if (order.empty()) {
for (int i = 0; i < values.size(); ++i) order.push_back(i);
}
CHECK_EQ(order.size(), values.size());
vector<int> sorted_order = order;
std::sort(sorted_order.begin(), sorted_order.end());
for (int i = 0; i < sorted_order.size(); ++i) CHECK_EQ(i, sorted_order[i]);
offsets->assign(values.size() + 1, 0);
for (int k : order) {
(*offsets)[k] = data->size();
data->insert(data->end(), values[k].begin(), values[k].end());
}
(*offsets)[values.size()] = data->size();
CHECK_LE(data->size(), 255);
}
// 'values[i]' is the value of 'field_ids[i]'. The field ids must be listed in field name
// order, but the values may be stored in any order: 'storage_order' lists the indices of
// 'values' in the order they are stored in (empty means the same order as 'field_ids').
static vector<uint8_t> BuildObject(const vector<int>& field_ids,
const vector<vector<uint8_t>>& values, const vector<int>& storage_order = {}) {
vector<uint32_t> offsets;
vector<uint8_t> data;
LayoutValues(values, storage_order, &offsets, &data);
vector<uint8_t> buf = {0x02}; // OBJECT, 1-byte fields/offsets
buf.push_back(static_cast<uint8_t>(field_ids.size()));
for (int fid : field_ids) buf.push_back(static_cast<uint8_t>(fid));
for (uint32_t o : offsets) buf.push_back(static_cast<uint8_t>(o));
buf.insert(buf.end(), data.begin(), data.end());
return buf;
}
// The elements are stored in 'storage_order' (see BuildObject()).
static vector<uint8_t> BuildArray(const vector<vector<uint8_t>>& elements,
const vector<int>& storage_order = {}) {
vector<uint32_t> offsets;
vector<uint8_t> data;
LayoutValues(elements, storage_order, &offsets, &data);
vector<uint8_t> buf = {0x03}; // ARRAY, 1-byte offsets
buf.push_back(static_cast<uint8_t>(elements.size()));
for (uint32_t o : offsets) buf.push_back(static_cast<uint8_t>(o));
buf.insert(buf.end(), data.begin(), data.end());
return buf;
}
// Parses space-separated hex bytes, e.g. "02 01 00".
static vector<uint8_t> FromHex(const string& hex) {
vector<uint8_t> buf;
std::istringstream in(hex);
string byte;
while (in >> byte) buf.push_back(static_cast<uint8_t>(std::stoul(byte, nullptr, 16)));
return buf;
}
// What try_variant_get() writes to stderr for a corrupt variant (see CaptureStderr()).
static const char* CORRUPT_WARNING =
"UDF WARNING: variant_get: corrupt or unreadable variant";
// --------------------------------- fixture ---------------------------------
class VariantFunctionsTest : public ::testing::Test {
protected:
virtual void SetUp() override {
pool_.reset(new MemPool(&tracker_));
FunctionContext::TypeDesc ret; // return type is unused by the tested logic
ret.type = FunctionContext::TYPE_STRING;
std::vector<FunctionContext::TypeDesc> args; // args not registered as constant
ctx_ = FunctionContextImpl::CreateContext(
nullptr, pool_.get(), pool_.get(), ret, args, 0, true);
ASSERT_TRUE(ctx_ != nullptr);
}
virtual void TearDown() override {
for (FunctionContext* c : owned_ctxs_) {
c->impl()->Close();
delete c;
}
owned_ctxs_.clear();
if (ctx_ != nullptr) {
ctx_->impl()->Close();
delete ctx_;
ctx_ = nullptr;
}
pool_->FreeAll();
}
// Builds a non-null VariantVal referencing the given blobs (kept alive by the caller).
VariantVal MakeVariant(const vector<uint8_t>& meta, const vector<uint8_t>& value) {
StringVal m(const_cast<uint8_t*>(meta.data()), meta.size());
StringVal v(const_cast<uint8_t*>(value.data()), value.size());
return VariantVal(m, v);
}
// Decodes a returned VARIANT's value as an int32 (asserts it is one).
int32_t DecodeInt32(const VariantVal& out) {
EXPECT_FALSE(out.is_null);
VariantMetadata meta;
EXPECT_OK(meta.Init(out.metadata.ptr, out.metadata.len));
VariantValue v(out.value.ptr, out.value.len, &meta);
EXPECT_EQ(VariantBasicType::PRIMITIVE, v.GetBasicType());
EXPECT_EQ(VariantPhysicalType::INT32, v.GetPhysicalType());
int32_t result = 0;
EXPECT_TRUE(v.GetInt32(&result));
return result;
}
string DecodeString(const VariantVal& out) {
EXPECT_FALSE(out.is_null);
VariantMetadata meta;
EXPECT_OK(meta.Init(out.metadata.ptr, out.metadata.len));
VariantValue v(out.value.ptr, out.value.len, &meta);
StringValue sv;
EXPECT_TRUE(v.GetString(&sv));
return string(sv.Ptr(), sv.Len());
}
// Renders a returned StringVal (asserts non-null).
string AsString(const StringVal& s) {
EXPECT_FALSE(s.is_null);
return string(reinterpret_cast<char*>(s.ptr), s.len);
}
// Renders a VARIANT as JSON with variant_to_json() (asserts non-null).
string Json(const VariantVal& v) {
return AsString(VariantFunctions::VariantToJson(ctx_, v));
}
// Runs 'fn' and returns what it wrote to stderr. A test FunctionContext has no
// RuntimeState, so FunctionContext::AddWarning() writes "UDF WARNING: ..." to stderr.
template <typename Fn>
string CaptureStderr(const Fn& fn) {
testing::internal::CaptureStderr();
fn();
return testing::internal::GetCapturedStderr();
}
// Creates an owned FunctionContext with 'num_args' (all non-constant by default).
// Registered in owned_ctxs_ for teardown. Used by tests that raise a strict error
// (which poisons the context) so the shared ctx_ stays clean for later assertions.
FunctionContext* MakeCtx(int num_args) {
FunctionContext::TypeDesc td;
td.type = FunctionContext::TYPE_STRING;
std::vector<FunctionContext::TypeDesc> args(num_args, td);
FunctionContext* c = FunctionContextImpl::CreateContext(
nullptr, pool_.get(), pool_.get(), td, args, 0, true);
owned_ctxs_.push_back(c);
return c;
}
MemTracker tracker_;
boost::scoped_ptr<MemPool> pool_;
FunctionContext* ctx_ = nullptr;
std::vector<FunctionContext*> owned_ctxs_;
};
// {"age": 30, "name": "Alice"} with a sorted dictionary ["age", "name"].
#define BUILD_PERSON() \
vector<uint8_t> meta = BuildMetadata({"age", "name"}); \
vector<uint8_t> obj = BuildObject({0, 1}, {BuildInt32(30), BuildShortString("Alice")})
TEST_F(VariantFunctionsTest, TwoArgObjectField) {
BUILD_PERSON();
VariantVal v = MakeVariant(meta, obj);
EXPECT_EQ(30, DecodeInt32(VariantFunctions::VariantGet(ctx_, v, StringVal("$.age"))));
EXPECT_EQ("Alice",
DecodeString(VariantFunctions::VariantGet(ctx_, v, StringVal("$.name"))));
}
TEST_F(VariantFunctionsTest, TwoArgIdentityAndMissing) {
BUILD_PERSON();
VariantVal v = MakeVariant(meta, obj);
// '$' returns the whole object; its value blob is the same slice.
VariantVal whole = VariantFunctions::VariantGet(ctx_, v, StringVal("$"));
EXPECT_FALSE(whole.is_null);
EXPECT_EQ(v.value.ptr, whole.value.ptr);
// Missing field -> SQL NULL.
EXPECT_TRUE(VariantFunctions::VariantGet(ctx_, v, StringVal("$.nope")).is_null);
}
TEST_F(VariantFunctionsTest, TwoArgNullInputsAndVNull) {
BUILD_PERSON();
EXPECT_TRUE(VariantFunctions::VariantGet(ctx_, VariantVal::null(),
StringVal("$.age")).is_null);
// A VNULL at the path is SQL NULL.
vector<uint8_t> meta2 = BuildMetadata({"a"});
vector<uint8_t> obj2 = BuildObject({0}, {BuildNull()});
VariantVal v2 = MakeVariant(meta2, obj2);
EXPECT_TRUE(VariantFunctions::VariantGet(ctx_, v2, StringVal("$.a")).is_null);
}
TEST_F(VariantFunctionsTest, TwoArgArrayIndexAndChaining) {
vector<uint8_t> meta = BuildMetadata({"arr"});
vector<uint8_t> arr = BuildArray({BuildInt32(1), BuildInt32(2), BuildInt32(3)});
vector<uint8_t> obj = BuildObject({0}, {arr});
VariantVal v = MakeVariant(meta, obj);
EXPECT_EQ(2, DecodeInt32(VariantFunctions::VariantGet(ctx_, v, StringVal("$.arr[1]"))));
// Chaining: outer get consumes the VARIANT produced by the inner get.
VariantVal inner = VariantFunctions::VariantGet(ctx_, v, StringVal("$.arr"));
ASSERT_FALSE(inner.is_null);
EXPECT_EQ(3, DecodeInt32(VariantFunctions::VariantGet(ctx_, inner, StringVal("$[2]"))));
}
TEST_F(VariantFunctionsTest, ThreeArgTypedExtraction) {
BUILD_PERSON();
VariantVal v = MakeVariant(meta, obj);
// Integer widening to the requested width.
EXPECT_EQ(30,
VariantFunctions::VariantGetInt(ctx_, v, StringVal("$.age"), StringVal("int")).val);
EXPECT_EQ(30, VariantFunctions::VariantGetBigInt(
ctx_, v, StringVal("$.age"), StringVal("bigint")).val);
// Raw string value, no JSON quoting.
StringVal s = VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.name"), StringVal("string"));
ASSERT_FALSE(s.is_null);
EXPECT_EQ("Alice", string(reinterpret_cast<char*>(s.ptr), s.len));
}
TEST_F(VariantFunctionsTest, ThreeArgBoolean) {
vector<uint8_t> meta = BuildMetadata({"b"});
vector<uint8_t> obj = BuildObject({0}, {BuildBoolean(true)});
VariantVal v = MakeVariant(meta, obj);
BooleanVal r = VariantFunctions::VariantGetBoolean(
ctx_, v, StringVal("$.b"), StringVal("boolean"));
ASSERT_FALSE(r.is_null);
EXPECT_TRUE(r.val);
}
TEST_F(VariantFunctionsTest, ThreeArgTryMismatchIsNull) {
BUILD_PERSON();
VariantVal v = MakeVariant(meta, obj);
// "Alice" is not convertible to INT; try_ returns NULL and does not set an error.
EXPECT_TRUE(VariantFunctions::TryVariantGetInt(
ctx_, v, StringVal("$.name"), StringVal("int")).is_null);
EXPECT_FALSE(ctx_->has_error());
}
TEST_F(VariantFunctionsTest, ThreeArgNarrowingOverflowIsNullForTry) {
// 30000 fits INT but overflows TINYINT.
vector<uint8_t> meta = BuildMetadata({"n"});
vector<uint8_t> obj = BuildObject({0}, {BuildInt32(30000)});
VariantVal v = MakeVariant(meta, obj);
EXPECT_EQ(30000,
VariantFunctions::VariantGetInt(ctx_, v, StringVal("$.n"), StringVal("int")).val);
EXPECT_TRUE(VariantFunctions::TryVariantGetTinyInt(
ctx_, v, StringVal("$.n"), StringVal("tinyint")).is_null);
}
// Strict narrowing overflow raises an error (not just a NULL). Uses an owned context
// because SetError poisons it.
TEST_F(VariantFunctionsTest, ThreeArgStrictOverflowSetsError) {
vector<uint8_t> meta = BuildMetadata({"n"});
vector<uint8_t> obj = BuildObject({0}, {BuildInt32(30000)});
VariantVal v = MakeVariant(meta, obj);
FunctionContext* c = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetTinyInt(
c, v, StringVal("$.n"), StringVal("tinyint")).is_null);
EXPECT_TRUE(c->has_error());
}
// Native integer widths extract into their matching typed getter. The dictionary is
// sorted (BuildMetadata sets the sorted flag), so field names are listed in sorted order.
TEST_F(VariantFunctionsTest, ThreeArgIntWidths) {
vector<uint8_t> meta = BuildMetadata({"i16", "i64", "i8"});
vector<uint8_t> obj = BuildObject({0, 1, 2},
{BuildInt16(1000), BuildInt64(10000000000LL), BuildInt8(34)});
VariantVal v = MakeVariant(meta, obj);
EXPECT_EQ(34, VariantFunctions::VariantGetTinyInt(
ctx_, v, StringVal("$.i8"), StringVal("tinyint")).val);
EXPECT_EQ(1000, VariantFunctions::VariantGetSmallInt(
ctx_, v, StringVal("$.i16"), StringVal("smallint")).val);
// int64 widens into BIGINT.
EXPECT_EQ(10000000000LL, VariantFunctions::VariantGetBigInt(
ctx_, v, StringVal("$.i64"), StringVal("bigint")).val);
// A smaller value also widens up when a wider type is requested.
EXPECT_EQ(34, VariantFunctions::VariantGetBigInt(
ctx_, v, StringVal("$.i8"), StringVal("bigint")).val);
}
// FLOAT extraction: native float and widening from an integer.
TEST_F(VariantFunctionsTest, ThreeArgFloat) {
vector<uint8_t> meta = BuildMetadata({"f", "i"});
vector<uint8_t> obj = BuildObject({0, 1}, {BuildFloat(1.5f), BuildInt32(5)});
VariantVal v = MakeVariant(meta, obj);
EXPECT_FLOAT_EQ(1.5f, VariantFunctions::VariantGetFloat(
ctx_, v, StringVal("$.f"), StringVal("float")).val);
EXPECT_FLOAT_EQ(5.0f, VariantFunctions::VariantGetFloat(
ctx_, v, StringVal("$.i"), StringVal("float")).val);
}
// A double outside the FLOAT range is an out-of-range coercion: strict errors, try_
// NULLs.
TEST_F(VariantFunctionsTest, ThreeArgFloatOverflow) {
vector<uint8_t> meta = BuildMetadata({"d"});
vector<uint8_t> obj = BuildObject({0}, {BuildDouble(1e100)});
VariantVal v = MakeVariant(meta, obj);
EXPECT_TRUE(VariantFunctions::TryVariantGetFloat(
ctx_, v, StringVal("$.d"), StringVal("float")).is_null);
EXPECT_FALSE(ctx_->has_error());
FunctionContext* c = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetFloat(
c, v, StringVal("$.d"), StringVal("float")).is_null);
EXPECT_TRUE(c->has_error());
}
// A value whose type has no numeric coercion (here a BOOLEAN) is a mismatch for both
// FLOAT and DOUBLE: strict errors, try_ NULLs.
TEST_F(VariantFunctionsTest, ThreeArgFloatDoubleMismatch) {
vector<uint8_t> meta = BuildMetadata({"b"});
vector<uint8_t> obj = BuildObject({0}, {BuildBoolean(true)});
VariantVal v = MakeVariant(meta, obj);
// try_ -> NULL, no error, for both target types.
EXPECT_TRUE(VariantFunctions::TryVariantGetFloat(
ctx_, v, StringVal("$.b"), StringVal("float")).is_null);
EXPECT_TRUE(VariantFunctions::TryVariantGetDouble(
ctx_, v, StringVal("$.b"), StringVal("double")).is_null);
EXPECT_FALSE(ctx_->has_error());
// Strict -> error. Each strict call uses its own context, since SetError poisons it.
FunctionContext* cf = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetFloat(
cf, v, StringVal("$.b"), StringVal("float")).is_null);
EXPECT_TRUE(cf->has_error());
FunctionContext* cd = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetDouble(
cd, v, StringVal("$.b"), StringVal("double")).is_null);
EXPECT_TRUE(cd->has_error());
}
// DOUBLE extraction widens every native numeric physical type; a native DOUBLE reads
// through unchanged. Exercises ToDouble()'s FLOAT/DOUBLE/INT8/INT32/INT64 branches.
TEST_F(VariantFunctionsTest, ThreeArgDoubleWidening) {
vector<uint8_t> meta = BuildMetadata({"d", "f", "i32", "i64", "i8"}); // sorted dict
vector<uint8_t> obj = BuildObject({0, 1, 2, 3, 4},
{BuildDouble(2.5), BuildFloat(1.5f), BuildInt32(5), BuildInt64(10000000000LL),
BuildInt8(34)});
VariantVal v = MakeVariant(meta, obj);
EXPECT_DOUBLE_EQ(2.5, VariantFunctions::VariantGetDouble(
ctx_, v, StringVal("$.d"), StringVal("double")).val);
// 1.5f is exactly representable, so it widens to exactly 1.5.
EXPECT_DOUBLE_EQ(1.5, VariantFunctions::VariantGetDouble(
ctx_, v, StringVal("$.f"), StringVal("double")).val);
EXPECT_DOUBLE_EQ(5.0, VariantFunctions::VariantGetDouble(
ctx_, v, StringVal("$.i32"), StringVal("double")).val);
EXPECT_DOUBLE_EQ(1e10, VariantFunctions::VariantGetDouble(
ctx_, v, StringVal("$.i64"), StringVal("double")).val);
EXPECT_DOUBLE_EQ(34.0, VariantFunctions::VariantGetDouble(
ctx_, v, StringVal("$.i8"), StringVal("double")).val);
}
// DATE extraction from the DATE physical type and from a date string; a bare INT32 is
// intentionally NOT coerced to a date.
TEST_F(VariantFunctionsTest, ThreeArgDate) {
vector<uint8_t> meta = BuildMetadata({"d", "i", "s"}); // sorted dictionary
vector<uint8_t> obj = BuildObject({0, 1, 2},
{BuildDate(0), BuildInt32(19669), BuildShortString("2024-11-07")});
VariantVal v = MakeVariant(meta, obj);
// days=0 is 1970-01-01; DateVal carries the day count.
DateVal d = VariantFunctions::VariantGetDate(
ctx_, v, StringVal("$.d"), StringVal("date"));
ASSERT_FALSE(d.is_null);
EXPECT_EQ(0, d.val);
// A date string parses.
EXPECT_FALSE(VariantFunctions::VariantGetDate(
ctx_, v, StringVal("$.s"), StringVal("date")).is_null);
// A plain INT32 is rejected (try_ -> NULL, no coercion to a day count).
EXPECT_TRUE(VariantFunctions::TryVariantGetDate(
ctx_, v, StringVal("$.i"), StringVal("date")).is_null);
}
// Invalid DATE inputs fail: a day count outside the supported DATE range and an
// unparseable date string. Strict errors, try_ NULLs.
TEST_F(VariantFunctionsTest, ThreeArgInvalidDate) {
// "big" is a DATE physical value whose day count is far past the max supported date;
// "s" is a non-date string. (Dictionary is sorted: "big" < "s".)
vector<uint8_t> meta = BuildMetadata({"big", "s"});
vector<uint8_t> obj = BuildObject({0, 1},
{BuildDate(2000000000), BuildShortString("not-a-date")});
VariantVal v = MakeVariant(meta, obj);
// try_ -> NULL, no error, for both the out-of-range value and the bad string.
EXPECT_TRUE(VariantFunctions::TryVariantGetDate(
ctx_, v, StringVal("$.big"), StringVal("date")).is_null);
EXPECT_TRUE(VariantFunctions::TryVariantGetDate(
ctx_, v, StringVal("$.s"), StringVal("date")).is_null);
EXPECT_FALSE(ctx_->has_error());
// Strict -> error. Each strict call uses its own context, since SetError poisons it.
FunctionContext* cb = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetDate(
cb, v, StringVal("$.big"), StringVal("date")).is_null);
EXPECT_TRUE(cb->has_error());
FunctionContext* cs = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetDate(
cs, v, StringVal("$.s"), StringVal("date")).is_null);
EXPECT_TRUE(cs->has_error());
}
// String extraction of objects/arrays renders JSON; scalars render their bare text.
TEST_F(VariantFunctionsTest, ThreeArgStringFormats) {
BUILD_PERSON();
VariantVal person = MakeVariant(meta, obj);
// Whole object -> JSON text (field id order, i.e. name order).
EXPECT_EQ("{\"age\":30,\"name\":\"Alice\"}",
AsString(VariantFunctions::VariantGetString(
ctx_, person, StringVal("$"), StringVal("string"))));
vector<uint8_t> meta2 = BuildMetadata({"b", "d", "dt", "i"}); // sorted dictionary
vector<uint8_t> obj2 = BuildObject({0, 1, 2, 3},
{BuildBoolean(true), BuildDouble(14.3), BuildDate(0), BuildInt32(42)});
VariantVal v = MakeVariant(meta2, obj2);
EXPECT_EQ("42", AsString(VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.i"), StringVal("string"))));
EXPECT_EQ("14.3", AsString(VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.d"), StringVal("string"))));
EXPECT_EQ("true", AsString(VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.b"), StringVal("string"))));
// DATE renders unquoted (the outer JSON quotes are stripped).
EXPECT_EQ("1970-01-01", AsString(VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.dt"), StringVal("string"))));
}
// A type the serializer cannot render must fail to coerce to STRING rather than return
// its placeholder: strict errors, try_ NULLs.
TEST_F(VariantFunctionsTest, ThreeArgStringUnsupportedType) {
vector<uint8_t> meta = BuildMetadata({"t", "tz", "tzn", "u"}); // sorted dictionary
vector<uint8_t> obj = BuildObject({0, 1, 2, 3},
{BuildTime(45234123456LL), BuildTimestampTz(1730000000000000LL),
BuildTimestampTzNanos(1730000000000000000LL), BuildUuid()});
VariantVal v = MakeVariant(meta, obj);
for (const char* path : {"$.t", "$.tz", "$.tzn", "$.u"}) {
EXPECT_TRUE(VariantFunctions::TryVariantGetString(
ctx_, v, StringVal(path), StringVal("string")).is_null) << path;
// Each strict call uses its own context, since SetError poisons it.
FunctionContext* c = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetString(
c, v, StringVal(path), StringVal("string")).is_null) << path;
EXPECT_TRUE(c->has_error()) << path;
}
EXPECT_FALSE(ctx_->has_error());
}
// A STRING result is a view into the input value blob, not a per-row copy.
TEST_F(VariantFunctionsTest, ThreeArgStringIsZeroCopy) {
BUILD_PERSON();
VariantVal v = MakeVariant(meta, obj);
StringVal s = VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.name"), StringVal("string"));
ASSERT_FALSE(s.is_null);
EXPECT_EQ("Alice", AsString(s));
EXPECT_GE(s.ptr, v.value.ptr);
EXPECT_LE(s.ptr + s.len, v.value.ptr + v.value.len);
}
// A NULL path argument yields SQL NULL, not an error.
TEST_F(VariantFunctionsTest, NullPath) {
BUILD_PERSON();
VariantVal v = MakeVariant(meta, obj);
EXPECT_TRUE(VariantFunctions::VariantGet(ctx_, v, StringVal::null()).is_null);
EXPECT_TRUE(VariantFunctions::VariantGetInt(
ctx_, v, StringVal::null(), StringVal("int")).is_null);
EXPECT_FALSE(ctx_->has_error());
}
// A syntactically malformed path yields SQL NULL by the backend function. Normally
// the FE raises analysis errors for such paths.
TEST_F(VariantFunctionsTest, MalformedPath) {
BUILD_PERSON();
VariantVal v = MakeVariant(meta, obj);
const char* bad_paths[] = {
"", // empty (no '$')
"$age", // missing '.' or '[' after '$'
"$.", // no segment after the prefix
"$[abc]", // non-numeric array index
"$[1", // unterminated array index
};
for (const char* path : bad_paths) {
EXPECT_TRUE(VariantFunctions::VariantGet(ctx_, v, StringVal(path)).is_null) << path;
EXPECT_TRUE(VariantFunctions::VariantGetInt(
ctx_, v, StringVal(path), StringVal("int")).is_null) << path;
}
}
// Corrupt metadata: strict raises an error, try_ yields NULL without erroring.
TEST_F(VariantFunctionsTest, CorruptMetadata) {
vector<uint8_t> bad_meta = {0x01}; // header only, no dictionary-size/offset bytes
vector<uint8_t> val = BuildInt32(1);
VariantVal v = MakeVariant(bad_meta, val);
EXPECT_TRUE(VariantFunctions::TryVariantGet(ctx_, v, StringVal("$")).is_null);
EXPECT_FALSE(ctx_->has_error());
FunctionContext* c = MakeCtx(2);
EXPECT_TRUE(VariantFunctions::VariantGet(c, v, StringVal("$")).is_null);
EXPECT_TRUE(c->has_error());
}
// Every encoded value has at least a header byte, so an empty value blob and an object
// field that starts at the end of the value data are corrupt: strict raises an error,
// try_ yields NULL with a warning. Covers an empty value blob (at '$' and when looking up
// a field in it) and such a field.
TEST_F(VariantFunctionsTest, EmptyValueAtPath) {
vector<uint8_t> meta = BuildMetadata({"a"});
vector<uint8_t> empty_val;
// {"a": <starts at the end of the (empty) value data>}
vector<uint8_t> empty_field = {0x02, 0x01, 0x00, 0x00, 0x00};
struct Case { const vector<uint8_t>* value; const char* path; };
for (const Case& tc : {Case{&empty_val, "$"}, Case{&empty_val, "$.a"},
Case{&empty_field, "$.a"}}) {
VariantVal v = MakeVariant(meta, *tc.value);
string warnings = CaptureStderr([&]() {
EXPECT_TRUE(VariantFunctions::TryVariantGet(ctx_, v, StringVal(tc.path)).is_null);
});
EXPECT_NE(string::npos, warnings.find(CORRUPT_WARNING)) << tc.path << warnings;
warnings = CaptureStderr([&]() {
EXPECT_TRUE(VariantFunctions::TryVariantGetBoolean(
ctx_, v, StringVal(tc.path), StringVal("boolean")).is_null);
});
EXPECT_NE(string::npos, warnings.find(CORRUPT_WARNING)) << tc.path << warnings;
EXPECT_FALSE(ctx_->has_error()) << tc.path;
FunctionContext* c = MakeCtx(2);
EXPECT_TRUE(VariantFunctions::VariantGet(c, v, StringVal(tc.path)).is_null);
EXPECT_TRUE(c->has_error()) << tc.path;
FunctionContext* c3 = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetInt(
c3, v, StringVal(tc.path), StringVal("int")).is_null);
EXPECT_TRUE(c3->has_error()) << tc.path;
}
}
// ------------------ values stored out of name order (IMPALA-15460) ------------------
// Spark 4.1.3 bytes of {"b":2,"a":1} and {"a":1,"c":3,"b":2}: the field ids and offsets
// are listed in name order, but the values are stored in document order.
TEST_F(VariantFunctionsTest, OutOfOrderSparkBytes) {
struct Case {
const char* meta;
const char* value;
const char* json;
};
const vector<Case> cases = {
{"01 02 00 01 02 62 61", "02 02 01 00 02 00 04 0c 02 0c 01", R"V({"a":1,"b":2})V"},
{"01 03 00 01 02 03 61 63 62", "02 03 00 02 01 00 04 02 06 0c 01 0c 03 0c 02",
R"V({"a":1,"b":2,"c":3})V"}};
for (const Case& c : cases) {
SCOPED_TRACE(c.value);
vector<uint8_t> meta = FromHex(c.meta);
vector<uint8_t> value = FromHex(c.value);
VariantVal v = MakeVariant(meta, value);
EXPECT_EQ(1, VariantFunctions::VariantGetInt(
ctx_, v, StringVal("$.a"), StringVal("int")).val);
EXPECT_EQ(2, VariantFunctions::VariantGetInt(
ctx_, v, StringVal("$.b"), StringVal("int")).val);
EXPECT_EQ(1, VariantFunctions::TryVariantGetInt(
ctx_, v, StringVal("$.a"), StringVal("int")).val);
EXPECT_EQ(2, VariantFunctions::TryVariantGetInt(
ctx_, v, StringVal("$.b"), StringVal("int")).val);
EXPECT_EQ(c.json, AsString(VariantFunctions::VariantGetString(
ctx_, v, StringVal("$"), StringVal("string"))));
EXPECT_EQ(c.json, Json(v));
// The 2-arg variant_get returns the exact bytes of the field.
VariantVal a = VariantFunctions::VariantGet(ctx_, v, StringVal("$.a"));
ASSERT_FALSE(a.is_null);
EXPECT_EQ(2, a.value.len);
EXPECT_EQ("1", Json(a));
}
EXPECT_FALSE(ctx_->has_error());
}
// Spark 4.1.3 bytes of {"a":{"y":1,"x":2},"b":0}.
TEST_F(VariantFunctionsTest, OutOfOrderNested) {
vector<uint8_t> meta = FromHex("01 04 00 01 02 03 04 61 79 78 62");
vector<uint8_t> value =
FromHex("02 02 00 03 00 0b 0d 02 02 02 01 02 00 04 0c 01 0c 02 0c 00");
VariantVal v = MakeVariant(meta, value);
EXPECT_EQ(2, VariantFunctions::VariantGetInt(
ctx_, v, StringVal("$.a.x"), StringVal("int")).val);
EXPECT_EQ(1, VariantFunctions::VariantGetInt(
ctx_, v, StringVal("$.a.y"), StringVal("int")).val);
EXPECT_EQ(0, VariantFunctions::VariantGetInt(
ctx_, v, StringVal("$.b"), StringVal("int")).val);
VariantVal a = VariantFunctions::VariantGet(ctx_, v, StringVal("$.a"));
EXPECT_EQ(R"V({"x":2,"y":1})V", Json(a));
EXPECT_EQ(R"V({"x":2,"y":1})V", AsString(VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.a"), StringVal("string"))));
// Chaining: the outer get consumes the VARIANT returned by the inner one.
EXPECT_EQ(2, VariantFunctions::VariantGetInt(
ctx_, a, StringVal("$.x"), StringVal("int")).val);
EXPECT_FALSE(ctx_->has_error());
}
// {"c":"s","b":true,"a":2.5}, stored in reverse name order, through the typed getters.
TEST_F(VariantFunctionsTest, OutOfOrderAllGetters) {
vector<uint8_t> meta = BuildMetadata({"a", "b", "c"});
vector<uint8_t> obj = BuildObject({0, 1, 2},
{BuildDouble(2.5), BuildBoolean(true), BuildShortString("s")},
/*storage_order=*/{2, 1, 0});
VariantVal v = MakeVariant(meta, obj);
EXPECT_EQ("s", AsString(VariantFunctions::VariantGetString(
ctx_, v, StringVal("$.c"), StringVal("string"))));
EXPECT_EQ("s", AsString(VariantFunctions::TryVariantGetString(
ctx_, v, StringVal("$.c"), StringVal("string"))));
BooleanVal b = VariantFunctions::VariantGetBoolean(
ctx_, v, StringVal("$.b"), StringVal("boolean"));
ASSERT_FALSE(b.is_null);
EXPECT_TRUE(b.val);
b = VariantFunctions::TryVariantGetBoolean(
ctx_, v, StringVal("$.b"), StringVal("boolean"));
ASSERT_FALSE(b.is_null);
EXPECT_TRUE(b.val);
EXPECT_DOUBLE_EQ(2.5, VariantFunctions::VariantGetDouble(
ctx_, v, StringVal("$.a"), StringVal("double")).val);
EXPECT_DOUBLE_EQ(2.5, VariantFunctions::TryVariantGetDouble(
ctx_, v, StringVal("$.a"), StringVal("double")).val);
EXPECT_EQ(R"V({"a":2.5,"b":true,"c":"s"})V", Json(v));
EXPECT_FALSE(ctx_->has_error());
}
TEST_F(VariantFunctionsTest, OutOfOrderArrays) {
// [{"b":2,"a":1}]
vector<uint8_t> meta = BuildMetadata({"a", "b"});
vector<uint8_t> arr = BuildArray(
{BuildObject({0, 1}, {BuildInt8(1), BuildInt8(2)}, /*storage_order=*/{1, 0})});
VariantVal v = MakeVariant(meta, arr);
EXPECT_EQ(1, VariantFunctions::VariantGetInt(
ctx_, v, StringVal("$[0].a"), StringVal("int")).val);
EXPECT_EQ(2, VariantFunctions::VariantGetInt(
ctx_, v, StringVal("$[0].b"), StringVal("int")).val);
// [5,6] with the elements stored as 6, 5: the offsets are [2, 0, 4].
vector<uint8_t> empty_meta = FromHex("01 00 00");
vector<uint8_t> arr2 = FromHex("03 02 02 00 04 0c 06 0c 05");
VariantVal v2 = MakeVariant(empty_meta, arr2);
EXPECT_EQ(5, VariantFunctions::VariantGetInt(
ctx_, v2, StringVal("$[0]"), StringVal("int")).val);
EXPECT_EQ(6, VariantFunctions::VariantGetInt(
ctx_, v2, StringVal("$[1]"), StringVal("int")).val);
EXPECT_EQ("[5,6]", Json(v2));
EXPECT_FALSE(ctx_->has_error());
}
// An unsorted dictionary ["x", "a", "a"] holds "a" twice, and {"a":7} uses the second id.
TEST_F(VariantFunctionsTest, DuplicateDictionaryStrings) {
vector<uint8_t> meta = FromHex("01 03 00 01 02 03 78 61 61");
vector<uint8_t> obj = FromHex("02 01 02 00 02 0c 07");
VariantVal v = MakeVariant(meta, obj);
IntVal a = VariantFunctions::VariantGetInt(ctx_, v, StringVal("$.a"), StringVal("int"));
ASSERT_FALSE(a.is_null);
EXPECT_EQ(7, a.val);
EXPECT_EQ(R"V({"a":7})V", Json(v));
EXPECT_FALSE(ctx_->has_error());
}
// A value on the path that cannot be read, the root included, is corrupt: strict
// variant_get raises an error, try_variant_get returns NULL with a warning.
TEST_F(VariantFunctionsTest, UnreadableValueIsCorrupt) {
vector<uint8_t> meta = BuildMetadata({"a"});
struct Case {
const char* value;
const char* path;
};
const vector<Case> cases = {
// An INT32 field that runs past the last offset (2).
{"02 01 00 00 02 14 01", "$.a"},
// A field of an unknown primitive type (id 21).
{"02 01 00 00 01 54", "$.a"},
// Field id 5 is outside the 1-entry dictionary.
{"02 01 05 00 02 0c 01", "$.a"},
// A nested object with 100 fields in 3 bytes.
{"02 01 00 00 03 02 64 00", "$.a.b"},
// {"a":{"a":<INT32 without a payload>}}: '$.a' can be read, '$.a.a' cannot.
{"02 01 00 00 07 02 01 00 00 02 14 01", "$.a.a"},
// [<INT32 without a payload>]
{"03 01 00 01 14", "$[0]"},
// An object with 100 fields in 3 bytes.
{"02 64 00", "$.a"},
// A root INT32 without a full payload, and a root of an unknown type (id 21).
{"14 01", "$"},
{"54", "$"},
{"54", "$.a"}};
for (const Case& c : cases) {
SCOPED_TRACE(Substitute("value: '$0' path: '$1'", c.value, c.path));
vector<uint8_t> value = FromHex(c.value);
VariantVal v = MakeVariant(meta, value);
FunctionContext* c2 = MakeCtx(2);
EXPECT_TRUE(VariantFunctions::VariantGet(c2, v, StringVal(c.path)).is_null);
EXPECT_TRUE(c2->has_error());
EXPECT_STREQ("variant_get: corrupt or unreadable variant", c2->error_msg());
FunctionContext* c3 = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetInt(
c3, v, StringVal(c.path), StringVal("int")).is_null);
EXPECT_TRUE(c3->has_error());
EXPECT_STREQ("variant_get: corrupt or unreadable variant", c3->error_msg());
string warnings = CaptureStderr([&]() {
EXPECT_TRUE(VariantFunctions::TryVariantGet(ctx_, v, StringVal(c.path)).is_null);
});
EXPECT_NE(string::npos, warnings.find(CORRUPT_WARNING)) << warnings;
warnings = CaptureStderr([&]() {
EXPECT_TRUE(VariantFunctions::TryVariantGetInt(
ctx_, v, StringVal(c.path), StringVal("int")).is_null);
});
EXPECT_NE(string::npos, warnings.find(CORRUPT_WARNING)) << warnings;
EXPECT_FALSE(ctx_->has_error());
}
}
// A path that does not match the value is SQL NULL, without an error or a warning.
TEST_F(VariantFunctionsTest, MissingPathsStayNull) {
// {"a":1,"b":[1]}
vector<uint8_t> meta = BuildMetadata({"a", "b"});
vector<uint8_t> obj = BuildObject({0, 1}, {BuildInt8(1), BuildArray({BuildInt8(1)})});
VariantVal v = MakeVariant(meta, obj);
for (const char* path : {"$.zzz", "$.a.b", "$[0]", "$.b[5]", "$.b.c"}) {
SCOPED_TRACE(path);
FunctionContext* c2 = MakeCtx(2);
EXPECT_TRUE(VariantFunctions::VariantGet(c2, v, StringVal(path)).is_null);
EXPECT_FALSE(c2->has_error());
FunctionContext* c3 = MakeCtx(3);
EXPECT_TRUE(VariantFunctions::VariantGetInt(
c3, v, StringVal(path), StringVal("int")).is_null);
EXPECT_FALSE(c3->has_error());
string warnings = CaptureStderr([&]() {
EXPECT_TRUE(VariantFunctions::TryVariantGet(ctx_, v, StringVal(path)).is_null);
});
EXPECT_EQ(string::npos, warnings.find("UDF WARNING")) << warnings;
warnings = CaptureStderr([&]() {
EXPECT_TRUE(VariantFunctions::TryVariantGetInt(
ctx_, v, StringVal(path), StringVal("int")).is_null);
});
EXPECT_EQ(string::npos, warnings.find("UDF WARNING")) << warnings;
}
}
} // namespace impala