diff --git a/tuple/include/tuple_sketch.hpp b/tuple/include/tuple_sketch.hpp index 7b636a78..82a86528 100644 --- a/tuple/include/tuple_sketch.hpp +++ b/tuple/include/tuple_sketch.hpp @@ -21,6 +21,7 @@ #define TUPLE_SKETCH_HPP_ #include +#include #include "serde.hpp" #include "theta_update_sketch_base.hpp" @@ -31,6 +32,7 @@ namespace datasketches { template class tuple_sketch; template class update_tuple_sketch; template class compact_tuple_sketch; +template class wrapped_compact_tuple_sketch; template class theta_sketch_alloc; template @@ -56,14 +58,12 @@ template< typename Summary, typename Allocator = std::allocator > -class tuple_sketch { +class base_tuple_sketch { public: using Entry = std::pair; using ExtractKey = pair_extract_key; - using iterator = theta_iterator; - using const_iterator = theta_const_iterator; - virtual ~tuple_sketch() = default; + virtual ~base_tuple_sketch() = default; /** * @return allocator @@ -159,34 +159,9 @@ class tuple_sketch { */ string to_string(bool print_items = false) const; - /** - * Iterator over entries in this sketch. - * @return begin iterator - */ - virtual iterator begin() = 0; - - /** - * Iterator pointing past the valid range. - * Not to be incremented or dereferenced. - * @return end iterator - */ - virtual iterator end() = 0; - - /** - * Const iterator over entries in this sketch. - * @return begin const iterator - */ - virtual const_iterator begin() const = 0; - - /** - * Const iterator pointing past the valid range. - * Not to be incremented or dereferenced. - * @return end const iterator - */ - virtual const_iterator end() const = 0; - protected: virtual void print_specifics(std::ostringstream& os) const = 0; + virtual void print_items(std::ostringstream& os) const = 0; static uint16_t get_seed_hash(uint64_t seed); @@ -195,6 +170,30 @@ class tuple_sketch { static void check_seed_hash(uint16_t actual, uint16_t expected); }; +/** Base class for Tuple sketches whose entries are materialized in memory. */ +template< + typename Summary, + typename Allocator = std::allocator +> +class tuple_sketch: public base_tuple_sketch { +public: + using Base = base_tuple_sketch; + using Entry = typename Base::Entry; + using ExtractKey = typename Base::ExtractKey; + using iterator = theta_iterator; + using const_iterator = theta_const_iterator; + + virtual ~tuple_sketch() = default; + + virtual iterator begin() = 0; + virtual iterator end() = 0; + virtual const_iterator begin() const = 0; + virtual const_iterator end() const = 0; + +protected: + virtual void print_items(std::ostringstream& os) const; +}; + // update sketch // for types with defined default constructor and + operation @@ -485,7 +484,9 @@ class compact_tuple_sketch: public tuple_sketch { * @param other sketch to be copied * @param ordered if true make the resulting sketch ordered */ - compact_tuple_sketch(const Base& other, bool ordered); + template + compact_tuple_sketch(const Sketch& other, bool ordered, + typename std::enable_if::value>::type* = nullptr); /** * Copy constructor. @@ -650,6 +651,97 @@ class compact_tuple_sketch: public tuple_sketch { compact_tuple_sketch(bool is_empty, bool is_ordered, uint16_t seed_hash, uint64_t theta, std::vector&& entries); }; +/** + * Wrapped Compact Tuple sketch. + * This wraps serialized bytes without taking ownership or materializing all entries. + * The input buffer must remain valid for the lifetime of this sketch and its iterators. + */ +template< + typename Summary, + typename Allocator = std::allocator, + typename SerDe = serde +> +class wrapped_compact_tuple_sketch: public base_tuple_sketch { +public: + using Base = base_tuple_sketch; + using Entry = typename Base::Entry; + class const_iterator; + + Allocator get_allocator() const; + bool is_empty() const; + bool is_ordered() const; + uint64_t get_theta64() const; + uint32_t get_num_retained() const; + uint16_t get_seed_hash() const; + + const_iterator begin() const; + const_iterator end() const; + + /** + * Wraps a buffer containing a serialized Compact Tuple sketch. + * The complete buffer is validated before this method returns. + */ + static wrapped_compact_tuple_sketch wrap(const void* bytes, size_t size, + uint64_t seed = DEFAULT_SEED, const SerDe& sd = SerDe(), + const Allocator& allocator = Allocator()); + +protected: + virtual void print_specifics(std::ostringstream& os) const; + virtual void print_items(std::ostringstream& os) const; + +private: + bool is_empty_; + bool is_ordered_; + uint16_t seed_hash_; + uint32_t num_entries_; + uint64_t theta_; + const char* entries_start_; + const char* entries_end_; + SerDe sd_; + Allocator allocator_; + + wrapped_compact_tuple_sketch(bool is_empty, bool is_ordered, uint16_t seed_hash, + uint32_t num_entries, uint64_t theta, const char* entries_start, + const char* entries_end, const SerDe& sd, const Allocator& allocator); +}; + +template +class wrapped_compact_tuple_sketch::const_iterator { +public: + using iterator_category = std::input_iterator_tag; + using value_type = Entry; + using difference_type = std::ptrdiff_t; + using pointer = const Entry*; + using reference = const Entry&; + + const_iterator(const char* ptr, const char* end, uint32_t num_entries, + uint32_t index, const SerDe& sd); + const_iterator(const const_iterator& other); + const_iterator& operator=(const const_iterator& other); + ~const_iterator(); + + const_iterator& operator++(); + const_iterator operator++(int); + bool operator==(const const_iterator& other) const; + bool operator!=(const const_iterator& other) const; + reference operator*() const; + pointer operator->() const; + +private: + const char* ptr_; + const char* end_; + uint32_t num_entries_; + uint32_t index_; + SerDe sd_; + typename std::aligned_storage::type entry_storage_; + bool entry_initialized_; + + Entry* entry(); + const Entry* entry() const; + void load_entry(); + void destroy_entry(); +}; + /// Tuple base builder template class tuple_base_builder: public theta_base_builder { diff --git a/tuple/include/tuple_sketch_impl.hpp b/tuple/include/tuple_sketch_impl.hpp index 0c64a3c8..391f801d 100644 --- a/tuple/include/tuple_sketch_impl.hpp +++ b/tuple/include/tuple_sketch_impl.hpp @@ -25,48 +25,59 @@ namespace datasketches { +template +auto print_tuple_entry(std::ostringstream& os, const Entry& entry, int) + -> decltype(os << entry.second, void()) { + os << entry.first << ": " << entry.second << std::endl; +} + +template +void print_tuple_entry(std::ostringstream& os, const Entry& entry, long) { + os << entry.first << std::endl; +} + template -bool tuple_sketch::is_estimation_mode() const { +bool base_tuple_sketch::is_estimation_mode() const { return get_theta64() < theta_constants::MAX_THETA && !is_empty(); } template -double tuple_sketch::get_theta() const { +double base_tuple_sketch::get_theta() const { return static_cast(get_theta64()) / static_cast(theta_constants::MAX_THETA); } template -double tuple_sketch::get_estimate() const { +double base_tuple_sketch::get_estimate() const { return get_num_retained() / get_theta(); } template -double tuple_sketch::get_lower_bound(uint8_t num_std_devs, uint32_t num_subset_entries) const { +double base_tuple_sketch::get_lower_bound(uint8_t num_std_devs, uint32_t num_subset_entries) const { num_subset_entries = std::min(num_subset_entries, get_num_retained()) ; if (!is_estimation_mode()) return num_subset_entries; return binomial_bounds::get_lower_bound(num_subset_entries, get_theta(), num_std_devs); } template -double tuple_sketch::get_lower_bound(uint8_t num_std_devs) const { +double base_tuple_sketch::get_lower_bound(uint8_t num_std_devs) const { return get_lower_bound(num_std_devs, get_num_retained()) ; } template -double tuple_sketch::get_upper_bound(uint8_t num_std_devs, uint32_t num_subset_entries) const { +double base_tuple_sketch::get_upper_bound(uint8_t num_std_devs, uint32_t num_subset_entries) const { num_subset_entries = std::min(num_subset_entries, get_num_retained()) ; if (!is_estimation_mode()) return num_subset_entries; return binomial_bounds::get_upper_bound(num_subset_entries, get_theta(), num_std_devs); } template -double tuple_sketch::get_upper_bound(uint8_t num_std_devs) const { +double base_tuple_sketch::get_upper_bound(uint8_t num_std_devs) const { return get_upper_bound(num_std_devs, get_num_retained()) ; } template -string tuple_sketch::to_string(bool detail) const { +string base_tuple_sketch::to_string(bool detail) const { // Using a temporary stream for implementation here does not comply with AllocatorAwareContainer requirements. // The stream does not support passing an allocator instance, and alternatives are complicated. std::ostringstream os; @@ -84,15 +95,20 @@ string tuple_sketch::to_string(bool detail) const { print_specifics(os); os << "### End sketch summary" << std::endl; if (detail) { - os << "### Retained entries" << std::endl; - for (const auto& it: *this) { - os << it.first << ": " << it.second << std::endl; - } - os << "### End retained entries" << std::endl; + print_items(os); } return string(os.str().c_str(), get_allocator()); } +template +void tuple_sketch::print_items(std::ostringstream& os) const { + os << "### Retained entries" << std::endl; + for (const auto& entry: *this) { + print_tuple_entry(os, entry, 0); + } + os << "### End retained entries" << std::endl; +} + // update sketch template @@ -284,7 +300,9 @@ entries_(std::move(entries)) {} template -compact_tuple_sketch::compact_tuple_sketch(const Base& other, bool ordered): +template +compact_tuple_sketch::compact_tuple_sketch(const Sketch& other, bool ordered, + typename std::enable_if::value>::type*): is_empty_(other.is_empty()), is_ordered_(other.is_ordered() || ordered), seed_hash_(other.get_seed_hash()), @@ -617,6 +635,294 @@ auto compact_tuple_sketch::end() const -> const_iterator { template void compact_tuple_sketch::print_specifics(std::ostringstream&) const {} +// wrapped compact sketch + +template +wrapped_compact_tuple_sketch::wrapped_compact_tuple_sketch( + bool is_empty, bool is_ordered, uint16_t seed_hash, uint32_t num_entries, + uint64_t theta, const char* entries_start, const char* entries_end, + const SD& sd, const A& allocator): +is_empty_(is_empty), +is_ordered_(is_ordered), +seed_hash_(seed_hash), +num_entries_(num_entries), +theta_(theta), +entries_start_(entries_start), +entries_end_(entries_end), +sd_(sd), +allocator_(allocator) +{} + +template +A wrapped_compact_tuple_sketch::get_allocator() const { + return allocator_; +} + +template +bool wrapped_compact_tuple_sketch::is_empty() const { + return is_empty_; +} + +template +bool wrapped_compact_tuple_sketch::is_ordered() const { + return is_ordered_; +} + +template +uint64_t wrapped_compact_tuple_sketch::get_theta64() const { + return is_empty_ ? theta_constants::MAX_THETA : theta_; +} + +template +uint32_t wrapped_compact_tuple_sketch::get_num_retained() const { + return num_entries_; +} + +template +uint16_t wrapped_compact_tuple_sketch::get_seed_hash() const { + return is_empty_ ? 0 : seed_hash_; +} + +template +auto wrapped_compact_tuple_sketch::begin() const -> const_iterator { + return const_iterator(entries_start_, entries_end_, num_entries_, 0, sd_); +} + +template +auto wrapped_compact_tuple_sketch::end() const -> const_iterator { + return const_iterator(entries_end_, entries_end_, num_entries_, num_entries_, sd_); +} + +template +wrapped_compact_tuple_sketch wrapped_compact_tuple_sketch::wrap( + const void* bytes, size_t size, uint64_t seed, const SD& sd, const A& allocator) { + ensure_minimum_memory(size, sizeof(uint64_t)); + const char* ptr = static_cast(bytes); + const char* const end = ptr + size; + + uint8_t preamble_longs; + ptr += copy_from_mem(ptr, preamble_longs); + uint8_t serial_version; + ptr += copy_from_mem(ptr, serial_version); + uint8_t family; + ptr += copy_from_mem(ptr, family); + uint8_t type; + ptr += copy_from_mem(ptr, type); + ptr += sizeof(uint8_t); // unused + uint8_t flags_byte; + ptr += copy_from_mem(ptr, flags_byte); + uint16_t seed_hash; + ptr += copy_from_mem(ptr, seed_hash); + + using CompactSketch = compact_tuple_sketch; + if (preamble_longs < 1 || preamble_longs > 3) { + throw std::invalid_argument("invalid preamble length"); + } + if (serial_version != CompactSketch::SERIAL_VERSION + && serial_version != CompactSketch::SERIAL_VERSION_LEGACY) { + throw std::invalid_argument("serial version mismatch: expected " + + std::to_string(CompactSketch::SERIAL_VERSION) + " or " + + std::to_string(CompactSketch::SERIAL_VERSION_LEGACY) + ", actual " + + std::to_string(serial_version)); + } + checker::check_sketch_family(family, CompactSketch::SKETCH_FAMILY); + if (type != CompactSketch::SKETCH_TYPE && type != CompactSketch::SKETCH_TYPE_LEGACY) { + throw std::invalid_argument("sketch type mismatch: expected " + + std::to_string(CompactSketch::SKETCH_TYPE) + " or " + + std::to_string(CompactSketch::SKETCH_TYPE_LEGACY) + ", actual " + + std::to_string(type)); + } + + const bool is_empty = flags_byte & (1 << CompactSketch::flags::IS_EMPTY); + if (is_empty && preamble_longs != 1) { + throw std::invalid_argument("empty sketch must have one preamble long"); + } + if (!is_empty) checker::check_seed_hash(seed_hash, compute_seed_hash(seed)); + + uint64_t theta = theta_constants::MAX_THETA; + uint32_t num_entries = 0; + if (!is_empty) { + if (preamble_longs == 1) { + num_entries = 1; + } else { + ensure_minimum_memory(static_cast(end - ptr), sizeof(uint64_t)); + ptr += copy_from_mem(ptr, num_entries); + ptr += sizeof(uint32_t); // unused + if (preamble_longs == 3) { + ensure_minimum_memory(static_cast(end - ptr), sizeof(uint64_t)); + ptr += copy_from_mem(ptr, theta); + } + } + } + + const char* const entries_start = ptr; + for (uint32_t i = 0; i < num_entries; ++i) { + ensure_minimum_memory(static_cast(end - ptr), sizeof(uint64_t)); + ptr += sizeof(uint64_t); + typename std::aligned_storage::type summary_storage; + S* summary = reinterpret_cast(&summary_storage); + const size_t bytes_read = sd.deserialize( + ptr, static_cast(end - ptr), summary, 1); + check_memory_size(bytes_read, static_cast(end - ptr)); + ptr += bytes_read; + summary->~S(); + } + if (ptr != end) { + throw std::invalid_argument("serialized sketch size mismatch"); + } + + const bool is_ordered = flags_byte & (1 << CompactSketch::flags::IS_ORDERED); + return wrapped_compact_tuple_sketch(is_empty, is_ordered, seed_hash, num_entries, + theta, entries_start, ptr, sd, allocator); +} + +template +void wrapped_compact_tuple_sketch::print_specifics(std::ostringstream&) const {} + +template +void wrapped_compact_tuple_sketch::print_items(std::ostringstream& os) const { + os << "### Retained entries" << std::endl; + for (const auto& entry: *this) { + print_tuple_entry(os, entry, 0); + } + os << "### End retained entries" << std::endl; +} + +template +wrapped_compact_tuple_sketch::const_iterator::const_iterator( + const char* ptr, const char* end, uint32_t num_entries, uint32_t index, + const SD& sd): +ptr_(ptr), +end_(end), +num_entries_(num_entries), +index_(index), +sd_(sd), +entry_initialized_(false) +{ + if (index_ < num_entries_) load_entry(); +} + +template +wrapped_compact_tuple_sketch::const_iterator::const_iterator( + const const_iterator& other): +ptr_(other.ptr_), +end_(other.end_), +num_entries_(other.num_entries_), +index_(other.index_), +sd_(other.sd_), +entry_initialized_(false) +{ + if (other.entry_initialized_) { + new (entry()) Entry(*other.entry()); + entry_initialized_ = true; + } +} + +template +auto wrapped_compact_tuple_sketch::const_iterator::operator=( + const const_iterator& other) -> const_iterator& { + if (this != &other) { + destroy_entry(); + ptr_ = other.ptr_; + end_ = other.end_; + num_entries_ = other.num_entries_; + index_ = other.index_; + sd_ = other.sd_; + if (other.entry_initialized_) { + new (entry()) Entry(*other.entry()); + entry_initialized_ = true; + } + } + return *this; +} + +template +wrapped_compact_tuple_sketch::const_iterator::~const_iterator() { + destroy_entry(); +} + +template +auto wrapped_compact_tuple_sketch::const_iterator::operator++() + -> const_iterator& { + destroy_entry(); + ++index_; + if (index_ < num_entries_) load_entry(); + return *this; +} + +template +auto wrapped_compact_tuple_sketch::const_iterator::operator++(int) + -> const_iterator { + const_iterator previous(*this); + operator++(); + return previous; +} + +template +bool wrapped_compact_tuple_sketch::const_iterator::operator==( + const const_iterator& other) const { + return index_ == other.index_ && ptr_ == other.ptr_; +} + +template +bool wrapped_compact_tuple_sketch::const_iterator::operator!=( + const const_iterator& other) const { + return !(*this == other); +} + +template +auto wrapped_compact_tuple_sketch::const_iterator::operator*() const + -> reference { + return *entry(); +} + +template +auto wrapped_compact_tuple_sketch::const_iterator::operator->() const + -> pointer { + return entry(); +} + +template +auto wrapped_compact_tuple_sketch::const_iterator::entry() -> Entry* { + return reinterpret_cast(&entry_storage_); +} + +template +auto wrapped_compact_tuple_sketch::const_iterator::entry() const + -> const Entry* { + return reinterpret_cast(&entry_storage_); +} + +template +void wrapped_compact_tuple_sketch::const_iterator::load_entry() { + ensure_minimum_memory(static_cast(end_ - ptr_), sizeof(uint64_t)); + uint64_t key; + ptr_ += copy_from_mem(ptr_, key); + + typename std::aligned_storage::type summary_storage; + S* summary = reinterpret_cast(&summary_storage); + const size_t bytes_read = sd_.deserialize( + ptr_, static_cast(end_ - ptr_), summary, 1); + check_memory_size(bytes_read, static_cast(end_ - ptr_)); + ptr_ += bytes_read; + try { + new (entry()) Entry(key, std::move(*summary)); + } catch (...) { + summary->~S(); + throw; + } + summary->~S(); + entry_initialized_ = true; +} + +template +void wrapped_compact_tuple_sketch::const_iterator::destroy_entry() { + if (entry_initialized_) { + entry()->~Entry(); + entry_initialized_ = false; + } +} + // builder template diff --git a/tuple/test/CMakeLists.txt b/tuple/test/CMakeLists.txt index 3d7ccca3..18306490 100644 --- a/tuple/test/CMakeLists.txt +++ b/tuple/test/CMakeLists.txt @@ -38,6 +38,7 @@ add_test( target_sources(tuple_test PRIVATE tuple_sketch_test.cpp + wrapped_compact_tuple_sketch_test.cpp tuple_sketch_allocation_test.cpp tuple_union_test.cpp tuple_intersection_test.cpp diff --git a/tuple/test/wrapped_compact_tuple_sketch_test.cpp b/tuple/test/wrapped_compact_tuple_sketch_test.cpp new file mode 100644 index 00000000..baa99226 --- /dev/null +++ b/tuple/test/wrapped_compact_tuple_sketch_test.cpp @@ -0,0 +1,206 @@ +/* + * 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 +#include + +#include +#include +#include +#include +#include +#include + +namespace datasketches { + +template +void check_same_entries(const Expected& expected, const Actual& actual) { + REQUIRE(actual.get_num_retained() == expected.get_num_retained()); + auto expected_it = expected.begin(); + auto actual_it = actual.begin(); + while (expected_it != expected.end()) { + REQUIRE(actual_it != actual.end()); + REQUIRE(actual_it->first == expected_it->first); + REQUIRE(actual_it->second == expected_it->second); + ++expected_it; + ++actual_it; + } + REQUIRE(actual_it == actual.end()); +} + +TEST_CASE("wrapped tuple sketch: empty, exact and estimation", "[tuple_sketch]") { + { + auto compact = update_tuple_sketch::builder().build().compact(); + auto bytes = compact.serialize(); + auto wrapped = wrapped_compact_tuple_sketch::wrap(bytes.data(), bytes.size()); + REQUIRE(wrapped.is_empty()); + REQUIRE(wrapped.is_ordered()); + REQUIRE_FALSE(wrapped.is_estimation_mode()); + REQUIRE(wrapped.get_num_retained() == 0); + REQUIRE(wrapped.get_estimate() == 0); + REQUIRE(wrapped.begin() == wrapped.end()); + } + + { + auto update = update_tuple_sketch::builder().build(); + update.update(1, 2.5f); + auto compact = update.compact(); + auto bytes = compact.serialize(); + auto wrapped = wrapped_compact_tuple_sketch::wrap(bytes.data(), bytes.size()); + REQUIRE_FALSE(wrapped.is_empty()); + REQUIRE(wrapped.is_ordered()); + REQUIRE_FALSE(wrapped.is_estimation_mode()); + REQUIRE(wrapped.get_num_retained() == 1); + REQUIRE(wrapped.get_theta64() == compact.get_theta64()); + REQUIRE(wrapped.get_seed_hash() == compact.get_seed_hash()); + check_same_entries(compact, wrapped); + + auto it = wrapped.begin(); + auto previous = it++; + REQUIRE(previous->second == 2.5f); + REQUIRE(it == wrapped.end()); + } + + { + auto update = update_tuple_sketch::builder().set_lg_k(6).build(); + for (int i = 0; i < 10000; ++i) update.update(i, static_cast(i)); + auto compact = update.compact(); + auto bytes = compact.serialize(); + auto wrapped = wrapped_compact_tuple_sketch::wrap(bytes.data(), bytes.size()); + REQUIRE_FALSE(wrapped.is_empty()); + REQUIRE(wrapped.is_estimation_mode()); + REQUIRE(wrapped.get_theta64() == compact.get_theta64()); + REQUIRE(wrapped.get_estimate() == compact.get_estimate()); + REQUIRE(wrapped.get_lower_bound(2) == compact.get_lower_bound(2)); + REQUIRE(wrapped.get_upper_bound(2) == compact.get_upper_bound(2)); + check_same_entries(compact, wrapped); + REQUIRE(wrapped.to_string(true).find("### Retained entries") != std::string::npos); + } +} + +TEST_CASE("wrapped tuple sketch: variable-width summaries", "[tuple_sketch]") { + auto update = update_tuple_sketch::builder().build(); + update.update(1, std::string("a")); + update.update(2, std::string("variable width")); + update.update(3, std::string(80, 'x')); + auto compact = update.compact(); + auto bytes = compact.serialize(); + auto wrapped = wrapped_compact_tuple_sketch::wrap( + bytes.data(), bytes.size()); + check_same_entries(compact, wrapped); +} + +TEST_CASE("wrapped tuple sketch: validates the complete buffer", "[tuple_sketch]") { + auto update = update_tuple_sketch::builder().build(); + update.update(1, 1.0f); + auto bytes = update.compact().serialize(); + + for (size_t size = 0; size < bytes.size(); ++size) { + REQUIRE_THROWS(wrapped_compact_tuple_sketch::wrap(bytes.data(), size)); + } + + auto with_trailing_byte = bytes; + with_trailing_byte.push_back(0); + REQUIRE_THROWS_AS(wrapped_compact_tuple_sketch::wrap( + with_trailing_byte.data(), with_trailing_byte.size()), std::invalid_argument); + + REQUIRE_THROWS_AS(wrapped_compact_tuple_sketch::wrap( + bytes.data(), bytes.size(), 123), std::invalid_argument); + REQUIRE_THROWS(wrapped_compact_tuple_sketch::wrap(bytes.data(), bytes.size())); + + auto invalid_preamble = bytes; + invalid_preamble[0] = 4; + REQUIRE_THROWS_AS(wrapped_compact_tuple_sketch::wrap( + invalid_preamble.data(), invalid_preamble.size()), std::invalid_argument); + + auto string_update = update_tuple_sketch::builder().build(); + string_update.update(1, std::string("summary")); + auto string_bytes = string_update.compact().serialize(); + REQUIRE_THROWS(wrapped_compact_tuple_sketch::wrap( + string_bytes.data(), string_bytes.size() - 1)); +} + +struct wrapped_intersection_policy { + void operator()(float& summary, const float& other) const { + summary += other; + } +}; + +TEST_CASE("wrapped tuple sketch: set operations", "[tuple_sketch]") { + auto a = update_tuple_sketch::builder().build(); + auto b = update_tuple_sketch::builder().build(); + for (int i = 0; i < 100; ++i) a.update(i, 1.0f); + for (int i = 50; i < 150; ++i) b.update(i, 1.0f); + + auto a_bytes = a.compact().serialize(); + auto b_bytes = b.compact().serialize(); + auto wrapped_a = wrapped_compact_tuple_sketch::wrap( + a_bytes.data(), a_bytes.size()); + auto wrapped_b = wrapped_compact_tuple_sketch::wrap( + b_bytes.data(), b_bytes.size()); + + auto tuple_union = datasketches::tuple_union::builder().build(); + tuple_union.update(wrapped_a); + tuple_union.update(wrapped_b); + auto union_result = tuple_union.get_result(); + REQUIRE(union_result.get_num_retained() == 150); + float union_sum = 0; + for (const auto& entry: union_result) union_sum += entry.second; + REQUIRE(union_sum == 200.0f); + + tuple_intersection intersection; + intersection.update(wrapped_a); + intersection.update(wrapped_b); + auto intersection_result = intersection.get_result(); + REQUIRE(intersection_result.get_num_retained() == 50); + for (const auto& entry: intersection_result) REQUIRE(entry.second == 2.0f); + + tuple_a_not_b a_not_b; + auto difference = a_not_b.compute(wrapped_a, wrapped_b); + REQUIRE(difference.get_num_retained() == 50); + + auto empty_bytes = update_tuple_sketch::builder().build().compact().serialize(); + auto wrapped_empty = wrapped_compact_tuple_sketch::wrap( + empty_bytes.data(), empty_bytes.size()); + auto unchanged = a_not_b.compute(wrapped_a, wrapped_empty); + REQUIRE(unchanged.get_num_retained() == wrapped_a.get_num_retained()); + check_same_entries(wrapped_a, unchanged); +} + +TEST_CASE("wrapped tuple sketch: arithmetic iteration does not allocate", "[tuple_sketch]") { + auto update = update_tuple_sketch::builder().build(); + for (int i = 0; i < 100; ++i) update.update(i, 1.0f); + auto bytes = update.compact().serialize(); + + test_allocator_total_bytes = 0; + test_allocator_net_allocations = 0; + using Wrapped = wrapped_compact_tuple_sketch>; + auto wrapped = Wrapped::wrap(bytes.data(), bytes.size(), DEFAULT_SEED, + serde(), test_allocator(0)); + uint32_t count = 0; + for (const auto& entry: wrapped) { + REQUIRE(entry.second == 1.0f); + ++count; + } + REQUIRE(count == wrapped.get_num_retained()); + REQUIRE(test_allocator_total_bytes == 0); + REQUIRE(test_allocator_net_allocations == 0); +} + +} /* namespace datasketches */