blob: bf78a7bc79bd36da53ca51ca94c25865d7750f98 [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 "util/variant-util.h"
#include <cstring>
#include <vector>
#include "testutil/gtest-util.h"
#include "common/names.h"
#include "runtime/variant-value.h"
namespace impala {
// Helper to build a metadata blob with a sorted dictionary.
// Format: header(1) + dict_size(1) + offsets(dict_size+1) + string_data
static vector<uint8_t> BuildMetadata(const vector<string>& field_names) {
vector<uint8_t> buf;
// Header: version=1, sorted=1, offset_size=1 (offset_size_minus_one=0)
uint8_t header = 0x01 | (1 << 4) | (0 << 6);
buf.push_back(header);
// Dictionary size (1 byte since offset_size=1)
buf.push_back(static_cast<uint8_t>(field_names.size()));
// Offsets: dict_size+1 entries
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();
}
// String data
for (const string& name : field_names) {
for (char c : name) buf.push_back(static_cast<uint8_t>(c));
}
return buf;
}
// Build a null primitive value.
static vector<uint8_t> BuildNull() {
// basic_type=0 (PRIMITIVE), type_info=0 (NULL) → header = 0x00
return {0x00};
}
// Build a boolean value.
static vector<uint8_t> BuildBoolean(bool val) {
// basic_type=0, type_info=1(TRUE) or 2(FALSE)
uint8_t type_info = val ? 1 : 2;
return {static_cast<uint8_t>(type_info << 2)};
}
// Build an int32 value.
static vector<uint8_t> BuildInt32(int32_t val) {
// basic_type=0, type_info=5 (INT32) → header = 5<<2 = 0x14
vector<uint8_t> buf = {0x14};
uint8_t bytes[4];
memcpy(bytes, &val, 4);
buf.insert(buf.end(), bytes, bytes + 4);
return buf;
}
// Build a short string value (length < 64).
static vector<uint8_t> BuildShortString(const string& s) {
DCHECK_LT(s.size(), 64);
// basic_type=1 (SHORT_STRING), type_info=length
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;
}
// Build a simple object with given field values.
// field_ids: indices into the metadata dictionary.
static vector<uint8_t> BuildObject(const vector<int>& field_ids,
const vector<vector<uint8_t>>& values) {
DCHECK_EQ(field_ids.size(), values.size());
int num_fields = field_ids.size();
// Compute value offsets.
vector<uint32_t> offsets;
uint32_t offset = 0;
for (const auto& v : values) {
offsets.push_back(offset);
offset += v.size();
}
offsets.push_back(offset); // final offset = total data size
// Header: basic_type=2 (OBJECT), field_offset_size=1 (bits2-3=0),
// field_id_size=1 (bits4-5=0), is_large=0 (bit6=0) -> header = 0x02
uint8_t header = 0x02;
vector<uint8_t> buf = {header};
// Num fields (1 byte since not large)
buf.push_back(static_cast<uint8_t>(num_fields));
// Field IDs (1 byte each)
for (int fid : field_ids) buf.push_back(static_cast<uint8_t>(fid));
// Offsets (1 byte each, num_fields+1 entries)
for (uint32_t o : offsets) buf.push_back(static_cast<uint8_t>(o));
// Value data
for (const auto& v : values) buf.insert(buf.end(), v.begin(), v.end());
return buf;
}
// Build a simple array with given element values.
static vector<uint8_t> BuildArray(const vector<vector<uint8_t>>& elements) {
int num_elements = elements.size();
vector<uint32_t> offsets;
uint32_t offset = 0;
for (const auto& e : elements) {
offsets.push_back(offset);
offset += e.size();
}
offsets.push_back(offset);
// Header: basic_type=3 (ARRAY), offset_size=1 (bits2-3=0), is_large=0 (bit4=0)
// → header = 0x03
uint8_t header = 0x03;
vector<uint8_t> buf = {header};
// Num elements (1 byte since not large)
buf.push_back(static_cast<uint8_t>(num_elements));
// Offsets (1 byte each)
for (uint32_t o : offsets) buf.push_back(static_cast<uint8_t>(o));
// Element data
for (const auto& e : elements) buf.insert(buf.end(), e.begin(), e.end());
return buf;
}
TEST(VariantUtilTest, MetadataParsing) {
vector<string> names = {"age", "city", "name"};
vector<uint8_t> meta_bytes = BuildMetadata(names);
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
EXPECT_EQ(3, metadata.DictionarySize());
EXPECT_EQ(metadata.GetFieldName(0), "age");
EXPECT_EQ(metadata.GetFieldName(1), "city");
EXPECT_EQ(metadata.GetFieldName(2), "name");
EXPECT_EQ(0, metadata.FindFieldId("age"));
EXPECT_EQ(1, metadata.FindFieldId("city"));
EXPECT_EQ(2, metadata.FindFieldId("name"));
EXPECT_EQ(-1, metadata.FindFieldId("unknown"));
}
TEST(VariantUtilTest, NullValue) {
vector<uint8_t> meta_bytes = BuildMetadata({});
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> val_bytes = BuildNull();
VariantValue val(val_bytes.data(), val_bytes.size(), &metadata);
EXPECT_TRUE(val.IsNull());
EXPECT_EQ(val.GetBasicType(), VariantBasicType::PRIMITIVE);
EXPECT_EQ(val.GetPhysicalType(), VariantPhysicalType::VNULL);
}
TEST(VariantUtilTest, BooleanValue) {
vector<uint8_t> meta_bytes = BuildMetadata({});
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> true_bytes = BuildBoolean(true);
VariantValue true_val(true_bytes.data(), true_bytes.size(), &metadata);
EXPECT_FALSE(true_val.IsNull());
EXPECT_TRUE(true_val.GetBoolean());
vector<uint8_t> false_bytes = BuildBoolean(false);
VariantValue false_val(false_bytes.data(), false_bytes.size(), &metadata);
EXPECT_FALSE(false_val.GetBoolean());
}
TEST(VariantUtilTest, Int32Value) {
vector<uint8_t> meta_bytes = BuildMetadata({});
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> val_bytes = BuildInt32(42);
VariantValue val(val_bytes.data(), val_bytes.size(), &metadata);
EXPECT_EQ(val.GetBasicType(), VariantBasicType::PRIMITIVE);
EXPECT_EQ(val.GetPhysicalType(), VariantPhysicalType::INT32);
EXPECT_EQ(42, val.GetInt32());
}
TEST(VariantUtilTest, ShortString) {
vector<uint8_t> meta_bytes = BuildMetadata({});
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> val_bytes = BuildShortString("hello");
VariantValue val(val_bytes.data(), val_bytes.size(), &metadata);
EXPECT_EQ(val.GetBasicType(), VariantBasicType::SHORT_STRING);
StringValue sv = val.GetString();
EXPECT_EQ(string(sv.Ptr(), sv.Len()), "hello");
}
TEST(VariantUtilTest, SimpleObject) {
// Object with fields: {"age": 30, "name": "Alice"}
vector<string> names = {"age", "name"};
vector<uint8_t> meta_bytes = BuildMetadata(names);
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> age_val = BuildInt32(30);
vector<uint8_t> name_val = BuildShortString("Alice");
vector<uint8_t> obj_bytes = BuildObject({0, 1}, {age_val, name_val});
VariantValue obj(obj_bytes.data(), obj_bytes.size(), &metadata);
EXPECT_EQ(obj.GetBasicType(), VariantBasicType::OBJECT);
EXPECT_EQ(2, obj.GetObjectSize());
VariantValue field_val;
EXPECT_TRUE(obj.GetFieldByName("age", &field_val));
EXPECT_EQ(30, field_val.GetInt32());
EXPECT_TRUE(obj.GetFieldByName("name", &field_val));
StringValue sv = field_val.GetString();
EXPECT_EQ(string(sv.Ptr(), sv.Len()), "Alice");
EXPECT_FALSE(obj.GetFieldByName("unknown", &field_val));
}
TEST(VariantUtilTest, SimpleArray) {
vector<uint8_t> meta_bytes = BuildMetadata({});
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> elem0 = BuildInt32(1);
vector<uint8_t> elem1 = BuildInt32(2);
vector<uint8_t> elem2 = BuildInt32(3);
vector<uint8_t> arr_bytes = BuildArray({elem0, elem1, elem2});
VariantValue arr(arr_bytes.data(), arr_bytes.size(), &metadata);
EXPECT_EQ(arr.GetBasicType(), VariantBasicType::ARRAY);
EXPECT_EQ(3, arr.GetArraySize());
VariantValue elem;
EXPECT_TRUE(arr.GetArrayElement(0, &elem));
EXPECT_EQ(1, elem.GetInt32());
EXPECT_TRUE(arr.GetArrayElement(2, &elem));
EXPECT_EQ(3, elem.GetInt32());
EXPECT_FALSE(arr.GetArrayElement(3, &elem));
}
TEST(VariantUtilTest, PathNavigation) {
// {"data": {"items": [10, 20, 30]}}
vector<string> names = {"data", "items"};
vector<uint8_t> meta_bytes = BuildMetadata(names);
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> items = BuildArray(
{BuildInt32(10), BuildInt32(20), BuildInt32(30)});
vector<uint8_t> inner_obj = BuildObject({1}, {items}); // field_id 1 = "items"
vector<uint8_t> outer_obj = BuildObject({0}, {inner_obj}); // field_id 0 = "data"
VariantValue root(outer_obj.data(), outer_obj.size(), &metadata);
VariantValue result;
EXPECT_TRUE(root.NavigatePath("$.data.items[1]", &result));
EXPECT_EQ(20, result.GetInt32());
EXPECT_TRUE(root.NavigatePath("$.data.items[2]", &result));
EXPECT_EQ(30, result.GetInt32());
EXPECT_FALSE(root.NavigatePath("$.data.unknown", &result));
EXPECT_FALSE(root.NavigatePath("$.data.items[100]", &result));
}
TEST(VariantUtilTest, PathNavigationInvalidPaths) {
// Test variant with invalid paths:
// {
// "context":{"page":"home","retries":3},
// "device":"mobile","
// "int_array":[1,2,3,4,10],
// "user":"Alice"
// }
vector<uint8_t> meta_bytes{
0x01, 0x06, 0x00, 0x07, 0x0b, 0x12, 0x18, 0x21, 0x25, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x78, 0x74, 0x70, 0x61, 0x67, 0x65, 0x72, 0x65, 0x74, 0x72,
0x69, 0x65, 0x73, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x69, 0x6e, 0x74,
0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x75, 0x73, 0x65, 0x72};
vector<uint8_t> value_bytes{
0x02, 0x04, 0x00, 0x03, 0x04, 0x05, 0x00, 0x0e, 0x15, 0x27, 0x2d, 0x02,
0x02, 0x01, 0x02, 0x00, 0x05, 0x07, 0x11, 0x68, 0x6f, 0x6d, 0x65, 0x0c,
0x03, 0x19, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x03, 0x05, 0x00, 0x02,
0x04, 0x06, 0x08, 0x0a, 0x0c, 0x01, 0x0c, 0x02, 0x0c, 0x03, 0x0c, 0x04,
0x0c, 0x0a, 0x15, 0x41, 0x6c, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
VariantValue root(value_bytes.data(), value_bytes.size(), &metadata);
VariantValue result;
EXPECT_FALSE(root.NavigatePath("", &result));
EXPECT_TRUE(root.NavigatePath("$", &result));
EXPECT_FALSE(root.NavigatePath("@", &result));
EXPECT_FALSE(root.NavigatePath(".", &result));
EXPECT_FALSE(root.NavigatePath("..", &result));
EXPECT_FALSE(root.NavigatePath("$.", &result));
EXPECT_FALSE(root.NavigatePath(".$", &result));
EXPECT_FALSE(root.NavigatePath("$..", &result));
EXPECT_FALSE(root.NavigatePath("$.context..page", &result));
EXPECT_TRUE(root.NavigatePath("$.int_array[0]", &result));
EXPECT_FALSE(root.NavigatePath("$.int_array[]", &result));
EXPECT_FALSE(root.NavigatePath("$.int_array]100[", &result));
EXPECT_FALSE(root.NavigatePath("$.int_array[100]", &result));
}
TEST(VariantUtilTest, ToJson) {
// {"age": 30, "name": "Alice", "active": true}
vector<string> names = {"active", "age", "name"};
vector<uint8_t> meta_bytes = BuildMetadata(names);
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> active_val = BuildBoolean(true);
vector<uint8_t> age_val = BuildInt32(30);
vector<uint8_t> name_val = BuildShortString("Alice");
vector<uint8_t> obj_bytes = BuildObject({1, 2, 0}, {age_val, name_val, active_val});
string json;
ASSERT_OK(VariantToJson(meta_bytes.data(), meta_bytes.size(),
obj_bytes.data(), obj_bytes.size(), &json));
// Field order in JSON follows the object's field order, not alphabetical.
EXPECT_EQ(json, R"V({"age":30,"name":"Alice","active":true})V");
}
TEST(VariantUtilTest, JsonEscaping) {
// Field names with special characters must be JSON-escaped.
vector<string> names = {"a\"b", "c\\d", "e\nf"};
vector<uint8_t> meta_bytes = BuildMetadata(names);
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> val0 = BuildInt32(1);
vector<uint8_t> val1 = BuildInt32(2);
vector<uint8_t> val2 = BuildInt32(3);
vector<uint8_t> obj_bytes = BuildObject({0, 1, 2}, {val0, val1, val2});
string json;
ASSERT_OK(VariantToJson(meta_bytes.data(), meta_bytes.size(),
obj_bytes.data(), obj_bytes.size(), &json));
EXPECT_EQ(json, R"V({"a\"b":1,"c\\d":2,"e\nf":3})V");
}
TEST(VariantUtilTest, JsonEscapingControlChars) {
// Control characters U+0000 through U+001F must be escaped per RFC 8259.
// Test \b (0x08), \f (0x0C), and a generic control char (0x01).
string s_backspace = "a\bb";
string s_formfeed = "c\fd";
string s_ctrl = string("e") + '\x01' + "f";
vector<string> names = {s_backspace, s_ctrl, s_formfeed};
vector<uint8_t> meta_bytes = BuildMetadata(names);
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
vector<uint8_t> val0 = BuildInt32(10);
vector<uint8_t> val1 = BuildInt32(20);
vector<uint8_t> val2 = BuildInt32(30);
vector<uint8_t> obj_bytes = BuildObject({0, 1, 2}, {val0, val1, val2});
string json;
ASSERT_OK(VariantToJson(meta_bytes.data(), meta_bytes.size(),
obj_bytes.data(), obj_bytes.size(), &json));
EXPECT_EQ(json, "{\"a\\bb\":10,\"e\\u0001f\":20,\"c\\fd\":30}");
}
TEST(VariantUtilTest, JsonEscapingStringValues) {
// String values with control characters are also properly escaped.
vector<uint8_t> meta_bytes = BuildMetadata({});
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
// Short string containing tab, quote, backslash, and a NULL byte.
string raw = string("a\t\"\\") + '\x00' + "b";
vector<uint8_t> val_bytes = BuildShortString(raw);
string json;
ASSERT_OK(VariantToJson(meta_bytes.data(), meta_bytes.size(),
val_bytes.data(), val_bytes.size(), &json));
EXPECT_EQ(json, "\"a\\t\\\"\\\\\\u0000b\"");
}
TEST(VariantUtilTest, HiveVariants) {
// Tests parsing VARIANT values written by Hive.
// Each entry: {metadata_bytes, value_bytes, expected_json}.
struct TestCase {
vector<uint8_t> metadata;
vector<uint8_t> value;
string expected_json;
};
vector<TestCase> test_cases = {
// INT64: 10000000000
{{0x01, 0x00, 0x00},
{0x18, 0x00, 0xe4, 0x0b, 0x54, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
R"V(10000000000)V"},
// INT8: 1
{{0x01, 0x00, 0x00},
{0x0c, 0x01},
R"V(1)V"},
// INT8: 2
{{0x01, 0x00, 0x00},
{0x0c, 0x02},
R"V(2)V"},
// INT8: 3
{{0x01, 0x00, 0x00},
{0x0c, 0x03},
R"V(3)V"},
// Array of INT8: [1,2,3]
{{0x01, 0x00, 0x00},
{0x03, 0x03, 0x00, 0x02, 0x04, 0x06, 0x0c, 0x01, 0x0c, 0x02, 0x0c, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
R"V([1,2,3])V"},
// INT8 with trailing byte: 1
{{0x01, 0x00, 0x00},
{0x0c, 0x01, 0x00},
R"V(1)V"},
// Short string: "impala"
{{0x01, 0x00, 0x00},
{0x19, 0x69, 0x6d, 0x70, 0x61, 0x6c, 0x61, 0x00, 0x00, 0x00},
R"V("impala")V"},
// INT32: 100000
{{0x01, 0x00, 0x00},
{0x14, 0xa0, 0x86, 0x01, 0x00, 0x00, 0x00},
R"V(100000)V"},
// Short string: "context"
{{0x01, 0x00, 0x00},
{0x1d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00},
R"V("context")V"},
// DECIMAL4: scale=2, unscaled=314 -> "3.14"
{{0x01, 0x00, 0x00},
{0x20, 0x02, 0x3a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00},
R"V(3.14)V"},
// Short string "home" with 6-field metadata dictionary
{{0x01, 0x06, 0x00, 0x07, 0x0b, 0x12, 0x18, 0x21, 0x25, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x78, 0x74, 0x70, 0x61, 0x67, 0x65, 0x72, 0x65, 0x74, 0x72,
0x69, 0x65, 0x73, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x69, 0x6e, 0x74,
0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x75, 0x73, 0x65, 0x72},
{0x11, 0x68, 0x6f, 0x6d, 0x65},
R"V("home")V"},
// Object: {"page":"home","retries":3}
{{0x01, 0x06, 0x00, 0x07, 0x0b, 0x12, 0x18, 0x21, 0x25, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x78, 0x74, 0x70, 0x61, 0x67, 0x65, 0x72, 0x65, 0x74, 0x72,
0x69, 0x65, 0x73, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x69, 0x6e, 0x74,
0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x75, 0x73, 0x65, 0x72},
{0x02, 0x02, 0x01, 0x02, 0x00, 0x05, 0x07, 0x11, 0x68, 0x6f, 0x6d, 0x65,
0x0c, 0x03},
R"V({"page":"home","retries":3})V"},
// Short string: "mobile"
{{0x01, 0x06, 0x00, 0x07, 0x0b, 0x12, 0x18, 0x21, 0x25, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x78, 0x74, 0x70, 0x61, 0x67, 0x65, 0x72, 0x65, 0x74, 0x72,
0x69, 0x65, 0x73, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x69, 0x6e, 0x74,
0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x75, 0x73, 0x65, 0x72},
{0x19, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65},
R"V("mobile")V"},
// Array: [1,2,3,4,10]
{{0x01, 0x06, 0x00, 0x07, 0x0b, 0x12, 0x18, 0x21, 0x25, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x78, 0x74, 0x70, 0x61, 0x67, 0x65, 0x72, 0x65, 0x74, 0x72,
0x69, 0x65, 0x73, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x69, 0x6e, 0x74,
0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x75, 0x73, 0x65, 0x72},
{0x03, 0x05, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x01, 0x0c, 0x02,
0x0c, 0x03, 0x0c, 0x04, 0x0c, 0x0a},
R"V([1,2,3,4,10])V"},
// Short string: "Alice"
{{0x01, 0x06, 0x00, 0x07, 0x0b, 0x12, 0x18, 0x21, 0x25, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x78, 0x74, 0x70, 0x61, 0x67, 0x65, 0x72, 0x65, 0x74, 0x72,
0x69, 0x65, 0x73, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x69, 0x6e, 0x74,
0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x75, 0x73, 0x65, 0x72},
{0x15, 0x41, 0x6c, 0x69, 0x63, 0x65},
R"V("Alice")V"},
// Nested object with all fields
{{0x01, 0x06, 0x00, 0x07, 0x0b, 0x12, 0x18, 0x21, 0x25, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x78, 0x74, 0x70, 0x61, 0x67, 0x65, 0x72, 0x65, 0x74, 0x72,
0x69, 0x65, 0x73, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x69, 0x6e, 0x74,
0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x75, 0x73, 0x65, 0x72},
{0x02, 0x04, 0x00, 0x03, 0x04, 0x05, 0x00, 0x0e, 0x15, 0x27, 0x2d, 0x02,
0x02, 0x01, 0x02, 0x00, 0x05, 0x07, 0x11, 0x68, 0x6f, 0x6d, 0x65, 0x0c,
0x03, 0x19, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x03, 0x05, 0x00, 0x02,
0x04, 0x06, 0x08, 0x0a, 0x0c, 0x01, 0x0c, 0x02, 0x0c, 0x03, 0x0c, 0x04,
0x0c, 0x0a, 0x15, 0x41, 0x6c, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
R"V({"context":{"page":"home","retries":3},"device":"mobile",)V"
R"V("int_array":[1,2,3,4,10],"user":"Alice"})V"},
// INT16: 1000
{{0x01, 0x00, 0x00},
{0x10, 0xe8, 0x03, 0x00},
R"V(1000)V"},
// Boolean: false
{{0x01, 0x00, 0x00},
{0x08},
R"V(false)V"},
// Boolean: true
{{0x01, 0x00, 0x00},
{0x04},
R"V(true)V"},
};
for (int i = 0; i < test_cases.size(); ++i) {
const TestCase& tc = test_cases[i];
string json;
ASSERT_OK(VariantToJson(tc.metadata.data(), tc.metadata.size(),
tc.value.data(), tc.value.size(), &json));
EXPECT_EQ(json, tc.expected_json) << "Mismatch on test case " << i;
}
}
// Test metadata with offset_size > 1 (2-byte offsets).
TEST(VariantUtilTest, MetadataLargeOffsetSize) {
// Build metadata manually with offset_size=2 (offset_size_minus_one=1).
// Header: version=1, sorted=1, offset_size_minus_one=1
// Bits: version(0-3)=1, sorted(4)=1, reserved(5)=0, offset_size_minus_one(6-7)=1
uint8_t header = 0x01 | (1 << 4) | (1 << 6); // = 0x51
vector<uint8_t> buf = {header};
// Dictionary size: 2 entries, encoded as 2 bytes (little-endian)
buf.push_back(0x02);
buf.push_back(0x00);
// Offsets: 3 entries (dict_size + 1) of 2 bytes each
// String "hello" at offset 0, "world" at offset 5, end at offset 10
buf.push_back(0x00); buf.push_back(0x00); // offset[0] = 0
buf.push_back(0x05); buf.push_back(0x00); // offset[1] = 5
buf.push_back(0x0a); buf.push_back(0x00); // offset[2] = 10
// String data
for (char c : string("helloworld")) buf.push_back(static_cast<uint8_t>(c));
VariantMetadata metadata;
ASSERT_OK(metadata.Init(buf.data(), buf.size()));
EXPECT_EQ(2, metadata.DictionarySize());
EXPECT_EQ(metadata.GetFieldName(0), "hello");
EXPECT_EQ(metadata.GetFieldName(1), "world");
}
// Test object with field_id_size != offset_size.
TEST(VariantUtilTest, ObjectDifferentFieldIdAndOffsetSize) {
constexpr uint32_t DICT_SIZE = 300;
vector<string> names;
names.reserve(DICT_SIZE);
for (int i = 0; i < DICT_SIZE; ++i) names.push_back("f" + std::to_string(i));
// Build metadata with offset_size=2 to accommodate large offsets.
uint8_t meta_header = 0x01 | (1 << 4) | (1 << 6); // version=1, sorted=1, os=2
vector<uint8_t> meta_buf = {meta_header};
// dict_size = 300, 2 bytes little-endian
meta_buf.push_back(0x2c);
meta_buf.push_back(0x01);
// Offsets: 301 entries of 2 bytes each
uint32_t offset = 0;
for (size_t i = 0; i <= names.size(); ++i) {
meta_buf.push_back(static_cast<uint8_t>(offset & 0xFF));
meta_buf.push_back(static_cast<uint8_t>((offset >> 8) & 0xFF));
if (i < names.size()) offset += names[i].size();
}
// String data
for (const string& name : names) {
for (char c : name) meta_buf.push_back(static_cast<uint8_t>(c));
}
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_buf.data(), meta_buf.size()));
// Build an object with field_id_size=2 (since field_ids > 255) and offset_size=1.
// Object header byte: basic_type=2 (bits0-1), field_offset_size_minus_one=0 (bits2-3),
// field_id_size_minus_one=1 (bits4-5), is_large=0 (bit6)
// = 0x02 | (0 << 2) | (1 << 4) = 0x12
int field_id_size = 2;
int offset_size = 1;
uint8_t obj_header = 0x02 | (((offset_size - 1) & 0x03) << 2)
| (((field_id_size - 1) & 0x03) << 4);
// Two fields: field_id=0 ("f0") with int32 value 42, field_id=299 ("f299") with int32 7
vector<uint8_t> val0 = BuildInt32(42);
vector<uint8_t> val1 = BuildInt32(7);
int num_fields = 2;
vector<uint8_t> obj_buf = {obj_header};
// num_fields: 1 byte (not large)
obj_buf.push_back(static_cast<uint8_t>(num_fields));
// Field IDs: 2 bytes each (little-endian)
obj_buf.push_back(0x00); obj_buf.push_back(0x00); // field_id = 0
obj_buf.push_back(0x2b); obj_buf.push_back(0x01); // field_id = 299
// Offsets: 1 byte each, (num_fields + 1) entries
obj_buf.push_back(0x00); // offset[0] = 0
obj_buf.push_back(static_cast<uint8_t>(val0.size())); // offset[1] = 5
obj_buf.push_back(static_cast<uint8_t>(val0.size() + val1.size())); // offset[2] = 10
// Value data
obj_buf.insert(obj_buf.end(), val0.begin(), val0.end());
obj_buf.insert(obj_buf.end(), val1.begin(), val1.end());
VariantValue obj(obj_buf.data(), obj_buf.size(), &metadata);
EXPECT_EQ(obj.GetBasicType(), VariantBasicType::OBJECT);
EXPECT_EQ(2, obj.GetObjectSize());
VariantValue field_val;
EXPECT_TRUE(obj.GetFieldByName("f0", &field_val));
EXPECT_EQ(42, field_val.GetInt32());
EXPECT_TRUE(obj.GetFieldByName("f299", &field_val));
EXPECT_EQ(7, field_val.GetInt32());
EXPECT_FALSE(obj.GetFieldByName("f1", &field_val));
}
// Test long STRING primitive (basic_type=0, physical_type=STRING=16).
TEST(VariantUtilTest, LongStringPrimitive) {
vector<uint8_t> meta_bytes = BuildMetadata({});
VariantMetadata metadata;
ASSERT_OK(metadata.Init(meta_bytes.data(), meta_bytes.size()));
// Long string encoding: basic_type=0 (PRIMITIVE), type_info=16 (STRING)
// Header byte: (16 << 2) | 0 = 0x40
uint8_t header = 0x40;
vector<uint8_t> val_buf = {header};
string long_str(100, 'x');
// Followed by 4-byte little-endian length
uint32_t len = long_str.size();
val_buf.push_back(static_cast<uint8_t>(len & 0xFF));
val_buf.push_back(static_cast<uint8_t>((len >> 8) & 0xFF));
val_buf.push_back(static_cast<uint8_t>((len >> 16) & 0xFF));
val_buf.push_back(static_cast<uint8_t>((len >> 24) & 0xFF));
// Then string data
for (char c : long_str) val_buf.push_back(static_cast<uint8_t>(c));
VariantValue val(val_buf.data(), val_buf.size(), &metadata);
EXPECT_EQ(val.GetBasicType(), VariantBasicType::PRIMITIVE);
EXPECT_EQ(val.GetPhysicalType(), VariantPhysicalType::STRING);
StringValue sv = val.GetString();
EXPECT_EQ(string(sv.Ptr(), sv.Len()), long_str);
}
TEST(VariantUtilTest, DuckDbVariants) {
// Tests parsing VARIANT values from DuckDB's unshredded Parquet test files.
// Source: duckdb/data/parquet-testing/variant_*.parquet (non-shredded only).
// Results can be checked in duckdb/test/parquet/variant/variant_basic.test
struct TestCase {
vector<uint8_t> metadata;
vector<uint8_t> value;
string expected_json;
};
vector<TestCase> test_cases = {
// variant_null.parquet: NULL
{{0x01, 0x00, 0x00},
{0x00},
R"V(null)V"},
// variant_bool_true.parquet: true
{{0x01, 0x00, 0x00},
{0x04},
R"V(true)V"},
// variant_bool_false.parquet: false
{{0x01, 0x00, 0x00},
{0x08},
R"V(false)V"},
// variant_int8_positive.parquet: 34
{{0x01, 0x00, 0x00},
{0x0c, 0x22},
R"V(34)V"},
// variant_int8_negative.parquet: -34
{{0x01, 0x00, 0x00},
{0x0c, 0xde},
R"V(-34)V"},
// variant_int16.parquet: -1234
{{0x01, 0x00, 0x00},
{0x10, 0x2e, 0xfb},
R"V(-1234)V"},
// variant_int32.parquet: -12345
{{0x01, 0x00, 0x00},
{0x14, 0xc7, 0xcf, 0xff, 0xff},
R"V(-12345)V"},
// variant_int32_positive.parquet: 12345
{{0x01, 0x00, 0x00},
{0x14, 0x39, 0x30, 0x00, 0x00},
R"V(12345)V"},
// variant_string.parquet: short string "iceberg"
{{0x01, 0x00, 0x00},
{0x1d, 0x69, 0x63, 0x65, 0x62, 0x65, 0x72, 0x67},
R"V("iceberg")V"},
// variant_double_positive.parquet: 14.3
{{0x01, 0x00, 0x00},
{0x1c, 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0x2c, 0x40},
R"V(14.3)V"},
// variant_double_negative.parquet: -14.3
{{0x01, 0x00, 0x00},
{0x1c, 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0x2c, 0xc0},
R"V(-14.3)V"},
// variant_float_negative.parquet: -10.11
{{0x01, 0x00, 0x00},
{0x38, 0x8f, 0xc2, 0x21, 0xc1},
R"V(-10.11)V"},
// variant_decimal4_positive.parquet: 123456.789 (scale=3, unscaled=123456789)
{{0x01, 0x00, 0x00},
{0x20, 0x03, 0x15, 0xcd, 0x5b, 0x07},
R"V(123456.789)V"},
// variant_decimal4_negative.parquet: -123456.789
{{0x01, 0x00, 0x00},
{0x20, 0x03, 0xeb, 0x32, 0xa4, 0xf8},
R"V(-123456.789)V"},
// variant_decimal8_negative.parquet: -123456789.987654321 (scale=9)
{{0x01, 0x00, 0x00},
{0x24, 0x09, 0x4f, 0x05, 0xad, 0x1f, 0xb4, 0x64, 0x49, 0xfe},
R"V(-123456789.987654321)V"},
// variant_decimal16.parquet: 9876543210.123456789 (scale=9)
{{0x01, 0x00, 0x00},
{0x28, 0x09, 0x15, 0x71, 0x34, 0xb0, 0xb8, 0x87, 0x10, 0x89, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
R"V(9876543210.123456789)V"},
// variant_array_empty.parquet: []
{{0x01, 0x00, 0x00},
{0x03, 0x00, 0x00},
R"V([])V"},
// variant_array_string.parquet: ["iceberg","string"]
{{0x01, 0x00, 0x00},
{0x03, 0x02, 0x00, 0x08, 0x0f, 0x1d, 0x69, 0x63, 0x65, 0x62, 0x65, 0x72,
0x67, 0x19, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67},
R"V(["iceberg","string"])V"},
// variant_array_array_string.parquet: [["string","iceberg"],["apple","banana"]]
{{0x01, 0x00, 0x00},
{0x03, 0x02, 0x00, 0x14, 0x26, 0x03, 0x02, 0x00, 0x07, 0x0f, 0x19, 0x73,
0x74, 0x72, 0x69, 0x6e, 0x67, 0x1d, 0x69, 0x63, 0x65, 0x62, 0x65, 0x72,
0x67, 0x03, 0x02, 0x00, 0x06, 0x0d, 0x15, 0x61, 0x70, 0x70, 0x6c, 0x65,
0x19, 0x62, 0x61, 0x6e, 0x61, 0x6e, 0x61},
R"V([["string","iceberg"],["apple","banana"]])V"},
// variant_array_array_string_and_integer.parquet:
// [["string","iceberg",34],[34,null],[],["string","iceberg"],34]
{{0x01, 0x00, 0x00},
{0x03, 0x05, 0x00, 0x1a, 0x25, 0x28, 0x3c, 0x41, 0x03, 0x03, 0x00, 0x07,
0x0f, 0x14, 0x19, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x1d, 0x69, 0x63,
0x65, 0x62, 0x65, 0x72, 0x67, 0x14, 0x22, 0x00, 0x00, 0x00, 0x03, 0x02,
0x00, 0x05, 0x06, 0x14, 0x22, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x03, 0x02, 0x00, 0x07, 0x0f, 0x19, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67,
0x1d, 0x69, 0x63, 0x65, 0x62, 0x65, 0x72, 0x67, 0x14, 0x22, 0x00, 0x00,
0x00},
R"V([["string","iceberg",34],[34,null],[],["string","iceberg"],34])V"},
// variant_array_object_string_and_integer.parquet:
// [{"a":123456789,"c":"string"},{"a":123456789,"c":"string"},"iceberg",34]
// Metadata dict: [a, b, c, d, e] (sorted)
{{0x11, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x61, 0x62, 0x63, 0x64,
0x65},
{0x03, 0x04, 0x00, 0x13, 0x26, 0x2e, 0x33, 0x02, 0x02, 0x00, 0x02, 0x00,
0x05, 0x0c, 0x14, 0x15, 0xcd, 0x5b, 0x07, 0x19, 0x73, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x02, 0x02, 0x00, 0x02, 0x00, 0x05, 0x0c, 0x14, 0x15, 0xcd,
0x5b, 0x07, 0x19, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x1d, 0x69, 0x63,
0x65, 0x62, 0x65, 0x72, 0x67, 0x14, 0x22, 0x00, 0x00, 0x00},
R"V([{"a":123456789,"c":"string"},{"a":123456789,"c":"string"},"iceberg",34])V"},
// variant_object_empty.parquet: {}
{{0x01, 0x00, 0x00},
{0x02, 0x00, 0x00},
R"V({})V"},
// variant_object_null_and_string.parquet: {"a":null,"d":"iceberg"}
// Metadata dict: [a, b, c, d, e] (sorted)
{{0x11, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x61, 0x62, 0x63, 0x64,
0x65},
{0x02, 0x02, 0x00, 0x03, 0x00, 0x01, 0x09, 0x00, 0x1d, 0x69, 0x63, 0x65,
0x62, 0x65, 0x72, 0x67},
R"V({"a":null,"d":"iceberg"})V"},
// variant_object_string_and_array.parquet: {"a":123456789,"c":["string","iceberg"]}
// Metadata dict: [a, b, c, d, e] (sorted)
{{0x11, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x61, 0x62, 0x63, 0x64,
0x65},
{0x02, 0x02, 0x00, 0x02, 0x00, 0x05, 0x19, 0x14, 0x15, 0xcd, 0x5b, 0x07,
0x03, 0x02, 0x00, 0x07, 0x0f, 0x19, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67,
0x1d, 0x69, 0x63, 0x65, 0x62, 0x65, 0x72, 0x67},
R"V({"a":123456789,"c":["string","iceberg"]})V"},
// variant_date_positive.parquet: 2024-11-07 (20034 days since epoch)
{{0x01, 0x00, 0x00},
{0x2c, 0x42, 0x4e, 0x00, 0x00},
R"V("2024-11-07")V"},
// variant_date_negative.parquet: 1957-11-07 (-4438 days since epoch)
{{0x01, 0x00, 0x00},
{0x2c, 0xaa, 0xee, 0xff, 0xff},
R"V("1957-11-07")V"},
// variant_binary.parquet: binary value (4 bytes: 0a 0b 0c 0d) -> base64
{{0x01, 0x00, 0x00},
{0x3c, 0x04, 0x00, 0x00, 0x00, 0x0a, 0x0b, 0x0c, 0x0d},
R"V("CgsMDQ==")V"},
// variant_time_ntz.parquet: TIME type
{{0x01, 0x00, 0x00},
{0x44, 0xc0, 0xf2, 0x29, 0x88, 0x0a, 0x00, 0x00, 0x00},
R"V("<unsupported-type>")V"},
// variant_timestamp_micros_positive.parquet: TIMESTAMPTZ
{{0x01, 0x00, 0x00},
{0x30, 0xc0, 0xb2, 0xf0, 0xd8, 0x51, 0x26, 0x06, 0x00},
R"V("<unsupported-type>")V"},
// variant_timestamp_micros_ntz_positive.parquet: TIMESTAMPNTZ
{{0x01, 0x00, 0x00},
{0x34, 0xc0, 0xb2, 0xf0, 0xd8, 0x51, 0x26, 0x06, 0x00},
R"V("2024-11-07 12:33:54.123456000")V"},
// variant_timestamp_nanos1.parquet: TIMESTAMPTZ_NANOS
{{0x01, 0x00, 0x00},
{0x48, 0x15, 0x41, 0x52, 0xd4, 0x94, 0xe5, 0xad, 0xfa},
R"V("<unsupported-type>")V"},
// variant_timestamp_nanos_ntz.parquet: TIMESTAMPNTZ_NANOS
{{0x01, 0x00, 0x00},
{0x4c, 0x15, 0x41, 0x52, 0xd4, 0x94, 0xe5, 0xad, 0xfa},
R"V("1957-11-07 12:33:54.123456789")V"},
// variant_uuid.parquet: UUID
{{0x01, 0x00, 0x00},
{0x50, 0xf2, 0x4f, 0x9b, 0x64, 0x81, 0xfa, 0x49, 0xd1, 0xb7, 0x4e, 0x8c,
0x09, 0xa6, 0xe3, 0x1c, 0x56},
R"V("<unsupported-type>")V"},
};
for (int i = 0; i < test_cases.size(); ++i) {
const TestCase& tc = test_cases[i];
string json;
ASSERT_OK(VariantToJson(tc.metadata.data(), tc.metadata.size(),
tc.value.data(), tc.value.size(), &json));
EXPECT_EQ(json, tc.expected_json) << "Mismatch on test case " << i;
}
}
} // namespace impala