blob: 42662c7262cd1b9c59f15627c72333aaef0530bc [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 "core/value/variant/variant_canonical.h"
#include <crc32c/crc32c.h>
#include <gtest/gtest.h>
#include <array>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <random>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "common/exception.h"
#include "core/value/variant/variant_parquet_encoding.h"
#include "exec/common/sip_hash.h"
#include "variant_test_utils.h"
namespace doris {
namespace {
struct OwnedValue {
std::string metadata;
std::string value;
VariantRef ref() const {
return {.metadata = {.data = metadata.data(), .size = metadata.size()},
.value = {value.data(), value.size()}};
}
};
struct Hashes {
uint64_t sip;
uint64_t xx;
uint32_t crc;
uint32_t crc32c;
};
void append_unsigned(std::string& output, unsigned __int128 value, uint8_t width) {
for (uint8_t index = 0; index < width; ++index) {
output.push_back(static_cast<char>(value >> (index * 8)));
}
}
void append_signed(std::string& output, __int128 value, uint8_t width) {
append_unsigned(output, static_cast<unsigned __int128>(value), width);
}
uint32_t read_u32(const char* data) {
uint32_t value = 0;
for (uint8_t index = 0; index < sizeof(uint32_t); ++index) {
value |= static_cast<uint32_t>(static_cast<uint8_t>(data[index])) << (index * 8);
}
return value;
}
uint32_t read_width(const char* data, uint8_t width) {
uint32_t value = 0;
for (uint8_t index = 0; index < width; ++index) {
value |= static_cast<uint32_t>(static_cast<uint8_t>(data[index])) << (index * 8);
}
return value;
}
void write_u32(char* data, uint32_t value) {
for (uint8_t index = 0; index < sizeof(uint32_t); ++index) {
data[index] = static_cast<char>(value >> (index * 8));
}
}
std::string encode_metadata(const std::vector<std::string>& keys, bool sorted) {
size_t strings_size = 0;
for (const std::string& key : keys) {
strings_size += key.size();
}
EXPECT_LE(keys.size(), std::numeric_limits<uint8_t>::max());
EXPECT_LE(strings_size, std::numeric_limits<uint8_t>::max());
std::string metadata;
metadata.push_back(static_cast<char>(VARIANT_ENCODING_VERSION |
(sorted ? VARIANT_METADATA_SORTED_STRINGS_MASK : 0)));
metadata.push_back(static_cast<char>(keys.size()));
metadata.push_back(0);
uint8_t offset = 0;
for (const std::string& key : keys) {
offset += static_cast<uint8_t>(key.size());
metadata.push_back(static_cast<char>(offset));
}
for (const std::string& key : keys) {
metadata.append(key);
}
return metadata;
}
std::string empty_metadata() {
return encode_metadata({}, true);
}
OwnedValue scalar(std::string value) {
return {.metadata = empty_metadata(), .value = std::move(value)};
}
std::string primitive(VariantPrimitiveId id, std::string payload = {}) {
std::string value;
value.push_back(static_cast<char>(static_cast<uint8_t>(id) << VARIANT_VALUE_HEADER_SHIFT));
value.append(payload);
return value;
}
OwnedValue integer_value(int64_t value, uint8_t width) {
VariantPrimitiveId id = VariantPrimitiveId::INT64;
if (width == 1) {
id = VariantPrimitiveId::INT8;
} else if (width == 2) {
id = VariantPrimitiveId::INT16;
} else if (width == 4) {
id = VariantPrimitiveId::INT32;
}
std::string payload;
append_signed(payload, value, width);
return scalar(primitive(id, payload));
}
OwnedValue decimal_value(__int128 unscaled, uint8_t scale, uint8_t width = 16) {
VariantPrimitiveId id = VariantPrimitiveId::DECIMAL16;
if (width == 4) {
id = VariantPrimitiveId::DECIMAL4;
} else if (width == 8) {
id = VariantPrimitiveId::DECIMAL8;
}
std::string payload(1, static_cast<char>(scale));
append_signed(payload, unscaled, width);
return scalar(primitive(id, payload));
}
OwnedValue double_value(double value) {
std::string payload;
append_unsigned(payload, std::bit_cast<uint64_t>(value), sizeof(uint64_t));
return scalar(primitive(VariantPrimitiveId::DOUBLE, payload));
}
OwnedValue double_bits(uint64_t bits) {
std::string payload;
append_unsigned(payload, bits, sizeof(uint64_t));
return scalar(primitive(VariantPrimitiveId::DOUBLE, payload));
}
OwnedValue float_value(float value) {
std::string payload;
append_unsigned(payload, std::bit_cast<uint32_t>(value), sizeof(uint32_t));
return scalar(primitive(VariantPrimitiveId::FLOAT, payload));
}
OwnedValue float_bits(uint32_t bits) {
std::string payload;
append_unsigned(payload, bits, sizeof(uint32_t));
return scalar(primitive(VariantPrimitiveId::FLOAT, payload));
}
OwnedValue fixed_value(VariantPrimitiveId id, int64_t value, uint8_t width) {
std::string payload;
append_signed(payload, value, width);
return scalar(primitive(id, payload));
}
OwnedValue string_value(std::string_view text, bool force_long = false) {
std::string value;
if (!force_long && text.size() <= VARIANT_MAX_SHORT_STRING_SIZE) {
value.push_back(static_cast<char>((text.size() << VARIANT_VALUE_HEADER_SHIFT) |
static_cast<uint8_t>(VariantBasicType::SHORT_STRING)));
value.append(text);
return scalar(std::move(value));
}
value = primitive(VariantPrimitiveId::STRING);
append_unsigned(value, text.size(), sizeof(uint32_t));
value.append(text);
return scalar(std::move(value));
}
OwnedValue binary_value(std::string_view bytes) {
std::string value = primitive(VariantPrimitiveId::BINARY);
append_unsigned(value, bytes.size(), sizeof(uint32_t));
value.append(bytes);
return scalar(std::move(value));
}
OwnedValue uuid_value(const std::array<uint8_t, 16>& uuid) {
std::string payload;
for (uint8_t byte : uuid) {
payload.push_back(static_cast<char>(byte));
}
return scalar(primitive(VariantPrimitiveId::UUID, payload));
}
std::string object_bytes(const std::vector<uint32_t>& field_ids,
const std::vector<std::string>& children,
const std::vector<uint32_t>& physical_order) {
EXPECT_EQ(field_ids.size(), children.size());
EXPECT_EQ(field_ids.size(), physical_order.size());
EXPECT_LE(children.size(), std::numeric_limits<uint8_t>::max());
std::vector<uint8_t> offsets(children.size());
std::string values;
for (uint32_t logical_index : physical_order) {
EXPECT_LT(logical_index, children.size());
EXPECT_LE(values.size(), std::numeric_limits<uint8_t>::max());
offsets[logical_index] = static_cast<uint8_t>(values.size());
values.append(children[logical_index]);
}
EXPECT_LE(values.size(), std::numeric_limits<uint8_t>::max());
std::string object;
object.push_back(static_cast<char>(VariantBasicType::OBJECT));
object.push_back(static_cast<char>(children.size()));
for (uint32_t id : field_ids) {
EXPECT_LE(id, std::numeric_limits<uint8_t>::max());
object.push_back(static_cast<char>(id));
}
for (uint8_t offset : offsets) {
object.push_back(static_cast<char>(offset));
}
object.push_back(static_cast<char>(values.size()));
object.append(values);
return object;
}
OwnedValue object_value(std::vector<std::string> dictionary, bool sorted,
const std::vector<uint32_t>& field_ids,
const std::vector<std::string>& children,
const std::vector<uint32_t>& physical_order) {
return {.metadata = encode_metadata(dictionary, sorted),
.value = object_bytes(field_ids, children, physical_order)};
}
std::string array_bytes(const std::vector<std::string>& children) {
std::string values;
std::vector<uint32_t> offsets;
offsets.push_back(0);
for (const std::string& child : children) {
values.append(child);
EXPECT_LE(values.size(), std::numeric_limits<uint32_t>::max());
offsets.push_back(static_cast<uint32_t>(values.size()));
}
uint8_t offset_width = 1;
if (values.size() > std::numeric_limits<uint8_t>::max()) {
offset_width = values.size() <= std::numeric_limits<uint16_t>::max() ? 2 : 4;
}
const auto value_header =
static_cast<uint8_t>((offset_width - 1) << VARIANT_ARRAY_OFFSET_SIZE_SHIFT);
std::string array;
array.push_back(static_cast<char>((value_header << VARIANT_VALUE_HEADER_SHIFT) |
static_cast<uint8_t>(VariantBasicType::ARRAY)));
array.push_back(static_cast<char>(children.size()));
for (uint32_t offset : offsets) {
append_unsigned(array, offset, offset_width);
}
array.append(values);
return array;
}
OwnedValue array_value(const std::vector<std::string>& children) {
return {.metadata = empty_metadata(), .value = array_bytes(children)};
}
Hashes hashes(VariantRef value) {
SipHash sip;
canonical_hash(value, sip);
VariantXxHashSink xx(0x123456789ABCDEF0ULL);
canonical_hash(value, xx);
VariantCrc32HashSink crc(0x13579BDFU);
canonical_hash(value, crc);
VariantCrc32cHashSink crc32c(0x2468ACE0U);
canonical_hash(value, crc32c);
return {.sip = sip.get64(), .xx = xx.digest(), .crc = crc.digest(), .crc32c = crc32c.digest()};
}
Hashes hashes(const VariantScalarRef& value) {
SipHash sip;
canonical_hash(value, sip);
VariantXxHashSink xx(0x123456789ABCDEF0ULL);
canonical_hash(value, xx);
VariantCrc32HashSink crc(0x13579BDFU);
canonical_hash(value, crc);
VariantCrc32cHashSink crc32c(0x2468ACE0U);
canonical_hash(value, crc32c);
return {.sip = sip.get64(), .xx = xx.digest(), .crc = crc.digest(), .crc32c = crc32c.digest()};
}
std::string arena(VariantRef value) {
std::string encoded;
const size_t appended = canonical_serialize(value, encoded);
EXPECT_EQ(appended, encoded.size());
EXPECT_GE(encoded.size(), sizeof(uint32_t));
EXPECT_EQ(read_u32(encoded.data()), encoded.size() - sizeof(uint32_t));
return encoded;
}
std::string arena(const VariantScalarRef& value) {
const CanonicalScalarSerializationPlan plan = prepare_canonical_serialize(value);
std::string encoded(plan.size(), '\0');
plan.write(encoded.data(), encoded.size());
EXPECT_GE(encoded.size(), sizeof(uint32_t));
EXPECT_EQ(read_u32(encoded.data()), encoded.size() - sizeof(uint32_t));
return encoded;
}
std::string physical(const VariantScalarRef& value) {
std::string encoded(value.encoded_size(), '\0');
value.write_physical(encoded.data(), encoded.size());
return encoded;
}
VariantRef arena_value_ref(const std::string& encoded) {
EXPECT_GE(encoded.size(), sizeof(uint32_t) + 3);
EXPECT_EQ(read_u32(encoded.data()), encoded.size() - sizeof(uint32_t));
const char* metadata = encoded.data() + sizeof(uint32_t);
const auto header = static_cast<uint8_t>(metadata[0]);
const auto width = static_cast<uint8_t>(
((header >> VARIANT_METADATA_OFFSET_SIZE_SHIFT) & VARIANT_METADATA_OFFSET_SIZE_MASK) +
1);
const uint32_t count = read_width(metadata + 1, width);
const size_t offsets_position = 1 + width;
const uint32_t strings_size =
read_width(metadata + offsets_position + static_cast<size_t>(count) * width, width);
const size_t metadata_size =
offsets_position + (static_cast<size_t>(count) + 1) * width + strings_size;
EXPECT_LT(sizeof(uint32_t) + metadata_size, encoded.size());
return {.metadata = {.data = metadata, .size = metadata_size},
.value = {metadata + metadata_size, encoded.size() - sizeof(uint32_t) - metadata_size}};
}
void expect_canonical_parse_error(const std::string& encoded, int expected) {
try {
static_cast<void>(parse_canonical_serialized(StringRef(encoded.data(), encoded.size())));
ADD_FAILURE() << "expected canonical cell parse failure";
} catch (const Exception& exception) {
EXPECT_EQ(exception.code(), expected) << exception.message();
}
}
void expect_equivalent(const OwnedValue& left, const OwnedValue& right) {
EXPECT_TRUE(canonical_equals(left.ref(), right.ref()));
const Hashes left_hashes = hashes(left.ref());
const Hashes right_hashes = hashes(right.ref());
EXPECT_EQ(left_hashes.sip, right_hashes.sip);
EXPECT_EQ(left_hashes.xx, right_hashes.xx);
EXPECT_EQ(left_hashes.crc, right_hashes.crc);
EXPECT_EQ(left_hashes.crc32c, right_hashes.crc32c);
EXPECT_EQ(arena(left.ref()), arena(right.ref()));
}
void expect_distinct(const OwnedValue& left, const OwnedValue& right) {
EXPECT_FALSE(canonical_equals(left.ref(), right.ref()));
EXPECT_NE(arena(left.ref()), arena(right.ref()));
}
void expect_scalar_equivalent(const VariantScalarRef& scalar_ref, const OwnedValue& encoded_value) {
EXPECT_TRUE(canonical_equals(scalar_ref, scalar_ref));
const Hashes scalar_hashes = hashes(scalar_ref);
const Hashes encoded_hashes = hashes(encoded_value.ref());
EXPECT_EQ(scalar_hashes.sip, encoded_hashes.sip);
EXPECT_EQ(scalar_hashes.xx, encoded_hashes.xx);
EXPECT_EQ(scalar_hashes.crc, encoded_hashes.crc);
EXPECT_EQ(scalar_hashes.crc32c, encoded_hashes.crc32c);
EXPECT_EQ(arena(scalar_ref), arena(encoded_value.ref()));
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- exhaustive typed/encoded parity matrix.
TEST(VariantCanonicalTest, TypedScalarCanonicalMatchesVariantRef) {
expect_scalar_equivalent(VariantScalarRef::null_value(),
scalar(primitive(VariantPrimitiveId::NULL_VALUE)));
expect_scalar_equivalent(VariantScalarRef::boolean(false),
scalar(primitive(VariantPrimitiveId::FALSE_VALUE)));
expect_scalar_equivalent(VariantScalarRef::boolean(true),
scalar(primitive(VariantPrimitiveId::TRUE_VALUE)));
expect_scalar_equivalent(VariantScalarRef::integer(42), integer_value(42, 1));
expect_scalar_equivalent(
VariantScalarRef::integer(static_cast<__int128>(1'000'000'000'000'000'000LL)),
decimal_value(static_cast<__int128>(1'000'000'000'000'000'000LL), 0, 16));
expect_scalar_equivalent(VariantScalarRef::decimal(12300, 4), decimal_value(123, 2, 4));
expect_scalar_equivalent(VariantScalarRef::decimal(4200, 2), integer_value(42, 1));
expect_scalar_equivalent(VariantScalarRef::float32(1.5F), float_value(1.5F));
expect_scalar_equivalent(VariantScalarRef::float64(42.0), integer_value(42, 1));
expect_scalar_equivalent(VariantScalarRef::float64(-0.0), integer_value(0, 1));
expect_scalar_equivalent(VariantScalarRef::float64(std::numeric_limits<double>::quiet_NaN()),
double_bits(0x7FF0000000000001ULL));
const std::string short_text = "typed string";
const std::string long_text(64, 'L');
const std::string binary("\0\xFF\x01", 3);
expect_scalar_equivalent(VariantScalarRef::string(StringRef(short_text)),
string_value(short_text));
expect_scalar_equivalent(VariantScalarRef::string(StringRef(long_text)),
string_value(long_text, true));
expect_scalar_equivalent(VariantScalarRef::binary(StringRef(binary)), binary_value(binary));
expect_scalar_equivalent(VariantScalarRef::date(-20000),
fixed_value(VariantPrimitiveId::DATE, -20000, 4));
expect_scalar_equivalent(VariantScalarRef::timestamp_micros(-1234567890, true),
fixed_value(VariantPrimitiveId::TIMESTAMP_MICROS, -1234567890, 8));
expect_scalar_equivalent(VariantScalarRef::timestamp_micros(2234567890, false),
fixed_value(VariantPrimitiveId::TIMESTAMP_NTZ_MICROS, 2234567890, 8));
expect_scalar_equivalent(VariantScalarRef::timestamp_nanos(-3234567891, true),
fixed_value(VariantPrimitiveId::TIMESTAMP_NANOS, -3234567891, 8));
expect_scalar_equivalent(VariantScalarRef::timestamp_nanos(4234567891, false),
fixed_value(VariantPrimitiveId::TIMESTAMP_NTZ_NANOS, 4234567891, 8));
expect_scalar_equivalent(VariantScalarRef::time_ntz_micros(5234567890),
fixed_value(VariantPrimitiveId::TIME_NTZ_MICROS, 5234567890, 8));
std::array<uint8_t, 16> uuid {};
for (uint8_t index = 0; index < uuid.size(); ++index) {
uuid[index] = index;
}
expect_scalar_equivalent(VariantScalarRef::uuid(uuid), uuid_value(uuid));
const VariantScalarRef integer_42 = VariantScalarRef::integer(42);
const VariantScalarRef decimal_42 = VariantScalarRef::decimal(4200, 2);
const VariantScalarRef double_42 = VariantScalarRef::float64(42.0);
EXPECT_TRUE(canonical_equals(integer_42, decimal_42));
EXPECT_TRUE(canonical_equals(decimal_42, double_42));
EXPECT_FALSE(canonical_equals(integer_42, VariantScalarRef::string(StringRef("42"))));
EXPECT_FALSE(canonical_equals(integer_42, VariantScalarRef::float64(42.5)));
std::string borrowed = "borrowed";
const VariantScalarRef borrowed_ref = VariantScalarRef::string(StringRef(borrowed));
const CanonicalScalarSerializationPlan borrowed_plan =
prepare_canonical_serialize(borrowed_ref);
borrowed.front() = 'B';
std::string borrowed_arena(borrowed_plan.size(), '\0');
borrowed_plan.write(borrowed_arena.data(), borrowed_arena.size());
EXPECT_EQ(borrowed_arena, arena(string_value(borrowed).ref()));
const CanonicalScalarSerializationPlan null_plan =
prepare_canonical_serialize(VariantScalarRef::null_value());
std::array<char, 16> unchanged {};
unchanged.fill('x');
EXPECT_THROW(null_plan.write(nullptr, null_plan.size()), Exception);
EXPECT_THROW(null_plan.write(unchanged.data(), null_plan.size() - 1), Exception);
EXPECT_TRUE(std::ranges::all_of(unchanged, [](char value) { return value == 'x'; }));
const std::string invalid_utf8("\xC3\x28", 2);
EXPECT_THROW(VariantScalarRef::string(StringRef(invalid_utf8)), Exception);
EXPECT_THROW(VariantScalarRef::decimal(1, 39), Exception);
const __int128 outside_decimal38 = [] {
__int128 value = 1;
for (uint8_t digit = 0; digit < 38; ++digit) {
value *= 10;
}
return value;
}();
EXPECT_THROW(VariantScalarRef::decimal(outside_decimal38, 0), Exception);
const StringRef null_bytes(static_cast<const char*>(nullptr), 1);
EXPECT_THROW(VariantScalarRef::string(null_bytes), Exception);
EXPECT_THROW(VariantScalarRef::binary(null_bytes), Exception);
}
TEST(VariantCanonicalTest, ScalarRefRetainsPhysicalEncodingBeforeCanonicalization) {
const auto expect_ids = [](const VariantScalarRef& scalar_ref, VariantPrimitiveId physical_id,
VariantPrimitiveId canonical_id) {
const OwnedValue physical_value {.metadata = empty_metadata(),
.value = physical(scalar_ref)};
EXPECT_EQ(physical_value.ref().primitive_id(), physical_id);
const std::string canonical = arena(scalar_ref);
EXPECT_EQ(arena_value_ref(canonical).primitive_id(), canonical_id);
};
expect_ids(VariantScalarRef::integer(42, 8), VariantPrimitiveId::INT64,
VariantPrimitiveId::INT8);
expect_ids(VariantScalarRef::decimal(4200, 2, 16), VariantPrimitiveId::DECIMAL16,
VariantPrimitiveId::INT8);
expect_ids(VariantScalarRef::float32(1.5F), VariantPrimitiveId::FLOAT,
VariantPrimitiveId::DOUBLE);
expect_ids(VariantScalarRef::timestamp_nanos(1000, true), VariantPrimitiveId::TIMESTAMP_NANOS,
VariantPrimitiveId::TIMESTAMP_MICROS);
}
TEST(VariantCanonicalTest, NumericEquivalenceMatrix) {
const OwnedValue int8 = integer_value(42, 1);
const OwnedValue int16 = integer_value(42, 2);
const OwnedValue int32 = integer_value(42, 4);
const OwnedValue int64 = integer_value(42, 8);
const OwnedValue decimal_integer = decimal_value(4200, 2, 16);
const OwnedValue double_integer = double_value(42.0);
const OwnedValue float_integer = float_value(42.0F);
expect_equivalent(int8, int16);
expect_equivalent(int8, int32);
expect_equivalent(int8, int64);
expect_equivalent(int8, decimal_integer);
expect_equivalent(int8, double_integer);
expect_equivalent(int8, float_integer);
const OwnedValue decimal_trailing = decimal_value(12300, 4, 16);
const OwnedValue decimal_stripped = decimal_value(123, 2, 4);
expect_equivalent(decimal_trailing, decimal_stripped);
expect_distinct(decimal_stripped, double_value(1.23));
expect_equivalent(float_value(1.5F), double_value(1.5));
expect_distinct(float_value(0.1F), double_value(0.1));
const OwnedValue double_nan_a = double_bits(0x7FF0000000000001ULL);
const OwnedValue double_nan_b = double_bits(0xFFFABCDE12345678ULL);
const OwnedValue float_nan = float_bits(0x7FC12345U);
expect_equivalent(double_nan_a, double_nan_b);
expect_equivalent(double_nan_a, float_nan);
expect_equivalent(double_value(0.0), double_value(-0.0));
expect_equivalent(double_value(-0.0), float_value(-0.0F));
expect_equivalent(double_value(-0.0), integer_value(0, 1));
// The binary64 value spelled 1E38 is slightly below mathematical 10^38 and is encodable;
// its next representable value toward +infinity is the first value outside decimal38.
const double inside_decimal38 = 1E38;
const OwnedValue inside = double_value(inside_decimal38);
const std::string inside_arena = arena(inside.ref());
EXPECT_EQ(arena_value_ref(inside_arena).primitive_id(), VariantPrimitiveId::DECIMAL16);
expect_equivalent(inside, decimal_value(static_cast<__int128>(inside_decimal38), 0, 16));
const OwnedValue outside =
double_value(std::nextafter(1E38, std::numeric_limits<double>::infinity()));
const std::string outside_arena = arena(outside.ref());
EXPECT_EQ(arena_value_ref(outside_arena).primitive_id(), VariantPrimitiveId::DOUBLE);
expect_distinct(inside, outside);
EXPECT_EQ(arena_value_ref(
arena(double_value(
std::nextafter(-1E38, -std::numeric_limits<double>::infinity()))
.ref()))
.primitive_id(),
VariantPrimitiveId::DOUBLE);
}
TEST(VariantCanonicalTest, PrimitiveTypeClasses) {
const OwnedValue null_a = scalar(primitive(VariantPrimitiveId::NULL_VALUE));
const OwnedValue null_b = scalar(primitive(VariantPrimitiveId::NULL_VALUE));
expect_equivalent(null_a, null_b);
const OwnedValue true_value = scalar(primitive(VariantPrimitiveId::TRUE_VALUE));
const OwnedValue false_value = scalar(primitive(VariantPrimitiveId::FALSE_VALUE));
expect_distinct(true_value, false_value);
const OwnedValue short_string = string_value("same bytes");
const OwnedValue long_form = string_value("same bytes", true);
expect_equivalent(short_string, long_form);
expect_distinct(short_string, binary_value("same bytes"));
expect_equivalent(fixed_value(VariantPrimitiveId::DATE, -20000, 4),
fixed_value(VariantPrimitiveId::DATE, -20000, 4));
expect_equivalent(fixed_value(VariantPrimitiveId::TIME_NTZ_MICROS, 1234567, 8),
fixed_value(VariantPrimitiveId::TIME_NTZ_MICROS, 1234567, 8));
expect_distinct(fixed_value(VariantPrimitiveId::DATE, 1, 4), integer_value(1, 1));
std::array<uint8_t, 16> uuid {};
for (uint8_t index = 0; index < uuid.size(); ++index) {
uuid[index] = index;
}
expect_equivalent(uuid_value(uuid), uuid_value(uuid));
uuid.back() ^= 1;
expect_distinct(uuid_value(std::array<uint8_t, 16> {}), uuid_value(uuid));
}
TEST(VariantCanonicalTest, TimestampUnitsNormalizeByLogicalTime) {
const OwnedValue micros = fixed_value(VariantPrimitiveId::TIMESTAMP_MICROS, 1, 8);
const OwnedValue nanos = fixed_value(VariantPrimitiveId::TIMESTAMP_NANOS, 1000, 8);
expect_equivalent(micros, nanos);
EXPECT_EQ(arena_value_ref(arena(nanos.ref())).primitive_id(),
VariantPrimitiveId::TIMESTAMP_MICROS);
const OwnedValue sub_micros = fixed_value(VariantPrimitiveId::TIMESTAMP_NANOS, 1001, 8);
expect_distinct(micros, sub_micros);
EXPECT_EQ(arena_value_ref(arena(sub_micros.ref())).primitive_id(),
VariantPrimitiveId::TIMESTAMP_NANOS);
expect_distinct(micros, fixed_value(VariantPrimitiveId::TIMESTAMP_NTZ_NANOS, 1000, 8));
expect_equivalent(fixed_value(VariantPrimitiveId::TIMESTAMP_NTZ_MICROS, -7, 8),
fixed_value(VariantPrimitiveId::TIMESTAMP_NTZ_NANOS, -7000, 8));
for (int64_t extreme :
{std::numeric_limits<int64_t>::min(), std::numeric_limits<int64_t>::max()}) {
const OwnedValue value = fixed_value(VariantPrimitiveId::TIMESTAMP_MICROS, extreme, 8);
const std::string encoded = arena(value.ref());
const VariantRef canonical = arena_value_ref(encoded);
EXPECT_EQ(canonical.primitive_id(), VariantPrimitiveId::TIMESTAMP_MICROS);
EXPECT_EQ(canonical.get_timestamp_micros(), extreme);
EXPECT_TRUE(canonical_equals(value.ref(), canonical));
}
}
TEST(VariantCanonicalTest, ObjectUsesKeyBytesAcrossMetadataAndPhysicalOrder) {
const std::string one = integer_value(1, 1).value;
const std::string two = integer_value(2, 8).value;
const OwnedValue canonical = object_value({"a", "b"}, true, {0, 1}, {one, two}, {0, 1});
const OwnedValue external =
object_value({"b", "a", "unused"}, false, {1, 0}, {one, two}, {1, 0});
expect_equivalent(canonical, external);
const std::string nested_a = object_bytes({0}, {one}, {0});
const OwnedValue nested_left =
object_value({"a", "inner", "z"}, true, {1, 2}, {nested_a, two}, {0, 1});
const std::string nested_b = object_bytes({1}, {one}, {0});
const OwnedValue nested_right =
object_value({"z", "a", "inner"}, false, {2, 0}, {nested_b, two}, {1, 0});
expect_equivalent(nested_left, nested_right);
const OwnedValue bad_order = object_value({"b", "a"}, false, {0, 1}, {one, two}, {0, 1});
VariantXxHashSink sink;
EXPECT_THROW(canonical_hash(bad_order.ref(), sink), Exception);
std::string destination = "unchanged";
EXPECT_THROW(canonical_serialize(bad_order.ref(), destination), Exception);
EXPECT_EQ(destination, "unchanged");
}
TEST(VariantCanonicalTest, ArrayOrderIsSignificant) {
const std::string one = integer_value(1, 1).value;
const std::string two = integer_value(2, 8).value;
const OwnedValue forward = array_value({one, two});
const OwnedValue same =
array_value({integer_value(1, 8).value, decimal_value(200, 2, 16).value});
const OwnedValue reverse = array_value({two, one});
expect_equivalent(forward, same);
expect_distinct(forward, reverse);
}
TEST(VariantCanonicalTest, Crc32cSinkUsesCanonicalTokensAndCallerSeed) {
const OwnedValue narrow = integer_value(42, 1);
const OwnedValue wide = decimal_value(4200, 2, 16);
constexpr uint32_t SEED = 0x89ABCDEFU;
VariantCrc32cHashSink narrow_sink(SEED);
canonical_hash(narrow.ref(), narrow_sink);
VariantCrc32cHashSink wide_sink(SEED);
canonical_hash(wide.ref(), wide_sink);
EXPECT_EQ(narrow_sink.digest(), wide_sink.digest());
EXPECT_NE(narrow_sink.digest(), SEED);
const std::string fixed_bytes = "variant-crc32c-sink";
VariantCrc32cHashSink identity_sink(SEED);
identity_sink.update(fixed_bytes.data(), fixed_bytes.size());
EXPECT_EQ(identity_sink.digest(),
crc32c_extend(SEED, reinterpret_cast<const uint8_t*>(fixed_bytes.data()),
fixed_bytes.size()));
}
TEST(VariantCanonicalTest, ArenaPrefixAndCanonicalEncoding) {
const OwnedValue input = object_value(
{"b", "a", "unused"}, false, {1, 0},
{decimal_value(4200, 2, 16).value, string_value("short", true).value}, {1, 0});
std::string destination = "prefix";
const size_t appended = canonical_serialize(input.ref(), destination);
ASSERT_GT(appended, sizeof(uint32_t));
EXPECT_EQ(destination.substr(0, 6), "prefix");
const std::string encoded = destination.substr(6);
EXPECT_EQ(read_u32(encoded.data()), appended - sizeof(uint32_t));
const VariantRef canonical = arena_value_ref(encoded);
validate_canonical(canonical);
EXPECT_TRUE(canonical.metadata.sorted_strings());
EXPECT_EQ(canonical.metadata.dict_size(), 2);
EXPECT_EQ(canonical.metadata.key_at(0), StringRef("a", 1));
EXPECT_EQ(canonical.metadata.key_at(1), StringRef("b", 1));
uint32_t field_id = 0;
EXPECT_EQ(canonical.object_value_at(0, &field_id).primitive_id(), VariantPrimitiveId::INT8);
EXPECT_EQ(field_id, 0);
EXPECT_EQ(canonical.object_value_at(1, &field_id).basic_type(), VariantBasicType::SHORT_STRING);
EXPECT_EQ(field_id, 1);
}
TEST(VariantCanonicalTest, ParseCanonicalCellRejectsInvalidBoundaries) {
const OwnedValue input = object_value(
{"a", "nested"}, true, {0, 1},
{integer_value(7, 8).value,
array_bytes({string_value("value", true).value, integer_value(9, 1).value})},
{0, 1});
const std::string encoded = arena(input.ref());
const VariantRef parsed = parse_canonical_serialized(StringRef(encoded.data(), encoded.size()));
EXPECT_EQ(parsed.value_size(), parsed.value.size);
EXPECT_TRUE(canonical_equals(input.ref(), parsed));
expect_canonical_parse_error(encoded.substr(0, sizeof(uint32_t) - 1), ErrorCode::CORRUPTION);
std::string payload_mismatch = encoded;
write_u32(payload_mismatch.data(), read_u32(payload_mismatch.data()) - 1);
expect_canonical_parse_error(payload_mismatch, ErrorCode::CORRUPTION);
std::string truncated_metadata(sizeof(uint32_t) + 1, '\0');
write_u32(truncated_metadata.data(), 1);
truncated_metadata[sizeof(uint32_t)] = static_cast<char>(VARIANT_ENCODING_VERSION);
expect_canonical_parse_error(truncated_metadata, ErrorCode::CORRUPTION);
std::string unsupported_version = encoded;
auto& metadata_header = unsupported_version[sizeof(uint32_t)];
metadata_header = static_cast<char>(
(static_cast<uint8_t>(metadata_header) & ~VARIANT_METADATA_VERSION_MASK) | 2);
expect_canonical_parse_error(unsupported_version, ErrorCode::INVALID_ARGUMENT);
std::string truncated_value = encoded;
truncated_value.pop_back();
write_u32(truncated_value.data(), truncated_value.size() - sizeof(uint32_t));
expect_canonical_parse_error(truncated_value, ErrorCode::CORRUPTION);
std::string trailing_value = encoded;
trailing_value.push_back('\0');
write_u32(trailing_value.data(), trailing_value.size() - sizeof(uint32_t));
expect_canonical_parse_error(trailing_value, ErrorCode::CORRUPTION);
VariantRef nested;
ASSERT_TRUE(parsed.object_find(StringRef("nested"), &nested));
ASSERT_EQ(nested.basic_type(), VariantBasicType::ARRAY);
const size_t nested_offset = nested.value.data - encoded.data();
ASSERT_EQ(static_cast<uint8_t>(encoded[nested_offset + 2]), 0);
std::string invalid_nested_boundary = encoded;
invalid_nested_boundary[nested_offset + 2] = 1;
const VariantRef valid_outer_layout = arena_value_ref(invalid_nested_boundary);
EXPECT_EQ(valid_outer_layout.value_size(), valid_outer_layout.value.size);
expect_canonical_parse_error(invalid_nested_boundary, ErrorCode::CORRUPTION);
const OwnedValue two_fields = object_value(
{"a", "b"}, true, {0, 1},
{primitive(VariantPrimitiveId::NULL_VALUE), primitive(VariantPrimitiveId::TRUE_VALUE)},
{0, 1});
std::string overlapping_object = arena(two_fields.ref());
const VariantRef object_ref = arena_value_ref(overlapping_object);
ASSERT_EQ(object_ref.num_elements(), 2);
const size_t object_offset = object_ref.value.data - overlapping_object.data();
// Small-object layout: header, count, two ids, then three one-byte value offsets.
ASSERT_GE(object_ref.value.size, 9);
overlapping_object[object_offset + 5] = 0;
expect_canonical_parse_error(overlapping_object, ErrorCode::CORRUPTION);
std::string invalid_metadata_utf8 = arena(two_fields.ref());
VariantRef metadata_ref = arena_value_ref(invalid_metadata_utf8);
const size_t metadata_offset = metadata_ref.metadata.data - invalid_metadata_utf8.data();
ASSERT_GE(metadata_ref.metadata.size, 7);
invalid_metadata_utf8[metadata_offset + metadata_ref.metadata.size - 1] =
static_cast<char>(0xFF);
expect_canonical_parse_error(invalid_metadata_utf8, ErrorCode::CORRUPTION);
}
TEST(VariantCanonicalTest, CallerOwnedBufferMatchesStringAndRejectsInsufficientCapacity) {
const OwnedValue input = object_value(
{"nested", "value", "unused"}, false, {0, 1},
{array_bytes({integer_value(7, 8).value, string_value("text", true).value}),
decimal_value(12300, 4, 16).value},
{1, 0});
const std::string string_encoded = arena(input.ref());
CanonicalSerializationPlan plan = prepare_canonical_serialize(input.ref());
ASSERT_EQ(plan.size(), string_encoded.size());
std::vector<char> caller_owned(plan.size(), static_cast<char>(0x5A));
plan.write(caller_owned.data(), caller_owned.size());
EXPECT_EQ(std::string(caller_owned.begin(), caller_owned.end()), string_encoded);
EXPECT_EQ(read_u32(caller_owned.data()), plan.size() - sizeof(uint32_t));
std::vector<char> insufficient(plan.size(), static_cast<char>(0x33));
const std::vector<char> before = insufficient;
ASSERT_GT(plan.size(), 0);
EXPECT_THROW(plan.write(insufficient.data(), plan.size() - 1), Exception);
EXPECT_EQ(insufficient, before);
EXPECT_THROW(plan.write(nullptr, plan.size()), Exception);
}
TEST(VariantCanonicalTest, RejectsTruncatedOverflowingAndTooDeepInputs) {
OwnedValue truncated = double_value(1.5);
truncated.value.resize(4);
EXPECT_THROW(canonical_equals(truncated.ref(), double_value(1.5).ref()), Exception);
VariantCrc32HashSink crc;
EXPECT_THROW(canonical_hash(truncated.ref(), crc), Exception);
std::string destination = "unchanged";
EXPECT_THROW(canonical_serialize(truncated.ref(), destination), Exception);
EXPECT_EQ(destination, "unchanged");
std::string maximal_count;
maximal_count.push_back(
static_cast<char>((VARIANT_ARRAY_LARGE_MASK << VARIANT_VALUE_HEADER_SHIFT) |
static_cast<uint8_t>(VariantBasicType::ARRAY)));
append_unsigned(maximal_count, std::numeric_limits<uint32_t>::max(), sizeof(uint32_t));
const OwnedValue overflowing_layout = scalar(maximal_count);
EXPECT_THROW(canonical_serialize(overflowing_layout.ref(), destination), Exception);
EXPECT_EQ(destination, "unchanged");
const unsigned __int128 int128_max = (static_cast<unsigned __int128>(1) << 127) - 1;
const OwnedValue invalid_decimal = decimal_value(static_cast<__int128>(int128_max), 0, 16);
EXPECT_THROW(canonical_serialize(invalid_decimal.ref(), destination), Exception);
const OwnedValue invalid_utf8 = string_value(std::string("\xFF", 1));
EXPECT_THROW(canonical_serialize(invalid_utf8.ref(), destination), Exception);
std::string deep = integer_value(1, 1).value;
for (uint32_t depth = 0; depth < 130; ++depth) {
deep = array_bytes({deep});
}
const OwnedValue too_deep = scalar(deep);
EXPECT_THROW(canonical_serialize(too_deep.ref(), destination), Exception);
}
OwnedValue equivalent_integer(int64_t value, uint32_t encoding) {
switch (encoding % 5) {
case 0:
return integer_value(value, 8);
case 1:
return decimal_value(static_cast<__int128>(value) * 100, 2, 16);
case 2:
return double_value(static_cast<double>(value));
case 3:
return float_value(static_cast<float>(value));
case 4:
return value >= std::numeric_limits<int8_t>::min() &&
value <= std::numeric_limits<int8_t>::max()
? integer_value(value, 1)
: integer_value(value, 4);
}
return integer_value(value, 8);
}
std::pair<OwnedValue, OwnedValue> random_pair(std::mt19937_64& random, uint32_t index,
bool equivalent) {
const uint32_t mode = index % 5;
int64_t left_number = static_cast<int64_t>(random() % 2'000'001) - 1'000'000;
int64_t right_number =
equivalent ? left_number : static_cast<int64_t>(random() % 2'000'001) - 1'000'000;
if (!equivalent && right_number == left_number) {
++right_number;
}
if (mode == 0) {
return {equivalent_integer(left_number, random()),
equivalent_integer(right_number, random())};
}
if (mode == 1) {
int64_t left_unscaled = left_number * 10 + 3;
int64_t right_unscaled = equivalent ? left_unscaled : right_number * 10 + 7;
return {decimal_value(left_unscaled * 100, 5, 16), decimal_value(right_unscaled, 3, 8)};
}
if (mode == 2) {
const std::string left = "value_" + std::to_string(left_number);
const std::string right = equivalent ? left : "value_" + std::to_string(right_number);
return {string_value(left, (random() & 1U) != 0),
string_value(right, (random() & 1U) != 0)};
}
if (mode == 3) {
const OwnedValue left_first = equivalent_integer(left_number, random());
const OwnedValue right_first = equivalent_integer(right_number, random());
const OwnedValue left_second = equivalent_integer(left_number + 1, random());
const OwnedValue right_second = equivalent_integer(right_number + 1, random());
return {array_value({left_first.value, left_second.value}),
array_value({right_first.value, right_second.value})};
}
const OwnedValue left_first = equivalent_integer(left_number, random());
const OwnedValue right_first = equivalent_integer(right_number, random());
const OwnedValue left_second = equivalent_integer(left_number + 1, random());
const OwnedValue right_second = equivalent_integer(right_number + 1, random());
return {object_value({"a", "b"}, true, {0, 1}, {left_first.value, left_second.value}, {0, 1}),
object_value({"b", "a", "unused"}, false, {1, 0},
{right_first.value, right_second.value}, {1, 0})};
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- GTest assertion macros inflate it.
TEST(VariantCanonicalTest, RandomizedEqualsHashAndArenaProperties) {
std::mt19937_64 random(0xC0FFEE123456789ULL);
uint32_t unequal_pairs = 0;
uint32_t observed_all_family_collisions = 0;
for (uint32_t index = 0; index < 10'000; ++index) {
SCOPED_TRACE(index);
const bool expected_equal = index % 2 == 0;
auto [left, right] = random_pair(random, index, expected_equal);
const bool equal = canonical_equals(left.ref(), right.ref());
EXPECT_EQ(equal, expected_equal);
const std::string left_arena = arena(left.ref());
const std::string right_arena = arena(right.ref());
EXPECT_EQ(left_arena == right_arena, equal);
const Hashes left_hashes = hashes(left.ref());
const Hashes right_hashes = hashes(right.ref());
if (equal) {
EXPECT_EQ(left_hashes.sip, right_hashes.sip);
EXPECT_EQ(left_hashes.xx, right_hashes.xx);
EXPECT_EQ(left_hashes.crc, right_hashes.crc);
EXPECT_EQ(left_hashes.crc32c, right_hashes.crc32c);
} else {
++unequal_pairs;
if (left_hashes.sip == right_hashes.sip && left_hashes.xx == right_hashes.xx &&
left_hashes.crc == right_hashes.crc && left_hashes.crc32c == right_hashes.crc32c) {
++observed_all_family_collisions;
}
}
}
EXPECT_EQ(unequal_pairs, 5'000);
// This is an observation for the fixed sample, not a claim that finite hashes are injective.
EXPECT_EQ(observed_all_family_collisions, 0);
}
} // namespace
} // namespace doris