| // 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_batch_builder.h" |
| |
| #include <gtest/gtest.h> |
| |
| #include <array> |
| #include <cstdint> |
| #include <initializer_list> |
| #include <limits> |
| #include <memory> |
| #include <ranges> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "common/exception.h" |
| #include "core/value/large_int_value.h" |
| #include "core/value/variant/variant_batch_builder.h" |
| #include "core/value/variant/variant_parquet_encoding.h" |
| #include "runtime/memory/mem_tracker_limiter.h" |
| #include "runtime/thread_context.h" |
| #include "variant_test_utils.h" |
| |
| namespace doris { |
| namespace { |
| |
| StringRef string_ref(std::string_view value) { |
| return {value.data(), value.size()}; |
| } |
| |
| std::string encode_scalar(const VariantScalarRef& scalar) { |
| std::string encoded(scalar.encoded_size(), '\0'); |
| scalar.write_physical(encoded.data(), encoded.size()); |
| return encoded; |
| } |
| |
| template <typename AddValue> |
| std::string encode_builder_scalar(AddValue&& add_value) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| add_value(row); |
| row.finish(); |
| VariantBatchBuilder block = builder.finish_batch(); |
| const VariantRef value = block.value_at(0); |
| return {value.value.data, value.value.size}; |
| } |
| |
| template <typename Function> |
| void expect_builder_exception_code(int code, Function&& function) { |
| try { |
| function(); |
| FAIL() << "Expected doris::Exception"; |
| } catch (const Exception& exception) { |
| EXPECT_EQ(exception.code(), code) << exception.what(); |
| } |
| } |
| |
| struct OwnedBuilderValue { |
| std::string metadata; |
| std::string value; |
| |
| VariantRef ref() const { |
| return {.metadata = {.data = metadata.data(), .size = metadata.size()}, |
| .value = {value.data(), value.size()}}; |
| } |
| }; |
| |
| template <typename Fill> |
| OwnedBuilderValue build_owned_value(Fill&& fill) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| fill(row); |
| row.finish(); |
| VariantBatchBuilder block = builder.finish_batch(); |
| const VariantMetadataRef metadata = block.metadata_ref(); |
| const VariantRef value = block.value_at(0); |
| return {.metadata = std::string(metadata.data, metadata.size), |
| .value = std::string(value.value.data, value.value.size)}; |
| } |
| |
| OwnedBuilderValue make_nested_owned_value() { |
| return build_owned_value([](VariantBatchBuilder::Row& builder) { |
| auto object = builder.start_object(); |
| object.add_key(string_ref("array")); |
| auto array = builder.start_array(); |
| builder.add_null(); |
| auto child = builder.start_object(); |
| child.add_key(string_ref("leaf")); |
| builder.add_string(string_ref("value")); |
| child.finish(); |
| array.finish(); |
| object.finish(); |
| }); |
| } |
| |
| OwnedBuilderValue make_nested_noncanonical_owned_value() { |
| std::string metadata {char {0x01}, char {0x02}, char {0x00}, char {0x01}, |
| char {0x02}, 'b', 'a'}; |
| const std::string object { |
| char {static_cast<uint8_t>(VariantBasicType::OBJECT)}, |
| char {0x02}, |
| char {0x01}, |
| char {0x00}, |
| char {0x01}, |
| char {0x00}, |
| char {0x02}, |
| char {static_cast<uint8_t>(VariantPrimitiveId::FALSE_VALUE) |
| << VARIANT_VALUE_HEADER_SHIFT}, |
| char {static_cast<uint8_t>(VariantPrimitiveId::TRUE_VALUE) |
| << VARIANT_VALUE_HEADER_SHIFT}, |
| }; |
| std::string value {char {static_cast<uint8_t>(VariantBasicType::ARRAY)}, char {0x01}, |
| char {0x00}, static_cast<char>(object.size())}; |
| value.append(object); |
| return {.metadata = std::move(metadata), .value = std::move(value)}; |
| } |
| |
| VariantRef required_field(VariantRef object, std::string_view key) { |
| VariantRef result; |
| EXPECT_TRUE(object.object_find(string_ref(key), &result)); |
| return result; |
| } |
| |
| unsigned __int128 power_of_ten(uint8_t exponent) { |
| unsigned __int128 value = 1; |
| for (uint8_t index = 0; index < exponent; ++index) { |
| value *= 10; |
| } |
| return value; |
| } |
| |
| std::string numbered_key(uint32_t number) { |
| return "key_" + std::to_string(1000 + number); |
| } |
| |
| std::string decimal_bytes(VariantPrimitiveId id, __int128 unscaled, uint8_t width) { |
| std::string encoded; |
| encoded.push_back(static_cast<char>(static_cast<uint8_t>(id) << VARIANT_VALUE_HEADER_SHIFT)); |
| encoded.push_back(0); |
| const auto unsigned_value = static_cast<unsigned __int128>(unscaled); |
| for (uint8_t byte = 0; byte < width; ++byte) { |
| encoded.push_back(static_cast<char>(unsigned_value >> (byte * 8))); |
| } |
| return encoded; |
| } |
| |
| VariantMetadataRef empty_metadata_ref() { |
| static constexpr std::array<char, 3> BYTES { |
| static_cast<char>(VARIANT_ENCODING_VERSION | VARIANT_METADATA_SORTED_STRINGS_MASK), |
| '\0', '\0'}; |
| return {.data = BYTES.data(), .size = BYTES.size()}; |
| } |
| |
| VariantRef value_with_empty_metadata(const std::string& bytes) { |
| return {.metadata = empty_metadata_ref(), .value = {bytes.data(), bytes.size()}}; |
| } |
| |
| TEST(VariantBatchBuilderTest, FinishedBatchOwnsEmptySmallAndMovedStorage) { |
| VariantBatchBuilder empty_builder; |
| VariantBatchBuilder empty = empty_builder.finish_batch(); |
| EXPECT_EQ(empty.num_rows(), 0); |
| EXPECT_EQ(empty.metadata_ref().dict_size(), 0); |
| EXPECT_EQ(empty.value_bytes().size, 0); |
| ASSERT_EQ(empty.value_offsets().size(), 1); |
| EXPECT_EQ(empty.value_offsets()[0], 0); |
| |
| VariantBatchBuilder small_builder; |
| auto row = small_builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("k")); |
| row.add_string(string_ref("v")); |
| object.finish(); |
| row.finish(); |
| VariantBatchBuilder source = small_builder.finish_batch(); |
| |
| VariantBatchBuilder moved(std::move(source)); |
| ASSERT_EQ(moved.num_rows(), 1); |
| EXPECT_EQ(moved.metadata_ref().key_at(0), string_ref("k")); |
| VariantRef field; |
| ASSERT_TRUE(moved.value_at(0).object_find(string_ref("k"), &field)); |
| EXPECT_EQ(field.get_string(), string_ref("v")); |
| |
| VariantBatchBuilder replacement_builder; |
| auto replacement_row = replacement_builder.begin_row(); |
| replacement_row.add_null(); |
| replacement_row.finish(); |
| VariantBatchBuilder assigned = replacement_builder.finish_batch(); |
| assigned = std::move(moved); |
| ASSERT_EQ(assigned.num_rows(), 1); |
| ASSERT_TRUE(assigned.value_at(0).object_find(string_ref("k"), &field)); |
| EXPECT_EQ(field.get_string(), string_ref("v")); |
| } |
| |
| // NOLINTNEXTLINE(readability-function-cognitive-complexity) -- exhaustive scalar parity matrix. |
| TEST(VariantBatchBuilderTest, ScalarRefPhysicalEncodingMatchesBuilder) { |
| const auto expect_parity = [](const VariantScalarRef& scalar, auto&& add_value) { |
| EXPECT_EQ(encode_scalar(scalar), encode_builder_scalar(add_value)); |
| }; |
| |
| expect_parity(VariantScalarRef::null_value(), |
| [](VariantBatchBuilder::Row& builder) { builder.add_null(); }); |
| expect_parity(VariantScalarRef::boolean(false), |
| [](VariantBatchBuilder::Row& builder) { builder.add_bool(false); }); |
| expect_parity(VariantScalarRef::boolean(true), |
| [](VariantBatchBuilder::Row& builder) { builder.add_bool(true); }); |
| for (const int64_t value : {int64_t {-129}, int64_t {-128}, int64_t {127}, int64_t {128}, |
| int64_t {32768}, int64_t {1} << 40}) { |
| expect_parity(VariantScalarRef::integer(value), |
| [value](VariantBatchBuilder::Row& builder) { builder.add_int(value); }); |
| } |
| const std::array<std::pair<int64_t, uint8_t>, 4> fixed_integers { |
| std::pair {int64_t {-128}, uint8_t {1}}, |
| std::pair {int64_t {-32768}, uint8_t {2}}, |
| std::pair {int64_t {std::numeric_limits<int32_t>::min()}, uint8_t {4}}, |
| std::pair {std::numeric_limits<int64_t>::min(), uint8_t {8}}, |
| }; |
| for (const auto& value_and_width : fixed_integers) { |
| const auto value = value_and_width.first; |
| const auto width = value_and_width.second; |
| expect_parity(VariantScalarRef::integer(value, width), |
| [value](VariantBatchBuilder::Row& builder) { builder.add_int(value); }); |
| } |
| const std::string empty_metadata("\x11\0\0", 3); |
| const std::array<std::pair<uint8_t, VariantPrimitiveId>, 4> requested_integers { |
| std::pair {uint8_t {1}, VariantPrimitiveId::INT8}, |
| std::pair {uint8_t {2}, VariantPrimitiveId::INT16}, |
| std::pair {uint8_t {4}, VariantPrimitiveId::INT32}, |
| std::pair {uint8_t {8}, VariantPrimitiveId::INT64}, |
| }; |
| for (const auto& [width, id] : requested_integers) { |
| const std::string encoded = encode_scalar(VariantScalarRef::integer(1, width)); |
| const VariantRef decoded { |
| .metadata = {.data = empty_metadata.data(), .size = empty_metadata.size()}, |
| .value = {encoded.data(), encoded.size()}}; |
| EXPECT_EQ(decoded.primitive_id(), id); |
| EXPECT_EQ(decoded.get_int(), 1); |
| EXPECT_EQ(decoded.value_size(), static_cast<size_t>(width) + 1); |
| } |
| expect_parity(VariantScalarRef::float32(-1.25F), |
| [](VariantBatchBuilder::Row& builder) { builder.add_float(-1.25F); }); |
| expect_parity(VariantScalarRef::float64(123.5), |
| [](VariantBatchBuilder::Row& builder) { builder.add_double(123.5); }); |
| expect_parity(VariantScalarRef::decimal(-123456789, 7), |
| [](VariantBatchBuilder::Row& builder) { builder.add_decimal(-123456789, 7); }); |
| expect_parity(VariantScalarRef::decimal(-123456789012345678LL, 7, 8), |
| [](VariantBatchBuilder::Row& builder) { |
| builder.add_decimal(-123456789012345678LL, 7, 8); |
| }); |
| const std::array<std::pair<__int128, uint8_t>, 3> fixed_decimals { |
| std::pair {static_cast<__int128>(999'999'999), uint8_t {4}}, |
| std::pair {static_cast<__int128>(999'999'999'999'999'999LL), uint8_t {8}}, |
| std::pair {static_cast<__int128>(1'000'000'000'000'000'000LL), uint8_t {16}}, |
| }; |
| for (const auto& unscaled_and_width : fixed_decimals) { |
| const auto unscaled = unscaled_and_width.first; |
| const auto width = unscaled_and_width.second; |
| expect_parity(VariantScalarRef::decimal(unscaled, 3, width), |
| [unscaled, width](VariantBatchBuilder::Row& builder) { |
| builder.add_decimal(unscaled, 3, width); |
| }); |
| } |
| const std::array<std::pair<uint8_t, VariantPrimitiveId>, 3> requested_decimals { |
| std::pair {uint8_t {4}, VariantPrimitiveId::DECIMAL4}, |
| std::pair {uint8_t {8}, VariantPrimitiveId::DECIMAL8}, |
| std::pair {uint8_t {16}, VariantPrimitiveId::DECIMAL16}, |
| }; |
| for (const auto& [width, id] : requested_decimals) { |
| const std::string encoded = encode_scalar(VariantScalarRef::decimal(1, 3, width)); |
| const VariantRef decoded { |
| .metadata = {.data = empty_metadata.data(), .size = empty_metadata.size()}, |
| .value = {encoded.data(), encoded.size()}}; |
| EXPECT_EQ(decoded.primitive_id(), id); |
| EXPECT_EQ(decoded.get_decimal(), (VariantDecimal {1, 3, width})); |
| } |
| expect_parity(VariantScalarRef::date(-20000), |
| [](VariantBatchBuilder::Row& builder) { builder.add_date(-20000); }); |
| expect_parity(VariantScalarRef::timestamp_micros(-1234567890, true), |
| [](VariantBatchBuilder::Row& builder) { |
| builder.add_timestamp_micros(-1234567890, true); |
| }); |
| expect_parity(VariantScalarRef::timestamp_micros(2234567890, false), |
| [](VariantBatchBuilder::Row& builder) { |
| builder.add_timestamp_micros(2234567890, false); |
| }); |
| expect_parity(VariantScalarRef::timestamp_nanos(-3234567890, true), |
| [](VariantBatchBuilder::Row& builder) { |
| builder.add_timestamp_nanos(-3234567890, true); |
| }); |
| expect_parity(VariantScalarRef::timestamp_nanos(4234567890, false), |
| [](VariantBatchBuilder::Row& builder) { |
| builder.add_timestamp_nanos(4234567890, false); |
| }); |
| expect_parity( |
| VariantScalarRef::time_ntz_micros(5234567890), |
| [](VariantBatchBuilder::Row& builder) { builder.add_time_ntz_micros(5234567890); }); |
| |
| const std::string binary("\0\xFF\x01", 3); |
| expect_parity(VariantScalarRef::binary(StringRef(binary)), |
| [&binary](VariantBatchBuilder::Row& builder) { |
| builder.add_binary(StringRef(binary)); |
| }); |
| const std::string short_text(63, 's'); |
| const std::string long_text(64, 'L'); |
| expect_parity(VariantScalarRef::string(StringRef(short_text)), |
| [&short_text](VariantBatchBuilder::Row& builder) { |
| builder.add_string(StringRef(short_text)); |
| }); |
| expect_parity(VariantScalarRef::string(StringRef(long_text)), |
| [&long_text](VariantBatchBuilder::Row& builder) { |
| builder.add_string(StringRef(long_text)); |
| }); |
| |
| std::array<uint8_t, 16> uuid {}; |
| for (uint8_t index = 0; index < uuid.size(); ++index) { |
| uuid[index] = index; |
| } |
| expect_parity(VariantScalarRef::uuid(uuid), |
| [&uuid](VariantBatchBuilder::Row& builder) { builder.add_uuid(uuid); }); |
| const auto decimal38 = static_cast<__int128>(power_of_ten(38) - 1); |
| expect_parity( |
| VariantScalarRef::decimal(decimal38, 0, 16), |
| [decimal38](VariantBatchBuilder::Row& builder) { builder.add_largeint(decimal38); }); |
| const auto outside_decimal38 = static_cast<__int128>(power_of_ten(38)); |
| const auto expect_largeint_fallback = [&](const __int128 value) { |
| const std::string text = LargeIntValue::to_string(value); |
| expect_parity(VariantScalarRef::string(StringRef(text)), |
| [value](VariantBatchBuilder::Row& builder) { builder.add_largeint(value); }); |
| }; |
| expect_largeint_fallback(outside_decimal38); |
| for (const __int128 value : {-outside_decimal38, std::numeric_limits<__int128>::min()}) { |
| expect_largeint_fallback(value); |
| } |
| |
| std::string borrowed_short = "borrowed"; |
| const VariantScalarRef borrowed_short_scalar = |
| VariantScalarRef::string(StringRef(borrowed_short)); |
| borrowed_short.front() = 'B'; |
| EXPECT_EQ(encode_scalar(borrowed_short_scalar), |
| encode_builder_scalar([&borrowed_short](VariantBatchBuilder::Row& builder) { |
| builder.add_string(StringRef(borrowed_short)); |
| })); |
| std::string borrowed_long(64, 'x'); |
| const VariantScalarRef borrowed_long_scalar = |
| VariantScalarRef::string(StringRef(borrowed_long)); |
| borrowed_long.back() = 'y'; |
| EXPECT_EQ(encode_scalar(borrowed_long_scalar), |
| encode_builder_scalar([&borrowed_long](VariantBatchBuilder::Row& builder) { |
| builder.add_string(StringRef(borrowed_long)); |
| })); |
| |
| const VariantScalarRef integer = VariantScalarRef::integer(1); |
| std::array<char, 4> unchanged {'a', 'b', 'c', 'd'}; |
| EXPECT_THROW(integer.write_physical(unchanged.data(), integer.encoded_size() - 1), Exception); |
| EXPECT_EQ(unchanged, (std::array<char, 4> {'a', 'b', 'c', 'd'})); |
| EXPECT_THROW(integer.write_physical(nullptr, integer.encoded_size()), Exception); |
| EXPECT_THROW(VariantScalarRef::integer(128, 1), Exception); |
| EXPECT_THROW(VariantScalarRef::integer(1, 3), Exception); |
| EXPECT_THROW(VariantScalarRef::decimal(1, 39), Exception); |
| EXPECT_THROW(VariantScalarRef::decimal(1, 0, 3), Exception); |
| EXPECT_THROW(VariantScalarRef::decimal(1'000'000'000, 0, 4), Exception); |
| EXPECT_THROW( |
| VariantScalarRef::decimal(static_cast<__int128>(1'000'000'000'000'000'000LL), 0, 8), |
| Exception); |
| EXPECT_THROW(VariantScalarRef::decimal(outside_decimal38, 0, 16), Exception); |
| const std::string invalid_utf8("\xC3\x28", 2); |
| EXPECT_THROW(VariantScalarRef::string(StringRef(invalid_utf8)), 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); |
| } |
| |
| // NOLINTNEXTLINE(readability-function-cognitive-complexity) -- GTest macros expand assertions. |
| TEST(VariantBatchBuilderTest, ScalarAndNestedValuesRoundTrip) { |
| const std::string binary("\0\xFF\x01", 3); |
| const std::string long_text(64, 'L'); |
| std::array<uint8_t, 16> uuid {}; |
| for (uint8_t index = 0; index < uuid.size(); ++index) { |
| uuid[index] = index; |
| } |
| const OwnedBuilderValue owned = build_owned_value([&](VariantBatchBuilder::Row& builder) { |
| auto root_scope = builder.start_object(); |
| |
| root_scope.add_key(string_ref("z_null")); |
| builder.add_null(); |
| root_scope.add_key(string_ref("bool")); |
| builder.add_bool(true); |
| root_scope.add_key(string_ref("int")); |
| builder.add_int(-12345); |
| root_scope.add_key(string_ref("float")); |
| builder.add_float(-1.25F); |
| root_scope.add_key(string_ref("double")); |
| builder.add_double(123.5); |
| root_scope.add_key(string_ref("decimal")); |
| builder.add_decimal(-123456789012345678LL, 7); |
| root_scope.add_key(string_ref("date")); |
| builder.add_date(-20000); |
| root_scope.add_key(string_ref("timestamp")); |
| builder.add_timestamp_micros(-1234567890, true); |
| root_scope.add_key(string_ref("timestamp_ntz")); |
| builder.add_timestamp_micros(2234567890, false); |
| root_scope.add_key(string_ref("timestamp_nanos")); |
| builder.add_timestamp_nanos(-3234567890, true); |
| root_scope.add_key(string_ref("timestamp_ntz_nanos")); |
| builder.add_timestamp_nanos(4234567890, false); |
| root_scope.add_key(string_ref("time")); |
| builder.add_time_ntz_micros(5234567890); |
| root_scope.add_key(string_ref("binary")); |
| builder.add_binary(StringRef(binary)); |
| root_scope.add_key(string_ref("short")); |
| builder.add_string(string_ref("short text")); |
| root_scope.add_key(string_ref("long")); |
| builder.add_string(StringRef(long_text)); |
| root_scope.add_key(string_ref("uuid")); |
| builder.add_uuid(uuid); |
| root_scope.add_key(string_ref("nested")); |
| auto array_scope = builder.start_array(); |
| builder.add_int(7); |
| builder.add_string(string_ref("array value")); |
| auto nested_object_scope = builder.start_object(); |
| nested_object_scope.add_key(string_ref("inside")); |
| builder.add_bool(false); |
| nested_object_scope.finish(); |
| array_scope.finish(); |
| root_scope.finish(); |
| }); |
| const VariantRef root = owned.ref(); |
| validate_canonical(root); |
| |
| EXPECT_TRUE(required_field(root, "z_null").is_null()); |
| EXPECT_TRUE(required_field(root, "bool").get_bool()); |
| EXPECT_EQ(required_field(root, "int").get_int(), -12345); |
| EXPECT_EQ(required_field(root, "float").get_float(), -1.25F); |
| EXPECT_EQ(required_field(root, "double").get_double(), 123.5); |
| EXPECT_EQ(required_field(root, "decimal").get_decimal(), |
| (VariantDecimal {-123456789012345678LL, 7, 8})); |
| EXPECT_EQ(required_field(root, "date").get_date(), -20000); |
| EXPECT_EQ(required_field(root, "timestamp").get_timestamp_micros(), -1234567890); |
| EXPECT_EQ(required_field(root, "timestamp_ntz").get_timestamp_ntz_micros(), 2234567890); |
| EXPECT_EQ(required_field(root, "timestamp_nanos").get_timestamp_nanos(), -3234567890); |
| EXPECT_EQ(required_field(root, "timestamp_ntz_nanos").get_timestamp_ntz_nanos(), 4234567890); |
| EXPECT_EQ(required_field(root, "time").get_time_ntz_micros(), 5234567890); |
| EXPECT_EQ(required_field(root, "binary").get_binary(), StringRef(binary)); |
| EXPECT_EQ(required_field(root, "short").get_string(), string_ref("short text")); |
| EXPECT_EQ(required_field(root, "long").get_string(), StringRef(long_text)); |
| EXPECT_EQ(required_field(root, "uuid").get_uuid(), uuid); |
| |
| const VariantRef nested = required_field(root, "nested"); |
| ASSERT_EQ(nested.num_elements(), 3); |
| EXPECT_EQ(nested.array_at(0).get_int(), 7); |
| EXPECT_EQ(nested.array_at(1).get_string(), string_ref("array value")); |
| EXPECT_FALSE(required_field(nested.array_at(2), "inside").get_bool()); |
| } |
| |
| TEST(VariantBatchBuilderTest, DictionaryRemapUsesUnsignedUtf8Ordering) { |
| const OwnedBuilderValue owned = build_owned_value([](VariantBatchBuilder::Row& builder) { |
| auto object_scope = builder.start_object(); |
| const std::array<std::string_view, 4> keys {"\xC3\xBF", "z", "\xC2\x80", "a"}; |
| for (int64_t index = 0; index < keys.size(); ++index) { |
| object_scope.add_key(string_ref(keys[index])); |
| builder.add_int(index); |
| } |
| object_scope.finish(); |
| }); |
| const VariantRef object = owned.ref(); |
| validate_canonical(object); |
| |
| const std::array<std::string_view, 4> sorted_keys {"a", "z", "\xC2\x80", "\xC3\xBF"}; |
| const std::array<int64_t, 4> expected_values {3, 1, 2, 0}; |
| for (uint32_t final_id = 0; final_id < sorted_keys.size(); ++final_id) { |
| EXPECT_EQ(object.metadata.key_at(final_id), string_ref(sorted_keys[final_id])); |
| uint32_t decoded_id = std::numeric_limits<uint32_t>::max(); |
| EXPECT_EQ(object.object_value_at(final_id, &decoded_id).get_int(), |
| expected_values[final_id]); |
| EXPECT_EQ(decoded_id, final_id); |
| } |
| } |
| |
| void expect_reverse_object_is_canonical(uint32_t count) { |
| const OwnedBuilderValue owned = build_owned_value([count](VariantBatchBuilder::Row& builder) { |
| auto object_scope = builder.start_object(); |
| for (uint32_t index = count; index != 0; --index) { |
| const std::string key = numbered_key(index - 1); |
| object_scope.add_key(StringRef(key)); |
| builder.add_int(index - 1); |
| } |
| object_scope.finish(); |
| }); |
| const VariantRef object = owned.ref(); |
| validate_canonical(object); |
| for (uint32_t index = 0; index < count; ++index) { |
| EXPECT_EQ(required_field(object, numbered_key(index)).get_int(), index); |
| } |
| } |
| |
| TEST(VariantBatchBuilderTest, ObjectPlanningSortsAcrossSmallObjectThreshold) { |
| // Pass 2 uses insertion sort through 16 fields and std::sort above that threshold. |
| expect_reverse_object_is_canonical(16); |
| expect_reverse_object_is_canonical(17); |
| } |
| |
| TEST(VariantBatchBuilderTest, IntegerAndDecimalWidthsAreMinimal) { |
| const std::array<int64_t, 12> integers { |
| std::numeric_limits<int8_t>::min() - 1LL, |
| std::numeric_limits<int8_t>::min(), |
| std::numeric_limits<int8_t>::max(), |
| std::numeric_limits<int8_t>::max() + 1LL, |
| std::numeric_limits<int16_t>::min() - 1LL, |
| std::numeric_limits<int16_t>::min(), |
| std::numeric_limits<int16_t>::max(), |
| std::numeric_limits<int16_t>::max() + 1LL, |
| static_cast<int64_t>(std::numeric_limits<int32_t>::min()) - 1, |
| std::numeric_limits<int32_t>::min(), |
| std::numeric_limits<int32_t>::max(), |
| static_cast<int64_t>(std::numeric_limits<int32_t>::max()) + 1, |
| }; |
| const std::array<__int128, 9> decimals { |
| 0, |
| 999'999'999, |
| 1'000'000'000, |
| -999'999'999, |
| -1'000'000'000, |
| static_cast<__int128>(999'999'999'999'999'999), |
| static_cast<__int128>(1'000'000'000'000'000'000), |
| -static_cast<__int128>(999'999'999'999'999'999), |
| -static_cast<__int128>(1'000'000'000'000'000'000), |
| }; |
| const OwnedBuilderValue owned = build_owned_value([&](VariantBatchBuilder::Row& row) { |
| auto array = row.start_array(); |
| for (int64_t value : integers) { |
| row.add_int(value); |
| } |
| for (__int128 value : decimals) { |
| row.add_decimal(value, 1); |
| } |
| row.add_decimal(static_cast<__int128>(power_of_ten(38) - 1), 38); |
| array.finish(); |
| }); |
| |
| const VariantRef value = owned.ref(); |
| validate_canonical(value); |
| for (uint32_t index = 0; index < integers.size(); ++index) { |
| EXPECT_EQ(value.array_at(index).get_int(), integers[index]); |
| } |
| const std::array<uint8_t, 9> expected_widths {4, 4, 8, 4, 8, 8, 16, 8, 16}; |
| for (uint32_t index = 0; index < decimals.size(); ++index) { |
| const VariantDecimal decimal = value.array_at(integers.size() + index).get_decimal(); |
| EXPECT_EQ(decimal.unscaled, decimals[index]); |
| EXPECT_EQ(decimal.width, expected_widths[index]); |
| } |
| EXPECT_EQ(value.array_at(integers.size() + decimals.size()).get_decimal().width, 16); |
| } |
| |
| TEST(VariantBatchBuilderTest, AddValuePreservesExplicitPrimitiveWidths) { |
| const OwnedBuilderValue source = build_owned_value([](VariantBatchBuilder::Row& row) { |
| auto array = row.start_array(); |
| row.add_scalar(VariantScalarRef::integer(7, 8)); |
| row.add_scalar(VariantScalarRef::decimal(8, 2, 16)); |
| array.finish(); |
| }); |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| row.add_value(source.ref()); |
| row.finish(); |
| VariantBatchBuilder imported = builder.finish_batch(); |
| |
| EXPECT_EQ(imported.value_at(0).array_at(0).primitive_id(), VariantPrimitiveId::INT64); |
| EXPECT_EQ(imported.value_at(0).array_at(1).primitive_id(), VariantPrimitiveId::DECIMAL16); |
| } |
| |
| TEST(VariantBatchBuilderTest, DecimalValidationLargeIntFallbackAndExplicitWidths) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| EXPECT_THROW(row.add_decimal(1, 39), Exception); |
| EXPECT_THROW(row.add_decimal(static_cast<__int128>(power_of_ten(38)), 0), Exception); |
| |
| auto array = row.start_array(); |
| row.add_decimal(1, 0); |
| row.add_decimal(1, 38, 4); |
| row.add_decimal(1, 38, 8); |
| row.add_decimal(1, 38, 16); |
| row.add_largeint(42); |
| row.add_largeint(std::numeric_limits<__int128>::max()); |
| row.add_largeint(std::numeric_limits<__int128>::min()); |
| array.finish(); |
| row.finish(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| const VariantRef value = block.value_at(0); |
| validate_canonical(value); |
| EXPECT_EQ(value.array_at(0).get_decimal(), (VariantDecimal {1, 0, 4})); |
| EXPECT_EQ(value.array_at(1).get_decimal(), (VariantDecimal {1, 38, 4})); |
| EXPECT_EQ(value.array_at(2).get_decimal(), (VariantDecimal {1, 38, 8})); |
| EXPECT_EQ(value.array_at(3).get_decimal(), (VariantDecimal {1, 38, 16})); |
| EXPECT_EQ(value.array_at(4).get_decimal(), (VariantDecimal {42, 0, 16})); |
| EXPECT_EQ(value.array_at(5).get_string(), |
| string_ref("170141183460469231731687303715884105727")); |
| EXPECT_EQ(value.array_at(6).get_string(), |
| string_ref("-170141183460469231731687303715884105728")); |
| |
| for (uint8_t invalid_width : {uint8_t {1}, uint8_t {5}, uint8_t {17}}) { |
| VariantBatchBuilder invalid_builder; |
| auto invalid_row = invalid_builder.begin_row(); |
| EXPECT_THROW(invalid_row.add_decimal(1, 0, invalid_width), Exception); |
| } |
| { |
| VariantBatchBuilder invalid_builder; |
| auto invalid_row = invalid_builder.begin_row(); |
| EXPECT_THROW(invalid_row.add_decimal(1'000'000'000, 0, 4), Exception); |
| } |
| { |
| VariantBatchBuilder invalid_builder; |
| auto invalid_row = invalid_builder.begin_row(); |
| EXPECT_THROW( |
| invalid_row.add_decimal(static_cast<__int128>(1'000'000'000'000'000'000), 0, 8), |
| Exception); |
| } |
| } |
| |
| TEST(VariantBatchBuilderTest, StringBoundariesAndUtf8ValidationPreserveRowState) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| const std::string invalid_key("\xC0\xAF", 2); |
| EXPECT_THROW(object.add_key(StringRef(invalid_key)), Exception); |
| |
| object.add_key(string_ref("short")); |
| const std::string invalid_short("\xE2\x28\xA1", 3); |
| EXPECT_THROW(row.add_string(StringRef(invalid_short)), Exception); |
| const std::string short_text(63, 's'); |
| row.add_string(StringRef(short_text)); |
| |
| object.add_key(string_ref("long")); |
| std::string invalid_long(64, 'x'); |
| invalid_long.back() = static_cast<char>(0xFF); |
| EXPECT_THROW(row.add_string(StringRef(invalid_long)), Exception); |
| const std::string long_text(64, 'l'); |
| row.add_string(StringRef(long_text)); |
| |
| object.add_key(string_ref("\xE9\x94\xAE")); |
| const std::string non_utf8_binary("\xFF\xC0\xAF", 3); |
| row.add_binary(StringRef(non_utf8_binary)); |
| object.finish(); |
| row.finish(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| const VariantRef value = block.value_at(0); |
| validate_canonical(value); |
| EXPECT_EQ(required_field(value, "short").basic_type(), VariantBasicType::SHORT_STRING); |
| EXPECT_EQ(required_field(value, "long").primitive_id(), VariantPrimitiveId::STRING); |
| EXPECT_EQ(required_field(value, "short").get_string(), StringRef(short_text)); |
| EXPECT_EQ(required_field(value, "long").get_string(), StringRef(long_text)); |
| EXPECT_EQ(required_field(value, "\xE9\x94\xAE").get_binary(), StringRef(non_utf8_binary)); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 3); |
| } |
| |
| TEST(VariantMetadataBuilderTest, RegisterKeyPreservesValidationOnDictionaryMiss) { |
| VariantMetadataBuilder metadata; |
| EXPECT_EQ(metadata.register_key(string_ref("valid")), 0); |
| EXPECT_EQ(metadata.register_key(string_ref("valid")), 0); |
| |
| const std::string invalid_utf8("\xC0\xAF", 2); |
| EXPECT_THROW(metadata.register_key(StringRef(invalid_utf8)), Exception); |
| EXPECT_THROW(metadata.register_key(StringRef(static_cast<const char*>(nullptr), 1)), Exception); |
| EXPECT_EQ(metadata.num_keys(), 1); |
| |
| metadata.seal(); |
| ASSERT_EQ(metadata.metadata_ref().dict_size(), 1); |
| EXPECT_EQ(metadata.metadata_ref().key_at(0), string_ref("valid")); |
| } |
| |
| std::string build_array_with_nulls(uint32_t count, uint8_t* value_header_out) { |
| const OwnedBuilderValue owned = build_owned_value([count](VariantBatchBuilder::Row& row) { |
| auto array = row.start_array(); |
| for (uint32_t index = 0; index < count; ++index) { |
| row.add_null(); |
| } |
| array.finish(); |
| }); |
| const VariantRef value = owned.ref(); |
| validate_canonical(value); |
| *value_header_out = static_cast<uint8_t>(owned.value[0]) >> VARIANT_VALUE_HEADER_SHIFT; |
| EXPECT_EQ(value.num_elements(), count); |
| return owned.value; |
| } |
| |
| TEST(VariantBatchBuilderTest, CountAndOffsetWidthsCrossAt255And256) { |
| uint8_t small_header = 0; |
| uint8_t large_header = 0; |
| const std::string small = build_array_with_nulls(255, &small_header); |
| const std::string large = build_array_with_nulls(256, &large_header); |
| EXPECT_EQ(small_header & VARIANT_ARRAY_LARGE_MASK, 0); |
| EXPECT_NE(large_header & VARIANT_ARRAY_LARGE_MASK, 0); |
| EXPECT_EQ(((small_header >> VARIANT_ARRAY_OFFSET_SIZE_SHIFT) & 0x03U) + 1, 1); |
| EXPECT_EQ(((large_header >> VARIANT_ARRAY_OFFSET_SIZE_SHIFT) & 0x03U) + 1, 2); |
| EXPECT_LT(small.size(), large.size()); |
| } |
| |
| struct ObjectBoundaryResult { |
| uint8_t id_width; |
| uint8_t offset_width; |
| bool is_large; |
| }; |
| |
| ObjectBoundaryResult build_object_boundary(uint32_t count) { |
| const OwnedBuilderValue owned = build_owned_value([count](VariantBatchBuilder::Row& row) { |
| auto object = row.start_object(); |
| for (uint32_t index = 0; index < count; ++index) { |
| const std::string key = numbered_key(index); |
| object.add_key(StringRef(key)); |
| row.add_null(); |
| } |
| object.finish(); |
| }); |
| const VariantRef value = owned.ref(); |
| validate_canonical(value); |
| EXPECT_EQ(value.num_elements(), count); |
| |
| const uint8_t header = static_cast<uint8_t>(owned.value[0]) >> VARIANT_VALUE_HEADER_SHIFT; |
| return {.id_width = |
| static_cast<uint8_t>(((header >> VARIANT_OBJECT_ID_SIZE_SHIFT) & 0x03U) + 1), |
| .offset_width = static_cast<uint8_t>( |
| ((header >> VARIANT_OBJECT_OFFSET_SIZE_SHIFT) & 0x03U) + 1), |
| .is_large = (header & VARIANT_OBJECT_LARGE_MASK) != 0}; |
| } |
| |
| TEST(VariantBatchBuilderTest, ObjectIdWidthCrossesAfterFinalId255) { |
| const ObjectBoundaryResult count255 = build_object_boundary(255); |
| const ObjectBoundaryResult count256 = build_object_boundary(256); |
| const ObjectBoundaryResult count257 = build_object_boundary(257); |
| EXPECT_EQ(count255.id_width, 1); |
| EXPECT_EQ(count255.offset_width, 1); |
| EXPECT_FALSE(count255.is_large); |
| EXPECT_EQ(count256.id_width, 1); |
| EXPECT_EQ(count256.offset_width, 2); |
| EXPECT_TRUE(count256.is_large); |
| EXPECT_EQ(count257.id_width, 2); |
| EXPECT_EQ(count257.offset_width, 2); |
| EXPECT_TRUE(count257.is_large); |
| } |
| |
| uint8_t build_metadata_with_key_size(size_t key_size) { |
| const OwnedBuilderValue owned = build_owned_value([key_size](VariantBatchBuilder::Row& row) { |
| auto object = row.start_object(); |
| const std::string key(key_size, 'k'); |
| object.add_key(StringRef(key)); |
| row.add_null(); |
| object.finish(); |
| }); |
| const VariantRef value = owned.ref(); |
| validate_canonical(value); |
| return value.metadata.offset_size(); |
| } |
| |
| TEST(VariantBatchBuilderTest, MetadataOffsetWidthCrossesAt255And256Bytes) { |
| EXPECT_EQ(build_metadata_with_key_size(255), 1); |
| EXPECT_EQ(build_metadata_with_key_size(256), 2); |
| } |
| |
| TEST(VariantBatchBuilderTest, CanonicalValidatorRejectsIndependentNonCanonicalBytes) { |
| const std::string empty_metadata_bytes("\x11\0\0", 3); |
| const VariantMetadataRef empty_metadata {empty_metadata_bytes.data(), |
| empty_metadata_bytes.size()}; |
| |
| std::string wide_integer; |
| wide_integer.push_back(static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::INT32) |
| << VARIANT_VALUE_HEADER_SHIFT)); |
| wide_integer.push_back(5); |
| wide_integer.append(3, '\0'); |
| EXPECT_THROW(validate_canonical({empty_metadata, {wide_integer.data(), wide_integer.size()}}), |
| Exception); |
| |
| std::string long_form_for_short_string; |
| long_form_for_short_string.push_back(static_cast<char>( |
| static_cast<uint8_t>(VariantPrimitiveId::STRING) << VARIANT_VALUE_HEADER_SHIFT)); |
| long_form_for_short_string.push_back(1); |
| long_form_for_short_string.append(3, '\0'); |
| long_form_for_short_string.push_back('x'); |
| EXPECT_THROW(validate_canonical( |
| {empty_metadata, |
| {long_form_for_short_string.data(), long_form_for_short_string.size()}}), |
| Exception); |
| |
| const OwnedBuilderValue object = build_owned_value([](VariantBatchBuilder::Row& row) { |
| auto scope = row.start_object(); |
| scope.add_key(string_ref("a")); |
| row.add_null(); |
| scope.add_key(string_ref("b")); |
| row.add_null(); |
| scope.finish(); |
| }); |
| std::string reversed_ids = object.value; |
| std::swap(reversed_ids[2], reversed_ids[3]); |
| EXPECT_THROW( |
| validate_canonical({object.ref().metadata, {reversed_ids.data(), reversed_ids.size()}}), |
| Exception); |
| } |
| |
| TEST(VariantBatchBuilderTest, CanonicalValidatorChecksDecimalPrecisionIndependently) { |
| const std::string undersized_decimal4 = |
| decimal_bytes(VariantPrimitiveId::DECIMAL4, 1'000'000'000, 4); |
| EXPECT_THROW(validate_canonical(value_with_empty_metadata(undersized_decimal4)), Exception); |
| const std::string undersized_decimal8 = decimal_bytes( |
| VariantPrimitiveId::DECIMAL8, static_cast<__int128>(1'000'000'000'000'000'000), 8); |
| EXPECT_THROW(validate_canonical(value_with_empty_metadata(undersized_decimal8)), Exception); |
| const std::string valid_wide_decimal = decimal_bytes(VariantPrimitiveId::DECIMAL16, 42, 16); |
| validate_canonical(value_with_empty_metadata(valid_wide_decimal)); |
| } |
| |
| TEST(VariantBatchBuilderTest, CanonicalValidatorChecksValueUtf8Independently) { |
| std::string invalid_short; |
| invalid_short.push_back( |
| static_cast<char>((1 << VARIANT_VALUE_HEADER_SHIFT) | |
| static_cast<uint8_t>(VariantBasicType::SHORT_STRING))); |
| invalid_short.push_back(static_cast<char>(0xFF)); |
| EXPECT_THROW(validate_canonical(value_with_empty_metadata(invalid_short)), Exception); |
| |
| std::string invalid_long; |
| invalid_long.push_back(static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::STRING) |
| << VARIANT_VALUE_HEADER_SHIFT)); |
| invalid_long.push_back(64); |
| invalid_long.append(3, '\0'); |
| invalid_long.append(63, 'x'); |
| invalid_long.push_back(static_cast<char>(0xFF)); |
| EXPECT_THROW(validate_canonical(value_with_empty_metadata(invalid_long)), Exception); |
| |
| std::string invalid_binary; |
| invalid_binary.push_back(static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::BINARY) |
| << VARIANT_VALUE_HEADER_SHIFT)); |
| invalid_binary.push_back(1); |
| invalid_binary.append(3, '\0'); |
| invalid_binary.push_back(static_cast<char>(0xFF)); |
| validate_canonical(value_with_empty_metadata(invalid_binary)); |
| } |
| |
| TEST(VariantBatchBuilderTest, CanonicalValidatorChecksMetadataKeyUtf8Independently) { |
| std::string invalid_metadata { |
| static_cast<char>(VARIANT_ENCODING_VERSION | VARIANT_METADATA_SORTED_STRINGS_MASK), |
| 1, |
| 0, |
| 1, |
| static_cast<char>(0xFF), |
| }; |
| const std::string object_with_invalid_key { |
| static_cast<char>(VariantBasicType::OBJECT), 1, 0, 0, 1, 0}; |
| const VariantRef row { |
| .metadata = {invalid_metadata.data(), invalid_metadata.size()}, |
| .value = {object_with_invalid_key.data(), object_with_invalid_key.size()}}; |
| EXPECT_THROW(validate_canonical(row), Exception); |
| } |
| |
| TEST(VariantBatchBuilderTest, SharedMetadataCoversMultipleRowsAndNestedContainers) { |
| VariantBatchBuilder builder({.rows = 3, |
| .metadata_keys = 3, |
| .scalar_bytes = 32, |
| .nodes = 32, |
| .containers = 16, |
| .children = 32}); |
| { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("z")); |
| row.add_int(7); |
| object.add_key(string_ref("nested")); |
| std::vector<VariantBatchBuilder::Row::ArrayScope> arrays; |
| for (uint32_t depth = 0; depth < 8; ++depth) { |
| arrays.emplace_back(row.start_array()); |
| } |
| row.add_null(); |
| for (auto& array : std::ranges::reverse_view(arrays)) { |
| array.finish(); |
| } |
| object.finish(); |
| row.finish(); |
| } |
| { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| row.add_bool(true); |
| auto object = row.start_object(); |
| object.add_key(string_ref("a")); |
| row.add_string(string_ref("value")); |
| object.finish(); |
| array.finish(); |
| row.finish(); |
| } |
| { |
| auto row = builder.begin_row(); |
| row.add_string(string_ref("scalar")); |
| row.finish(); |
| } |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 3); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 3); |
| EXPECT_EQ(block.metadata_ref().key_at(0), string_ref("a")); |
| EXPECT_EQ(block.metadata_ref().key_at(1), string_ref("nested")); |
| EXPECT_EQ(block.metadata_ref().key_at(2), string_ref("z")); |
| EXPECT_EQ(block.value_at(0).metadata.data, block.value_at(1).metadata.data); |
| EXPECT_EQ(block.value_at(1).metadata.data, block.value_at(2).metadata.data); |
| std::vector<VariantRef> rows; |
| rows.reserve(block.num_rows()); |
| for (size_t index = 0; index < block.num_rows(); ++index) { |
| rows.push_back(block.value_at(index)); |
| } |
| validate_canonical(block.metadata_ref(), rows); |
| |
| VariantRef nested = required_field(block.value_at(0), "nested"); |
| for (uint32_t depth = 0; depth < 8; ++depth) { |
| ASSERT_EQ(nested.num_elements(), 1); |
| nested = nested.array_at(0); |
| } |
| EXPECT_TRUE(nested.is_null()); |
| } |
| |
| TEST(VariantBatchBuilderTest, EnforcesSingleActiveRowAndTerminalBatchState) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, |
| [&] { static_cast<void>(builder.begin_row()); }); |
| row.add_null(); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, |
| [&] { static_cast<void>(builder.finish_batch()); }); |
| row.finish(); |
| EXPECT_TRUE(row.is_finished()); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 1); |
| EXPECT_TRUE(block.value_at(0).is_null()); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, |
| [&] { static_cast<void>(builder.begin_row()); }); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, |
| [&] { static_cast<void>(builder.finish_batch()); }); |
| } |
| |
| TEST(VariantBatchBuilderTest, MovedFromRowCannotMutateTheActiveRow) { |
| VariantBatchBuilder builder; |
| auto moved_from = builder.begin_row(); |
| auto active = std::move(moved_from); |
| // NOLINTNEXTLINE(bugprone-use-after-move) -- Negative API contract. |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { |
| // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move) -- Negative API contract. |
| moved_from.add_null(); |
| }); |
| active.add_int(1); |
| active.finish(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 1); |
| EXPECT_EQ(block.value_at(0).get_int(), 1); |
| } |
| |
| TEST(VariantBatchBuilderTest, MovedFromScopeCannotMutateTheActiveScope) { |
| VariantBatchBuilder builder; |
| auto object_row = builder.begin_row(); |
| auto moved_from = object_row.start_object(); |
| auto active_object = std::move(moved_from); |
| // NOLINTNEXTLINE(bugprone-use-after-move) -- Negative API contract. |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { |
| // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move) -- Negative API contract. |
| moved_from.add_key(string_ref("stale")); |
| }); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { moved_from.finish(); }); |
| active_object.add_key(string_ref("active")); |
| object_row.add_bool(true); |
| active_object.finish(); |
| object_row.finish(); |
| |
| auto array_row = builder.begin_row(); |
| auto moved_array = array_row.start_array(); |
| auto active_array = std::move(moved_array); |
| // NOLINTNEXTLINE(bugprone-use-after-move) -- Negative API contract. |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { |
| // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move) -- Negative API contract. |
| moved_array.finish(); |
| }); |
| array_row.add_null(); |
| active_array.finish(); |
| array_row.finish(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| const std::vector<VariantRef> rows {block.value_at(0), block.value_at(1)}; |
| validate_canonical(block.metadata_ref(), rows); |
| EXPECT_TRUE(required_field(block.value_at(0), "active").get_bool()); |
| ASSERT_EQ(block.value_at(1).num_elements(), 1); |
| EXPECT_TRUE(block.value_at(1).array_at(0).is_null()); |
| } |
| |
| TEST(VariantBatchBuilderTest, RowMoveKeepsActiveScopesUsableAndScopesRejectStaleGeneration) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("nested")); |
| auto array = row.start_array(); |
| |
| auto moved_row = std::move(row); |
| // NOLINTNEXTLINE(bugprone-use-after-move) -- Negative API contract. |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { |
| // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move) -- Negative API contract. |
| row.add_null(); |
| }); |
| moved_row.add_int(17); |
| array.finish(); |
| object.finish(); |
| moved_row.finish(); |
| |
| auto aborted = builder.begin_row(); |
| auto stale_scope = aborted.start_object(); |
| aborted.abort(); |
| auto current = builder.begin_row(); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, |
| [&] { stale_scope.add_key(string_ref("stale")); }); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { stale_scope.finish(); }); |
| current.add_null(); |
| current.finish(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| const std::vector<VariantRef> rows {block.value_at(0), block.value_at(1)}; |
| validate_canonical(block.metadata_ref(), rows); |
| const VariantRef nested = required_field(block.value_at(0), "nested"); |
| ASSERT_EQ(nested.num_elements(), 1); |
| EXPECT_EQ(nested.array_at(0).get_int(), 17); |
| EXPECT_TRUE(block.value_at(1).is_null()); |
| } |
| |
| TEST(VariantBatchBuilderTest, OldFinishedAndAbortedRowsCannotCrossGenerations) { |
| VariantBatchBuilder builder; |
| auto finished = builder.begin_row(); |
| finished.add_null(); |
| finished.finish(); |
| |
| auto aborted = builder.begin_row(); |
| aborted.abort(); |
| |
| auto current = builder.begin_row(); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { finished.add_bool(true); }); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { aborted.add_int(7); }); |
| current.add_string(string_ref("current")); |
| current.finish(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 2); |
| const std::vector<VariantRef> rows {block.value_at(0), block.value_at(1)}; |
| validate_canonical(block.metadata_ref(), rows); |
| EXPECT_TRUE(block.value_at(0).is_null()); |
| EXPECT_EQ(block.value_at(1).get_string(), string_ref("current")); |
| } |
| |
| TEST(VariantBatchBuilderTest, EmptyBatchHasMinimalMetadataAndOnlyZeroOffset) { |
| VariantBatchBuilder builder; |
| VariantBatchBuilder block = builder.finish_batch(); |
| const std::string expected_metadata("\x11\0\0", 3); |
| EXPECT_EQ(std::string(block.metadata_ref().data, block.metadata_ref().size), expected_metadata); |
| EXPECT_EQ(block.value_bytes().size, 0); |
| ASSERT_EQ(block.value_offsets().size(), 1); |
| EXPECT_EQ(block.value_offsets().front(), 0); |
| EXPECT_EQ(block.num_rows(), 0); |
| EXPECT_NO_THROW(block.metadata_ref().validate()); |
| } |
| |
| #ifdef BE_TEST |
| TEST(VariantBatchBuilderTest, SmallCanonicalScalarsStayInlineWithoutArena) { |
| VariantBatchBuilder builder; |
| { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| row.add_null(); |
| row.add_bool(true); |
| row.add_int(-128); |
| row.add_int(128); |
| row.add_string(string_ref("abc")); |
| array.finish(); |
| row.finish(); |
| } |
| |
| EXPECT_EQ(builder.test_counters().scalar_byte_capacity, 0); |
| |
| { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| row.add_null(); |
| row.add_int(1); |
| array.finish(); |
| row.abort(); |
| } |
| EXPECT_EQ(builder.test_counters().scalar_byte_capacity, 0); |
| |
| { |
| auto row = builder.begin_row(); |
| row.add_string(string_ref("abcd")); |
| row.finish(); |
| } |
| EXPECT_GT(builder.test_counters().scalar_byte_capacity, 0); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 2); |
| const VariantRef value = block.value_at(0); |
| ASSERT_EQ(value.basic_type(), VariantBasicType::ARRAY); |
| ASSERT_EQ(value.num_elements(), 5); |
| EXPECT_TRUE(value.array_at(0).is_null()); |
| EXPECT_TRUE(value.array_at(1).get_bool()); |
| EXPECT_EQ(value.array_at(2).get_int(), -128); |
| EXPECT_EQ(value.array_at(3).get_int(), 128); |
| EXPECT_EQ(value.array_at(4).get_string(), string_ref("abc")); |
| validate_canonical(value); |
| EXPECT_EQ(block.value_at(1).get_string(), string_ref("abcd")); |
| validate_canonical(block.value_at(1)); |
| } |
| |
| TEST(VariantBatchBuilderTest, InlineScalarPathsMatchPlanFactoryAndCanonicalBytes) { |
| const OwnedBuilderValue canonical = build_owned_value([](VariantBatchBuilder::Row& builder) { |
| auto array = builder.start_array(); |
| builder.add_null(); |
| builder.add_bool(true); |
| builder.add_int(-128); |
| builder.add_int(128); |
| builder.add_string(string_ref("abc")); |
| array.finish(); |
| }); |
| const VariantRef canonical_value = canonical.ref(); |
| const std::array<std::string, 5> plan_bytes { |
| encode_scalar(VariantScalarRef::null_value()), |
| encode_scalar(VariantScalarRef::boolean(true)), |
| encode_scalar(VariantScalarRef::integer(-128)), |
| encode_scalar(VariantScalarRef::integer(128)), |
| encode_scalar(VariantScalarRef::string(string_ref("abc"))), |
| }; |
| for (size_t index = 0; index < plan_bytes.size(); ++index) { |
| const VariantRef child = canonical_value.array_at(static_cast<uint32_t>(index)); |
| EXPECT_EQ(std::string(child.value.data, child.value.size), plan_bytes[index]); |
| } |
| |
| VariantBatchBuilder builder; |
| { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| row.add_null(); |
| row.add_bool(true); |
| row.add_int(-128); |
| row.add_int(128); |
| row.add_string(string_ref("abc")); |
| array.finish(); |
| row.finish(); |
| } |
| { |
| auto row = builder.begin_row(); |
| row.add_value(canonical.ref()); |
| row.finish(); |
| } |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 2); |
| for (size_t row = 0; row < block.num_rows(); ++row) { |
| EXPECT_EQ(std::string(block.value_at(row).value.data, block.value_at(row).value.size), |
| canonical.value); |
| validate_canonical(block.value_at(row)); |
| } |
| } |
| |
| TEST(VariantBatchBuilderTest, ReserveHintIsVisibleThroughOwningBufferCounters) { |
| const VariantBatchBuilder::ReserveHint hint {.rows = 4, |
| .metadata_keys = 2, |
| .scalar_bytes = 64, |
| .nodes = 16, |
| .containers = 4, |
| .children = 12}; |
| VariantBatchBuilder builder(hint); |
| const VariantBatchBuilder::TestCounters initial = builder.test_counters(); |
| EXPECT_GE(initial.row_root_capacity, hint.rows); |
| EXPECT_GE(initial.metadata_key_capacity, hint.metadata_keys); |
| EXPECT_GE(initial.scalar_byte_capacity, hint.scalar_bytes); |
| EXPECT_GE(initial.node_capacity, hint.nodes); |
| EXPECT_GE(initial.container_capacity, hint.containers); |
| EXPECT_GE(initial.child_capacity, hint.children); |
| EXPECT_GE(initial.scope_stack_capacity, hint.containers); |
| EXPECT_GE(initial.object_id_scratch_capacity, hint.children); |
| EXPECT_GE(initial.container_plan_capacity, hint.containers); |
| EXPECT_GE(initial.planned_object_child_capacity, hint.children); |
| EXPECT_GE(initial.previous_object_token_capacity, hint.containers); |
| EXPECT_GE(initial.pending_object_token_capacity, hint.containers); |
| |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("key")); |
| row.add_string(string_ref("value")); |
| object.finish(); |
| row.finish(); |
| static_cast<void>(builder.finish_batch()); |
| EXPECT_EQ(builder.test_counters().total_capacity_growths(), 0); |
| } |
| |
| TEST(VariantBatchBuilderTest, AbortedRowCapacityGrowthIsObservedOnceAtRowBoundary) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| for (int64_t value = 0; value < 1'024; ++value) { |
| row.add_int(value); |
| } |
| row.add_int(32'768); |
| array.finish(); |
| row.abort(); |
| |
| const VariantBatchBuilder::TestCounters counters = builder.test_counters(); |
| EXPECT_EQ(counters.scalar_capacity_growths, 1); |
| EXPECT_EQ(counters.node_capacity_growths, 1); |
| EXPECT_EQ(counters.container_capacity_growths, 1); |
| EXPECT_EQ(counters.child_capacity_growths, 1); |
| EXPECT_EQ(counters.scope_stack_capacity_growths, 1); |
| EXPECT_EQ(counters.row_root_capacity_growths, 0); |
| } |
| |
| TEST(VariantBatchBuilderTest, PreviousObjectSchemaCacheTracksSuccessfulTransitions) { |
| VariantBatchBuilder builder; |
| const auto add_object_row = [&builder](std::initializer_list<std::string_view> keys) { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| int64_t value = 0; |
| for (std::string_view key : keys) { |
| // Use fresh storage on every row: a cache hit is byte identity, not pointer identity. |
| const std::string copied_key(key); |
| object.add_key(StringRef(copied_key)); |
| row.add_int(value++); |
| } |
| object.finish(); |
| row.finish(); |
| }; |
| |
| add_object_row({}); |
| add_object_row({}); |
| add_object_row({"a", "b"}); |
| add_object_row({"a", "b"}); |
| add_object_row({"b", "a"}); |
| add_object_row({"b", "a"}); |
| add_object_row({"a"}); |
| add_object_row({"a"}); |
| add_object_row({"a", "b", "c"}); |
| add_object_row({"a", "b", "c"}); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 10); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 3); |
| std::vector<VariantRef> rows; |
| rows.reserve(block.num_rows()); |
| for (size_t index = 0; index < block.num_rows(); ++index) { |
| rows.push_back(block.value_at(index)); |
| } |
| validate_canonical(block.metadata_ref(), rows); |
| EXPECT_EQ(block.value_at(0).num_elements(), 0); |
| EXPECT_EQ(block.value_at(9).num_elements(), 3); |
| |
| const VariantBatchBuilder::TestCounters counters = builder.test_counters(); |
| EXPECT_EQ(counters.object_schema_hits, 5); |
| EXPECT_EQ(counters.object_schema_fallbacks, 5); |
| EXPECT_EQ(counters.object_plan_reuses, 5); |
| EXPECT_EQ(counters.object_plan_fallbacks, 5); |
| } |
| |
| // NOLINTNEXTLINE(readability-function-cognitive-complexity): GTest macros inflate the cache matrix. |
| TEST(VariantBatchBuilderTest, PreviousObjectSchemaCachePublishesOnlySuccessfulRows) { |
| VariantBatchBuilder builder; |
| const auto add_pair_row = [&builder] { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("a")); |
| row.add_int(1); |
| object.add_key(string_ref("b")); |
| row.add_int(2); |
| object.finish(); |
| row.finish(); |
| }; |
| const auto add_array_object_row = [&builder](std::initializer_list<std::string_view> keys) { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| auto object = row.start_object(); |
| for (std::string_view key : keys) { |
| object.add_key(string_ref(key)); |
| row.add_null(); |
| } |
| object.finish(); |
| array.finish(); |
| row.finish(); |
| }; |
| const auto add_array_objects_row = [&builder](std::initializer_list<std::string_view> keys) { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| for (std::string_view key : keys) { |
| auto object = row.start_object(); |
| object.add_key(string_ref(key)); |
| row.add_null(); |
| object.finish(); |
| } |
| array.finish(); |
| row.finish(); |
| }; |
| |
| add_pair_row(); |
| { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| const std::string invalid_key(1, static_cast<char>(0xFF)); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, |
| [&] { object.add_key(StringRef(invalid_key)); }); |
| row.abort(); |
| } |
| { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("a")); |
| row.add_null(); |
| object.add_key(string_ref("ghost")); |
| row.add_null(); |
| object.finish(); |
| row.abort(); |
| } |
| add_pair_row(); |
| { |
| auto row = builder.begin_row(); |
| row.add_null(); |
| row.finish(); |
| } |
| add_pair_row(); |
| |
| const OwnedBuilderValue imported = build_owned_value([](VariantBatchBuilder::Row& source) { |
| auto object = source.start_object(); |
| object.add_key(string_ref("a")); |
| source.add_int(3); |
| object.add_key(string_ref("b")); |
| source.add_int(4); |
| object.finish(); |
| }); |
| { |
| auto row = builder.begin_row(); |
| row.add_value(imported.ref()); |
| row.finish(); |
| } |
| add_array_object_row({"a", "b"}); |
| add_array_object_row({"a", "b"}); |
| add_array_objects_row({"left", "right"}); |
| add_array_objects_row({"right"}); |
| add_array_objects_row({"right"}); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 10); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 4); |
| EXPECT_EQ(block.metadata_ref().key_at(0), string_ref("a")); |
| EXPECT_EQ(block.metadata_ref().key_at(1), string_ref("b")); |
| EXPECT_EQ(block.metadata_ref().key_at(2), string_ref("left")); |
| EXPECT_EQ(block.metadata_ref().key_at(3), string_ref("right")); |
| std::vector<VariantRef> rows; |
| rows.reserve(block.num_rows()); |
| for (size_t index = 0; index < block.num_rows(); ++index) { |
| rows.push_back(block.value_at(index)); |
| } |
| validate_canonical(block.metadata_ref(), rows); |
| EXPECT_TRUE(block.value_at(2).is_null()); |
| ASSERT_EQ(block.value_at(5).num_elements(), 1); |
| EXPECT_EQ(block.value_at(5).array_at(0).num_elements(), 2); |
| EXPECT_EQ(block.value_at(9).num_elements(), 1); |
| |
| const VariantBatchBuilder::TestCounters counters = builder.test_counters(); |
| EXPECT_EQ(counters.object_schema_hits, 5); |
| EXPECT_EQ(counters.object_schema_fallbacks, 6); |
| EXPECT_EQ(counters.object_plan_reuses, 5); |
| EXPECT_EQ(counters.object_plan_fallbacks, 5); |
| } |
| |
| TEST(VariantBatchBuilderTest, PreviousObjectSchemaCacheRejectsDuplicatesAndRecovers) { |
| VariantBatchBuilder builder; |
| const auto add_unique_row = [&builder] { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("duplicate")); |
| row.add_null(); |
| object.finish(); |
| row.finish(); |
| }; |
| add_unique_row(); |
| { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("duplicate")); |
| row.add_null(); |
| object.add_key(string_ref("duplicate")); |
| row.add_null(); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { object.finish(); }); |
| row.abort(); |
| } |
| add_unique_row(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 2); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 1); |
| const std::vector<VariantRef> rows {block.value_at(0), block.value_at(1)}; |
| validate_canonical(block.metadata_ref(), rows); |
| const VariantBatchBuilder::TestCounters counters = builder.test_counters(); |
| EXPECT_EQ(counters.object_schema_hits, 1); |
| EXPECT_EQ(counters.object_schema_fallbacks, 1); |
| EXPECT_EQ(counters.object_plan_reuses, 1); |
| EXPECT_EQ(counters.object_plan_fallbacks, 1); |
| } |
| |
| TEST(VariantBatchBuilderTest, PreviousObjectSchemaCacheAllowsSameSchemaAtShiftedOrdinal) { |
| VariantBatchBuilder builder; |
| { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| for (int64_t value : {1, 2}) { |
| auto object = row.start_object(); |
| object.add_key(string_ref("value")); |
| row.add_int(value); |
| object.finish(); |
| } |
| array.finish(); |
| row.finish(); |
| } |
| { |
| // The first logical item disappeared. The remaining item now occupies ordinal zero, and |
| // byte-identical schema makes reuse safe even though its logical path shifted. |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("value")); |
| row.add_int(2); |
| object.finish(); |
| array.finish(); |
| row.finish(); |
| } |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 2); |
| const std::vector<VariantRef> rows {block.value_at(0), block.value_at(1)}; |
| validate_canonical(block.metadata_ref(), rows); |
| EXPECT_EQ(required_field(block.value_at(1).array_at(0), "value").get_int(), 2); |
| const VariantBatchBuilder::TestCounters counters = builder.test_counters(); |
| EXPECT_EQ(counters.object_schema_hits, 1); |
| EXPECT_EQ(counters.object_schema_fallbacks, 2); |
| EXPECT_EQ(counters.object_plan_reuses, 1); |
| EXPECT_EQ(counters.object_plan_fallbacks, 2); |
| } |
| |
| // NOLINTNEXTLINE(readability-function-cognitive-complexity): GTest macros inflate the boundary matrix. |
| TEST(VariantBatchBuilderTest, PreviousObjectSchemaCacheCrossesObjectCountBoundary) { |
| VariantBatchBuilder builder; |
| const auto add_object_row = [&builder](uint32_t count) { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| for (uint32_t index = 0; index < count; ++index) { |
| const std::string key = numbered_key(index); |
| object.add_key(StringRef(key)); |
| row.add_null(); |
| } |
| object.finish(); |
| row.finish(); |
| }; |
| add_object_row(255); |
| add_object_row(255); |
| add_object_row(256); |
| add_object_row(256); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 4); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 256); |
| std::vector<VariantRef> rows; |
| rows.reserve(block.num_rows()); |
| for (size_t index = 0; index < block.num_rows(); ++index) { |
| rows.push_back(block.value_at(index)); |
| } |
| validate_canonical(block.metadata_ref(), rows); |
| EXPECT_EQ(block.value_at(0).num_elements(), 255); |
| EXPECT_EQ(block.value_at(1).num_elements(), 255); |
| EXPECT_EQ(block.value_at(2).num_elements(), 256); |
| EXPECT_EQ(block.value_at(3).num_elements(), 256); |
| const uint8_t small_header = |
| static_cast<uint8_t>(block.value_at(0).value.data[0]) >> VARIANT_VALUE_HEADER_SHIFT; |
| const uint8_t large_header = |
| static_cast<uint8_t>(block.value_at(2).value.data[0]) >> VARIANT_VALUE_HEADER_SHIFT; |
| EXPECT_EQ(small_header & VARIANT_OBJECT_LARGE_MASK, 0); |
| EXPECT_NE(large_header & VARIANT_OBJECT_LARGE_MASK, 0); |
| |
| const VariantBatchBuilder::TestCounters counters = builder.test_counters(); |
| EXPECT_EQ(counters.object_schema_hits, 2); |
| EXPECT_EQ(counters.object_schema_fallbacks, 2); |
| EXPECT_EQ(counters.object_plan_reuses, 2); |
| EXPECT_EQ(counters.object_plan_fallbacks, 2); |
| } |
| |
| TEST(VariantBatchBuilderTest, ArrayAndScalarRowsDoNotAllocateObjectCacheScratch) { |
| VariantBatchBuilder builder; |
| for (size_t index = 0; index < 32; ++index) { |
| auto row = builder.begin_row(); |
| if (index % 2 == 0) { |
| auto array = row.start_array(); |
| row.add_int(static_cast<int64_t>(index)); |
| array.finish(); |
| } else { |
| row.add_null(); |
| } |
| row.finish(); |
| } |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 32); |
| const VariantBatchBuilder::TestCounters counters = builder.test_counters(); |
| EXPECT_EQ(counters.object_schema_hits, 0); |
| EXPECT_EQ(counters.object_schema_fallbacks, 0); |
| EXPECT_EQ(counters.object_plan_reuses, 0); |
| EXPECT_EQ(counters.object_plan_fallbacks, 0); |
| EXPECT_EQ(counters.object_id_scratch_capacity_growths, 0); |
| EXPECT_EQ(counters.object_id_scratch_capacity, 0); |
| EXPECT_EQ(counters.object_token_capacity_growths, 0); |
| EXPECT_EQ(counters.previous_object_token_capacity, 0); |
| EXPECT_EQ(counters.pending_object_token_capacity, 0); |
| } |
| #endif |
| |
| TEST(VariantBatchBuilderTest, AbortAndRowErrorsRollbackBeforeTheNextRow) { |
| VariantBatchBuilder builder; |
| { |
| auto discarded = builder.begin_row(); |
| auto object = discarded.start_object(); |
| object.add_key(string_ref("destroyed")); |
| discarded.add_null(); |
| object.finish(); |
| } |
| { |
| auto bad = builder.begin_row(); |
| auto object = bad.start_object(); |
| object.add_key(string_ref("duplicate")); |
| bad.add_null(); |
| object.add_key(string_ref("duplicate")); |
| bad.add_bool(true); |
| expect_builder_exception_code(ErrorCode::INVALID_ARGUMENT, [&] { object.finish(); }); |
| bad.abort(); |
| } |
| { |
| std::string source_metadata {char {0x11}, char {0x01}, char {0x00}, char {0x0D}}; |
| source_metadata.append("failed_import"); |
| const std::string source_value {char {0x02}, |
| char {0x01}, |
| char {0x00}, |
| char {0x00}, |
| char {0x02}, |
| char {0x05}, |
| static_cast<char>(0xFF)}; |
| const VariantRef source { |
| .metadata = {.data = source_metadata.data(), .size = source_metadata.size()}, |
| .value = {source_value.data(), source_value.size()}}; |
| auto bad = builder.begin_row(); |
| expect_builder_exception_code(ErrorCode::CORRUPTION, [&] { bad.add_value(source); }); |
| bad.abort(); |
| } |
| { |
| auto retained = builder.begin_row(); |
| auto object = retained.start_object(); |
| object.add_key(string_ref("retained")); |
| retained.add_int(9); |
| object.finish(); |
| retained.finish(); |
| } |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 1); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 1); |
| EXPECT_EQ(block.metadata_ref().key_at(0), string_ref("retained")); |
| EXPECT_EQ(required_field(block.value_at(0), "retained").get_int(), 9); |
| } |
| |
| TEST(VariantBatchBuilderTest, CopiesBorrowedKeysStringsAndBinaryBeforeRowReturns) { |
| VariantBatchBuilder builder; |
| std::string key = "borrowed"; |
| std::string text = "text before mutation"; |
| std::string binary("\0\xFF", 2); |
| { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(StringRef(key)); |
| auto array = row.start_array(); |
| row.add_string(StringRef(text)); |
| row.add_binary(StringRef(binary)); |
| array.finish(); |
| object.finish(); |
| row.finish(); |
| } |
| key.assign("changed"); |
| text.assign("changed"); |
| binary.assign("changed"); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| const VariantRef array = required_field(block.value_at(0), "borrowed"); |
| ASSERT_EQ(array.num_elements(), 2); |
| EXPECT_EQ(array.array_at(0).get_string(), string_ref("text before mutation")); |
| EXPECT_EQ(array.array_at(1).get_binary(), StringRef(std::string("\0\xFF", 2))); |
| } |
| |
| TEST(VariantBatchBuilderTest, RowAddValueCanonicalizesNestedBorrowedArrayChild) { |
| OwnedBuilderValue source = make_nested_noncanonical_owned_value(); |
| VariantBatchBuilder builder; |
| { |
| auto row = builder.begin_row(); |
| row.add_value(source.ref()); |
| row.finish(); |
| } |
| { |
| auto row = builder.begin_row(); |
| auto array = row.start_array(); |
| row.add_int(7); |
| row.add_value(source.ref()); |
| source.metadata.assign("invalidated"); |
| source.value.assign("invalidated"); |
| array.finish(); |
| row.finish(); |
| } |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), 2); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 2); |
| EXPECT_EQ(block.metadata_ref().key_at(0), string_ref("a")); |
| EXPECT_EQ(block.metadata_ref().key_at(1), string_ref("b")); |
| EXPECT_EQ(block.value_at(0).metadata.data, block.value_at(1).metadata.data); |
| const std::vector<VariantRef> rows {block.value_at(0), block.value_at(1)}; |
| validate_canonical(block.metadata_ref(), rows); |
| |
| const VariantRef direct_object = block.value_at(0).array_at(0); |
| EXPECT_TRUE(required_field(direct_object, "a").get_bool()); |
| EXPECT_FALSE(required_field(direct_object, "b").get_bool()); |
| const VariantRef array_child = block.value_at(1).array_at(1).array_at(0); |
| EXPECT_TRUE(required_field(array_child, "a").get_bool()); |
| EXPECT_FALSE(required_field(array_child, "b").get_bool()); |
| } |
| |
| TEST(VariantBatchBuilderAddValueTest, ImportsEveryPrimitiveClassAndCopiesBorrowedInput) { |
| OwnedBuilderValue source = build_owned_value([](VariantBatchBuilder::Row& row) { |
| auto array = row.start_array(); |
| row.add_null(); |
| row.add_bool(false); |
| row.add_bool(true); |
| row.add_int(1); |
| row.add_int(200); |
| row.add_int(70'000); |
| row.add_int(5'000'000'000); |
| row.add_double(-1.25); |
| row.add_decimal(123, 2, 4); |
| row.add_decimal(12'345'678'901, 3, 8); |
| row.add_decimal(static_cast<__int128>(1) << 80, 4, 16); |
| row.add_date(-20'000); |
| row.add_timestamp_micros(-1, true); |
| row.add_timestamp_micros(1, false); |
| row.add_float(1.5F); |
| const std::string binary("\0\xFF", 2); |
| row.add_binary(StringRef(binary)); |
| row.add_string(string_ref("short")); |
| const std::string long_string(64, 'x'); |
| row.add_string(StringRef(long_string)); |
| row.add_time_ntz_micros(1); |
| row.add_timestamp_nanos(-1, true); |
| row.add_timestamp_nanos(1, false); |
| const std::array<uint8_t, 16> uuid {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; |
| row.add_uuid(uuid); |
| array.finish(); |
| }); |
| const std::string expected_metadata = source.metadata; |
| const std::string expected_value = source.value; |
| |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| row.add_value(source.ref()); |
| source.metadata.assign("invalidated"); |
| source.value.assign("invalidated"); |
| row.finish(); |
| VariantBatchBuilder block = builder.finish_batch(); |
| |
| EXPECT_EQ(std::string(block.metadata_ref().data, block.metadata_ref().size), expected_metadata); |
| EXPECT_EQ(std::string(block.value_at(0).value.data, block.value_at(0).value.size), |
| expected_value); |
| const VariantRef root = block.value_at(0); |
| validate_canonical(root); |
| ASSERT_EQ(root.num_elements(), 22); |
| EXPECT_EQ(root.array_at(16).basic_type(), VariantBasicType::SHORT_STRING); |
| EXPECT_EQ(root.array_at(17).primitive_id(), VariantPrimitiveId::STRING); |
| EXPECT_EQ(root.array_at(21).primitive_id(), VariantPrimitiveId::UUID); |
| } |
| |
| TEST(VariantBatchBuilderAddValueTest, ImportsNestedValuesAsActiveChildren) { |
| const OwnedBuilderValue source = make_nested_owned_value(); |
| const OwnedBuilderValue array_owned = build_owned_value([&](VariantBatchBuilder::Row& row) { |
| auto array = row.start_array(); |
| row.add_int(1); |
| row.add_value(source.ref()); |
| array.finish(); |
| }); |
| const VariantRef array_root = array_owned.ref(); |
| validate_canonical(array_root); |
| ASSERT_EQ(array_root.num_elements(), 2); |
| EXPECT_EQ(array_root.array_at(0).get_int(), 1); |
| EXPECT_EQ(required_field(required_field(array_root.array_at(1), "array").array_at(1), "leaf") |
| .get_string(), |
| string_ref("value")); |
| |
| const OwnedBuilderValue object_owned = build_owned_value([&](VariantBatchBuilder::Row& row) { |
| auto object = row.start_object(); |
| object.add_key(string_ref("wrapped")); |
| row.add_value(source.ref()); |
| object.finish(); |
| }); |
| const VariantRef object_root = object_owned.ref(); |
| validate_canonical(object_root); |
| EXPECT_EQ(required_field( |
| required_field(required_field(object_root, "wrapped"), "array").array_at(1), |
| "leaf") |
| .get_string(), |
| string_ref("value")); |
| } |
| |
| TEST(VariantBatchBuilderAddValueTest, CanonicalizesLegalNonCanonicalInputAndCopiesIt) { |
| std::string source_metadata {char {0x01}, char {0x02}, char {0x00}, char {0x01}, |
| char {0x02}, 'b', 'a'}; |
| std::string source_value { |
| char {0x02}, |
| char {0x02}, |
| char {0x01}, |
| char {0x00}, |
| char {0x01}, |
| char {0x00}, |
| char {0x02}, |
| char {static_cast<uint8_t>(VariantPrimitiveId::FALSE_VALUE) |
| << VARIANT_VALUE_HEADER_SHIFT}, |
| char {static_cast<uint8_t>(VariantPrimitiveId::TRUE_VALUE) |
| << VARIANT_VALUE_HEADER_SHIFT}, |
| }; |
| const VariantRef source { |
| .metadata = {.data = source_metadata.data(), .size = source_metadata.size()}, |
| .value = {source_value.data(), source_value.size()}}; |
| |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| row.add_value(source); |
| source_metadata.assign("invalidated"); |
| source_value.assign("invalidated"); |
| row.finish(); |
| VariantBatchBuilder block = builder.finish_batch(); |
| |
| const VariantRef canonical = block.value_at(0); |
| validate_canonical(canonical); |
| ASSERT_EQ(block.metadata_ref().dict_size(), 2); |
| EXPECT_EQ(block.metadata_ref().key_at(0), string_ref("a")); |
| EXPECT_EQ(block.metadata_ref().key_at(1), string_ref("b")); |
| EXPECT_TRUE(required_field(canonical, "a").get_bool()); |
| EXPECT_FALSE(required_field(canonical, "b").get_bool()); |
| } |
| |
| void expect_add_value_failure(int code, VariantRef source) { |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| expect_builder_exception_code(code, [&] { row.add_value(source); }); |
| row.abort(); |
| VariantBatchBuilder block = builder.finish_batch(); |
| EXPECT_EQ(block.num_rows(), 0); |
| EXPECT_EQ(block.metadata_ref().dict_size(), 0); |
| } |
| |
| TEST(VariantBatchBuilderAddValueTest, RejectsTrailingBytesDepthOverflowAndInvalidObjects) { |
| const std::string empty_metadata("\x11\0\0", 3); |
| const std::string trailing_nulls(2, '\0'); |
| expect_add_value_failure( |
| ErrorCode::CORRUPTION, |
| {.metadata = {.data = empty_metadata.data(), .size = empty_metadata.size()}, |
| .value = {trailing_nulls.data(), trailing_nulls.size()}}); |
| |
| const std::string decimal_overflow = decimal_bytes(VariantPrimitiveId::DECIMAL16, |
| static_cast<__int128>(power_of_ten(38)), 16); |
| expect_add_value_failure( |
| ErrorCode::CORRUPTION, |
| {.metadata = {.data = empty_metadata.data(), .size = empty_metadata.size()}, |
| .value = {decimal_overflow.data(), decimal_overflow.size()}}); |
| |
| const OwnedBuilderValue too_deep = build_owned_value([](VariantBatchBuilder::Row& row) { |
| std::vector<VariantBatchBuilder::Row::ArrayScope> arrays; |
| for (uint32_t depth = 0; depth <= VARIANT_MAX_NESTING_DEPTH; ++depth) { |
| arrays.emplace_back(row.start_array()); |
| } |
| row.add_null(); |
| for (auto& array : std::ranges::reverse_view(arrays)) { |
| array.finish(); |
| } |
| }); |
| expect_add_value_failure(ErrorCode::INVALID_ARGUMENT, too_deep.ref()); |
| |
| const std::string one_key_metadata {char {0x11}, char {0x01}, char {0x00}, char {0x01}, 'a'}; |
| const std::string duplicate_object {char {0x02}, char {0x02}, char {0x00}, |
| char {0x00}, char {0x00}, char {0x01}, |
| char {0x02}, char {0x00}, char {0x00}}; |
| expect_add_value_failure( |
| ErrorCode::CORRUPTION, |
| {.metadata = {.data = one_key_metadata.data(), .size = one_key_metadata.size()}, |
| .value = {duplicate_object.data(), duplicate_object.size()}}); |
| |
| const std::string two_key_metadata {char {0x11}, char {0x02}, char {0x00}, char {0x01}, |
| char {0x02}, 'a', 'b'}; |
| const auto expect_invalid_object_partition = [&](const std::string& object) { |
| expect_add_value_failure( |
| ErrorCode::CORRUPTION, |
| {.metadata = {.data = two_key_metadata.data(), .size = two_key_metadata.size()}, |
| .value = {object.data(), object.size()}}); |
| }; |
| const std::string overlapping_object {char {0x02}, char {0x02}, char {0x00}, char {0x01}, |
| char {0x00}, char {0x00}, char {0x01}, char {0x00}}; |
| const std::string object_with_gap {char {0x02}, char {0x02}, char {0x00}, char {0x01}, |
| char {0x00}, char {0x02}, char {0x03}, char {0x00}, |
| char {0x00}, char {0x00}}; |
| const std::string object_with_trailing_value { |
| char {0x02}, char {0x02}, char {0x00}, char {0x01}, char {0x00}, |
| char {0x01}, char {0x03}, char {0x00}, char {0x00}, char {0x00}}; |
| expect_invalid_object_partition(overlapping_object); |
| expect_invalid_object_partition(object_with_gap); |
| expect_invalid_object_partition(object_with_trailing_value); |
| } |
| |
| TEST(VariantBatchBuilderAddValueTest, InvalidUtf8DoesNotRetainMetadata) { |
| const std::string empty_metadata("\x11\0\0", 3); |
| const std::string invalid_string {char {0x05}, static_cast<char>(0xFF)}; |
| expect_add_value_failure( |
| ErrorCode::CORRUPTION, |
| {.metadata = {.data = empty_metadata.data(), .size = empty_metadata.size()}, |
| .value = {invalid_string.data(), invalid_string.size()}}); |
| |
| const std::string invalid_key_metadata {char {0x11}, char {0x01}, char {0x00}, char {0x01}, |
| static_cast<char>(0xFF)}; |
| const std::string object_value {char {0x02}, char {0x01}, char {0x00}, |
| char {0x00}, char {0x01}, char {0x00}}; |
| expect_add_value_failure( |
| ErrorCode::CORRUPTION, |
| {.metadata = {.data = invalid_key_metadata.data(), .size = invalid_key_metadata.size()}, |
| .value = {object_value.data(), object_value.size()}}); |
| } |
| |
| #ifdef BE_TEST |
| VariantBatchBuilder::TestCounters collect_capacity_counters(size_t rows) { |
| VariantBatchBuilder builder; |
| for (size_t index = 0; index < rows; ++index) { |
| auto row = builder.begin_row(); |
| auto object = row.start_object(); |
| object.add_key(string_ref("value")); |
| row.add_int(static_cast<int64_t>(index) + 32'768); |
| object.finish(); |
| row.finish(); |
| } |
| VariantBatchBuilder block = builder.finish_batch(); |
| EXPECT_EQ(block.num_rows(), rows); |
| std::vector<VariantRef> values; |
| values.reserve(block.num_rows()); |
| for (size_t index = 0; index < block.num_rows(); ++index) { |
| values.push_back(block.value_at(index)); |
| } |
| validate_canonical(block.metadata_ref(), values); |
| return builder.test_counters(); |
| } |
| |
| std::array<size_t, 11> owning_buffer_growths(const VariantBatchBuilder::TestCounters& value) { |
| return {value.metadata_capacity_growths, |
| value.scalar_capacity_growths, |
| value.node_capacity_growths, |
| value.container_capacity_growths, |
| value.child_capacity_growths, |
| value.scope_stack_capacity_growths, |
| value.object_id_scratch_capacity_growths, |
| value.container_plan_capacity_growths, |
| value.planned_object_child_capacity_growths, |
| value.row_root_capacity_growths, |
| value.object_token_capacity_growths}; |
| } |
| |
| TEST(VariantBatchBuilderTest, CapacityGrowthIsBatchBoundedRatherThanPerRow) { |
| const VariantBatchBuilder::TestCounters small = collect_capacity_counters(4'096); |
| const VariantBatchBuilder::TestCounters large = collect_capacity_counters(8'192); |
| for (const VariantBatchBuilder::TestCounters* counters : {&small, &large}) { |
| for (size_t growths : owning_buffer_growths(*counters)) { |
| EXPECT_GT(growths, 0); |
| } |
| } |
| EXPECT_EQ(small.metadata_unique_keys, 1); |
| EXPECT_EQ(large.metadata_unique_keys, 1); |
| EXPECT_GT(small.total_capacity_growths(), 0); |
| EXPECT_LE(large.total_capacity_growths(), small.total_capacity_growths() + 12); |
| EXPECT_GT(small.previous_object_token_capacity, 0); |
| EXPECT_GT(small.pending_object_token_capacity, 0); |
| EXPECT_GE(large.previous_object_token_capacity, small.previous_object_token_capacity); |
| EXPECT_GE(large.pending_object_token_capacity, small.pending_object_token_capacity); |
| } |
| |
| TEST(VariantBatchBuilderTest, BatchLifetimeAllocationsAreMemTrackerVisible) { |
| constexpr size_t ROWS = 2'048; |
| constexpr size_t PAYLOAD_BYTES = 4'096; |
| const std::string payload(PAYLOAD_BYTES, 'x'); |
| const auto tracker = MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER, |
| "VariantBatchBuilderTrackedAllocations"); |
| auto scoped_tracker = SwitchThreadMemTrackerLimiter(tracker); |
| thread_context()->thread_mem_tracker_mgr->flush_untracked_mem(); |
| const int64_t baseline = tracker->consumption(); |
| |
| { |
| VariantBatchBuilder builder( |
| {.rows = ROWS, .scalar_bytes = ROWS * (PAYLOAD_BYTES + 5), .nodes = ROWS}); |
| for (size_t index = 0; index < ROWS; ++index) { |
| auto row = builder.begin_row(); |
| row.add_string(StringRef(payload)); |
| row.finish(); |
| } |
| thread_context()->thread_mem_tracker_mgr->flush_untracked_mem(); |
| EXPECT_GT(tracker->consumption(), |
| baseline + static_cast<int64_t>(ROWS * PAYLOAD_BYTES / 2)); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| ASSERT_EQ(block.num_rows(), ROWS); |
| thread_context()->thread_mem_tracker_mgr->flush_untracked_mem(); |
| EXPECT_GT(tracker->consumption(), |
| baseline + static_cast<int64_t>(ROWS * PAYLOAD_BYTES / 2)); |
| } |
| |
| thread_context()->thread_mem_tracker_mgr->flush_untracked_mem(); |
| EXPECT_EQ(tracker->consumption(), baseline); |
| } |
| #endif |
| |
| TEST(VariantBatchBuilderTest, MillionFieldObjectIsCanonicalAndReadable) { |
| constexpr uint32_t FIELD_COUNT = 1'000'000; |
| VariantBatchBuilder builder; |
| auto row = builder.begin_row(); |
| auto object_scope = row.start_object(); |
| char key[7] {'k', '0', '0', '0', '0', '0', '0'}; |
| for (uint32_t index = 0; index < FIELD_COUNT; ++index) { |
| uint32_t remaining = index; |
| for (uint8_t digit = 0; digit < 6; ++digit) { |
| key[6 - digit] = static_cast<char>('0' + remaining % 10); |
| remaining /= 10; |
| } |
| object_scope.add_key({key, sizeof(key)}); |
| row.add_null(); |
| } |
| object_scope.finish(); |
| row.finish(); |
| |
| VariantBatchBuilder block = builder.finish_batch(); |
| const VariantRef value = block.value_at(0); |
| validate_canonical(value); |
| ASSERT_EQ(block.metadata_ref().dict_size(), FIELD_COUNT); |
| ASSERT_EQ(value.num_elements(), FIELD_COUNT); |
| EXPECT_EQ(block.metadata_ref().offset_size(), 3); |
| EXPECT_TRUE(value.object_value_at(0, nullptr).is_null()); |
| EXPECT_TRUE(value.object_value_at(FIELD_COUNT / 2, nullptr).is_null()); |
| EXPECT_TRUE(value.object_value_at(FIELD_COUNT - 1, nullptr).is_null()); |
| VariantRef found; |
| EXPECT_TRUE(value.object_find(string_ref("k000000"), &found)); |
| EXPECT_TRUE(found.is_null()); |
| EXPECT_TRUE(value.object_find(string_ref("k999999"), &found)); |
| EXPECT_TRUE(found.is_null()); |
| } |
| |
| } // namespace |
| } // namespace doris |