From 32a0018ef85703261d31dad8068772df97a2ae7f Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 19 Jul 2026 19:00:37 -0400 Subject: [PATCH 1/2] Add hashmaps SoA table with hashmap head. --- .../database/impl/primitives/hashmaps.ipp | 614 ++++++++++++++++++ .../bitcoin/database/primitives/hashmaps.hpp | 282 ++++++++ test/primitives/hashmaps.cpp | 295 +++++++++ 3 files changed, 1191 insertions(+) create mode 100644 include/bitcoin/database/impl/primitives/hashmaps.ipp create mode 100644 include/bitcoin/database/primitives/hashmaps.hpp create mode 100644 test/primitives/hashmaps.cpp diff --git a/include/bitcoin/database/impl/primitives/hashmaps.ipp b/include/bitcoin/database/impl/primitives/hashmaps.ipp new file mode 100644 index 000000000..e69c11660 --- /dev/null +++ b/include/bitcoin/database/impl/primitives/hashmaps.ipp @@ -0,0 +1,614 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_DATABASE_PRIMITIVES_HASHMAPS_IPP +#define LIBBITCOIN_DATABASE_PRIMITIVES_HASHMAPS_IPP + +#include + +namespace libbitcoin { +namespace database { + +TEMPLATE +CLASS::hashmaps(storage& header, storage& body, const Link& buckets) NOEXCEPT + : head_(header, buckets), body_(body) +{ +} + +// not thread safe +// ---------------------------------------------------------------------------- + +TEMPLATE +bool CLASS::create() NOEXCEPT +{ + Link count{}; + return head_.create() && head_.get_body_count(count) && + body_.truncate(count); +} + +TEMPLATE +bool CLASS::close() NOEXCEPT +{ + return head_.set_body_count(body_.count()); +} + +TEMPLATE +bool CLASS::backup(bool) NOEXCEPT +{ + return head_.set_body_count(body_.count()); +} + +TEMPLATE +bool CLASS::restore() NOEXCEPT +{ + Link count{}; + return head_.verify() && head_.get_body_count(count) && + body_.truncate(count); +} + +TEMPLATE +bool CLASS::verify() const NOEXCEPT +{ + Link count{}; + return head_.verify() && head_.get_body_count(count) && + (count == body_.count()); +} + +// sizing +// ---------------------------------------------------------------------------- + +TEMPLATE +bool CLASS::enabled() const NOEXCEPT +{ + return head_.buckets() > one; +} + +TEMPLATE +size_t CLASS::buckets() const NOEXCEPT +{ + return head_.buckets(); +} + +TEMPLATE +size_t CLASS::head_size() const NOEXCEPT +{ + return head_.size(); +} + +TEMPLATE +size_t CLASS::body_size() const NOEXCEPT +{ + return body_.size(); +} + +TEMPLATE +size_t CLASS::capacity() const NOEXCEPT +{ + return body_.capacity(); +} + +TEMPLATE +Link CLASS::count() const NOEXCEPT +{ + return body_.count(); +} + +// error condition +// ---------------------------------------------------------------------------- + +TEMPLATE +code CLASS::get_fault() const NOEXCEPT +{ + return body_.get_fault(); +} + +TEMPLATE +size_t CLASS::get_space() const NOEXCEPT +{ + return body_.get_space(); +} + +TEMPLATE +code CLASS::reload() NOEXCEPT +{ + return body_.reload(); +} + +// query interface +// ---------------------------------------------------------------------------- + +TEMPLATE +inline Link CLASS::top(const Link& link) const NOEXCEPT +{ + if (link >= head_.buckets()) + return {}; + + return head_.top(link); +} + +TEMPLATE +inline bool CLASS::exists(const memory& ptr, const Key& key) const NOEXCEPT +{ + return !first(ptr, key).is_terminal(); +} + +TEMPLATE +inline bool CLASS::exists(const Key& key) const NOEXCEPT +{ + return !first(key).is_terminal(); +} + +TEMPLATE +inline Link CLASS::first(const memory& ptr, const Key& key) const NOEXCEPT +{ + return first(ptr, head_.top(key), key); +} + +TEMPLATE +inline Link CLASS::first(const Key& key) const NOEXCEPT +{ + return first(get_memory(), key); +} + +TEMPLATE +inline typename CLASS::iterator CLASS::it(Key&& key) const NOEXCEPT +{ + const auto top = head_.top(key); + return { get_memory(), top, std::forward(key) }; +} + +TEMPLATE +inline typename CLASS::iterator CLASS::it(const Key& key) const NOEXCEPT +{ + return { get_memory(), head_.top(key), key }; +} + +TEMPLATE +inline Link CLASS::allocate(const Link& size) NOEXCEPT +{ + return body_.allocate(size); +} + +TEMPLATE +template +inline memory CLASS::get_memory() const NOEXCEPT +{ + return body_.template get(); +} + +TEMPLATE +Key CLASS::get_key(const Link& link) NOEXCEPT +{ + using namespace system; + const auto ptr = body_.get(link); + if (!ptr || is_lesser(ptr.size(), index_size)) + return {}; + + return unsafe_array_cast(std::next(ptr.begin(), + Link::size)); +} + +// spine (column zero) +// ---------------------------------------------------------------------------- + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::find(const Key& key, Element& element) const NOEXCEPT +{ + return !find_link(key, element).is_terminal(); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline Link CLASS::find_link(const Key& key, Element& element) const NOEXCEPT +{ + // This override avoids duplicated memory construct in get(first()). + const auto ptr = get_memory(); + const auto link = first(ptr, head_.top(key), key); + if (link.is_terminal()) + return {}; + + return read(ptr, link, element); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::get(const Link& link, Element& element) const NOEXCEPT +{ + // This override is the normal form. + return read(get_memory(), link, element); +} + +// static +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::get(const memory& ptr, const Link& link, + Element& element) NOEXCEPT +{ + return read(ptr, link, element); +} + +// static +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::get(const iterator& it, Element& element) NOEXCEPT +{ + // This override avoids deadlock when holding iterator to the same table. + return read(it.ptr(), *it, element); +} + +// static +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::get(const iterator& it, const Link& link, + Element& element) NOEXCEPT +{ + // This override avoids deadlock when holding iterator to the same table. + return read(it.ptr(), link, element); +} + +// static +TEMPLATE +ELEMENT_CONSTRAINT +bool CLASS::set(const memory& ptr, const Link& link, const Key& key, + const Element& element) NOEXCEPT +{ + using namespace system; + if (!ptr) + return false; + + const auto start = body::link_to_position(link); + if (is_limited(start)) + return false; + + const auto size = ptr.size(); + const auto position = possible_narrow_and_sign_cast(start); + if (position >= size) + return false; + + // Stream starts at record and the index is skipped for reader convenience. + const auto offset = ptr.offset(start); + if (is_null(offset)) + return false; + + // Set element search key. + unsafe_array_cast(std::next(offset, + Link::size)) = key; + + iostream stream{ offset, size - position }; + finalizer sink{ stream }; + sink.skip_bytes(index_size); + + if constexpr (!is_slab) { BC_DEBUG_ONLY(sink.set_limit(RowSize * element.count());) } + return element.to_data(sink); +} + +TEMPLATE +ELEMENT_CONSTRAINT +bool CLASS::set(const Link& link, const Key& key, + const Element& element) NOEXCEPT +{ + return set(get_memory(), link, key, element); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline Link CLASS::set_link(const Key& key, const Element& element) NOEXCEPT +{ + Link link{}; + if (!set_link(link, key, element)) + return {}; + + return link; +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::set_link(Link& link, const Key& key, + const Element& element) NOEXCEPT +{ + link = allocate(element.count()); + return set(link, key, element); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline Link CLASS::put_link(const Key& key, const Element& element) NOEXCEPT +{ + Link link{}; + if (!put_link(link, key, element)) + return {}; + + return link; +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::put_link(Link& link, const Key& key, + const Element& element) NOEXCEPT +{ + link = allocate(element.count()); + return put(link, key, element); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::put(const Key& key, const Element& element) NOEXCEPT +{ + return !put_link(key, element).is_terminal(); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::put(const Link& link, const Key& key, + const Element& element) NOEXCEPT +{ + // This override is the normal form. + return write(get_memory(), link, key, element); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::put(const memory& ptr, const Link& link, const Key& key, + const Element& element) NOEXCEPT +{ + return write(ptr, link, key, element); +} + +TEMPLATE +ELEMENT_CONSTRAINT +inline bool CLASS::put(bool& duplicate, const memory& ptr, + const Link& link, const Key& key, const Element& element) NOEXCEPT +{ + Link previous{}; + if (!write(previous, ptr, link, key, element)) + return false; + + if (previous.is_terminal()) + { + duplicate = false; + } + else + { + // Search the previous conflicts to determine if actual duplicate. + duplicate = !first(ptr, previous, key).is_terminal(); + } + + return true; +} + +TEMPLATE +inline Link CLASS::commit_link(const Link& link, const Key& key) NOEXCEPT +{ + if (!commit(link, key)) + return {}; + + return link; +} + +TEMPLATE +inline bool CLASS::commit(const Link& link, const Key& key) NOEXCEPT +{ + return commit(get_memory(), link, key); +} + +TEMPLATE +bool CLASS::commit(const memory& ptr, const Link& link, + const Key& key) NOEXCEPT +{ + using namespace system; + if (!ptr) + return false; + + // get element offset (fault) + const auto offset = ptr.offset(body::link_to_position(link)); + if (is_null(offset)) + return false; + + // Commit element to search index (terminal is a valid bucket index). + auto& next = unsafe_array_cast(offset); + return head_.push(link, next, key); +} + +// satellites (nonzero Column) +// ---------------------------------------------------------------------------- + +// static +TEMPLATE +template +bool CLASS::get(const memory& ptr, const Link& link, Element& element) NOEXCEPT +{ + static_assert(is_nonzero(Column), "column zero is the keyed spine"); + static_assert(Element::size == width); + if (!ptr || link.is_terminal()) + return false; + + using namespace system; + const auto start = body::template link_to_position(link); + if (is_limited(start)) + return false; + + const auto size = ptr.size(); + const auto position = possible_narrow_and_sign_cast(start); + if (position >= size) + return false; + + const auto offset = ptr.offset(start); + if (is_null(offset)) + return false; + + iostream stream{ offset, size - position }; + reader source{ stream }; + + BC_DEBUG_ONLY(source.set_limit(width * element.count());) + return element.from_data(source); +} + +TEMPLATE +template +bool CLASS::get(const Link& link, Element& element) const NOEXCEPT +{ + return get(get_memory(), link, element); +} + +TEMPLATE +template +bool CLASS::put(const Link& link, const Element& element) NOEXCEPT +{ + const auto ptr = body_.template get_raw1(link); + return put(ptr, element); +} + +// protected (unguarded memory access) +TEMPLATE +template +bool CLASS::put(memory::iterator it, const Element& element) NOEXCEPT +{ + static_assert(is_nonzero(Column), "column zero is the keyed spine"); + static_assert(Element::size == width); + if (is_null(it)) + return false; + + using namespace system; + const auto bytes = width * element.count(); + iostream stream{ it, possible_narrow_and_sign_cast(bytes) }; + flipper sink{ stream }; + + BC_DEBUG_ONLY(sink.set_limit(width * element.count());) + return element.to_data(sink); +} + +// protected +// ---------------------------------------------------------------------------- + +// static +TEMPLATE +Link CLASS::first(const memory& ptr, const Link& link, const Key& key) NOEXCEPT +{ + using namespace system; + if (!ptr) + return {}; + + auto next = link; + while (!next.is_terminal()) + { + // get element offset (fault) + const auto offset = ptr.offset(body::link_to_position(next)); + if (is_null(offset)) + return {}; + + // element key matches (found) + if (keys::compare(unsafe_array_cast( + std::next(offset, Link::size)), key)) + return next; + + // set next element link (loop) + next = unsafe_array_cast(offset); + } + + return next; +} + +// static +TEMPLATE +ELEMENT_CONSTRAINT +bool CLASS::read(const memory& ptr, const Link& link, Element& element) NOEXCEPT +{ + using namespace system; + if (!ptr || link.is_terminal()) + return false; + + const auto start = body::link_to_position(link); + if (is_limited(start)) + return false; + + const auto size = ptr.size(); + const auto position = possible_narrow_and_sign_cast(start); + if (position >= size) + return false; + + const auto offset = ptr.offset(start); + if (is_null(offset)) + return false; + + // Stream starts at record and the index is skipped for reader convenience. + iostream stream{ offset, size - position }; + reader source{ stream }; + source.skip_bytes(index_size); + + if constexpr (!is_slab) { BC_DEBUG_ONLY(source.set_limit(RowSize * element.count());) } + return element.from_data(source); +} + +TEMPLATE +ELEMENT_CONSTRAINT +bool CLASS::write(const memory& ptr, const Link& link, const Key& key, + const Element& element) NOEXCEPT +{ + Link unused{}; + return write(unused, ptr, link, key, element); +} + +TEMPLATE +ELEMENT_CONSTRAINT +bool CLASS::write(Link& previous, const memory& ptr, const Link& link, + const Key& key, const Element& element) NOEXCEPT +{ + using namespace system; + if (!ptr || link.is_terminal()) + return false; + + const auto start = body::link_to_position(link); + if (is_limited(start)) + return false; + + const auto size = ptr.size(); + const auto position = possible_narrow_and_sign_cast(start); + if (position >= size) + return false; + + const auto offset = ptr.offset(start); + if (is_null(offset)) + return false; + + // iostream.flush is a nop (direct copy). + iostream stream{ offset, size - position }; + finalizer sink{ stream }; + sink.skip_bytes(Link::size); + keys::write(sink, key); + + // Commit element to body. + if constexpr (!is_slab) { BC_DEBUG_ONLY(sink.set_limit(RowSize * element.count());) } + auto& next = unsafe_array_cast(offset); + if (!element.to_data(sink)) + return false; + + // Commit element to search (terminal is a valid bucket index). + bool search{}; + if (!head_.push(search, link, next, key)) + return false; + + // If collision set previous stack head for conflict resolution search. + previous = search ? Link{ next } : Link{}; + return true; +} + +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/database/primitives/hashmaps.hpp b/include/bitcoin/database/primitives/hashmaps.hpp new file mode 100644 index 000000000..f7e508d06 --- /dev/null +++ b/include/bitcoin/database/primitives/hashmaps.hpp @@ -0,0 +1,282 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_DATABASE_PRIMITIVES_HASHMAPS_HPP +#define LIBBITCOIN_DATABASE_PRIMITIVES_HASHMAPS_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace database { + +/// SoA aggregate hash table: one keyed spine with satellite record columns. +/// Column zero is the keyed spine (link/key/value rows searchable from the +/// hash head). Trailing Widths are satellite records sharing the spine's row +/// count, with allocation unified across all columns (one lock). One storage +/// backs all columns, so any column accessor guards the body against remap. +/// Caution: iterator/reader/finalizer hold body remap lock until disposed. +/// These handles should be used for serialization and immediately disposed. +template +class hashmaps +{ +public: + DELETE_COPY_MOVE_DESTRUCT(hashmaps); + + using key = Key; + using link = Link; + using iterator = database::iterator; + + template + static constexpr size_t width = std::get + ( + std::array{ RowSize, Widths... } + ); + + hashmaps(storage& header, storage& body, const Link& buckets) NOEXCEPT; + + /// Setup, not thread safe. + /// ----------------------------------------------------------------------- + + bool create() NOEXCEPT; + bool close() NOEXCEPT; + bool backup(bool=false) NOEXCEPT; + bool restore() NOEXCEPT; + bool verify() const NOEXCEPT; + + /// Sizing. + /// ----------------------------------------------------------------------- + + /// The instance is enabled (more than 1 bucket). + bool enabled() const NOEXCEPT; + + /// Hash table bucket count. + size_t buckets() const NOEXCEPT; + + /// Head file bytes. + size_t head_size() const NOEXCEPT; + + /// Aggregate body bytes across all columns. + size_t body_size() const NOEXCEPT; + + /// Aggregate body byte capacity across all columns. + size_t capacity() const NOEXCEPT; + + /// Count of body records (common across columns). + Link count() const NOEXCEPT; + + /// Errors. + /// ----------------------------------------------------------------------- + + /// Get the fault condition. + code get_fault() const NOEXCEPT; + + /// Get the space required to clear the disk full condition. + size_t get_space() const NOEXCEPT; + + /// Resume from disk full condition. + code reload() NOEXCEPT; + + /// Query interface, iterator is not thread safe. + /// ----------------------------------------------------------------------- + + /// Return the link at the top of the conflict list (for table scanning). + inline Link top(const Link& list) const NOEXCEPT; + + /// True if an instance of object with key exists. + inline bool exists(const memory& ptr, const Key& key) const NOEXCEPT; + inline bool exists(const Key& key) const NOEXCEPT; + + /// Return first element link or terminal if not found/error. + inline Link first(const memory& ptr, const Key& key) const NOEXCEPT; + inline Link first(const Key& key) const NOEXCEPT; + + /// Iterator holds shared lock on storage remap. + inline iterator it(Key&& key) const NOEXCEPT; + inline iterator it(const Key& key) const NOEXCEPT; + + /// Unified allocation across all columns (one lock). + /// Allocate count records at returned link (follow with set|put). + inline Link allocate(const Link& size) NOEXCEPT; + + /// Column base ptr for batch processing (holds shared lock on body remap). + template + inline memory get_memory() const NOEXCEPT; + + /// Return the associated search key (terminal link returns default). + Key get_key(const Link& link) NOEXCEPT; + + /// Spine (column zero). + /// ----------------------------------------------------------------------- + + /// Get first element matching the search key, false if not found/error. + template = true> + inline bool find(const Key& key, Element& element) const NOEXCEPT; + + /// Get first element matching the search key, and return its link. + template = true> + inline Link find_link(const Key& key, Element& element) const NOEXCEPT; + + /// Get element at link, false if deserialize error. + template = true> + inline bool get(const Link& link, Element& element) const NOEXCEPT; + + /// Get element at link using get_memory() ptr, false if deserialize error. + template = true> + static inline bool get(const memory& ptr, const Link& link, + Element& element) NOEXCEPT; + + /// Get element at link, false if deserialize error. + /// Iterator must not be terminal, must be guarded by caller. + template = true> + static inline bool get(const iterator& it, Element& element) NOEXCEPT; + + /// Get element at link using it memory object, false if deserialize error. + template = true> + static inline bool get(const iterator& it, const Link& link, + Element& element) NOEXCEPT; + + /// Set element into previously allocated link (follow with commit). + template = true> + static bool set(const memory& ptr, const Link& link, const Key& key, + const Element& element) NOEXCEPT; + + /// Set element into previously allocated link (follow with commit). + template = true> + bool set(const Link& link, const Key& key, const Element& element) NOEXCEPT; + + /// Allocate and set element, and return link (follow with commit). + template = true> + inline Link set_link(const Key& key, const Element& element) NOEXCEPT; + template = true> + inline bool set_link(Link& link, const Key& key, + const Element& element) NOEXCEPT; + + /// Allocate, set, commit element to key, and return link. + template = true> + inline Link put_link(const Key& key, const Element& element) NOEXCEPT; + template = true> + inline bool put_link(Link& link, const Key& key, + const Element& element) NOEXCEPT; + + /// Allocate, set, commit element to key. + template = true> + inline bool put(const Key& key, const Element& element) NOEXCEPT; + + /// Set and commit previously allocated element at link to key. + template = true> + inline bool put(const Link& link, const Key& key, + const Element& element) NOEXCEPT; + + /// Set/commit allocated element at link to key, using get_memory() ptr. + template = true> + inline bool put(const memory& ptr, const Link& link, + const Key& key, const Element& element) NOEXCEPT; + + /// Set/commit allocated element at link to key, using get_memory() ptr. + template = true> + inline bool put(bool& duplicate, const memory& ptr, const Link& link, + const Key& key, const Element& element) NOEXCEPT; + + /// Commit previously set element at link to key. + inline Link commit_link(const Link& link, const Key& key) NOEXCEPT; + inline bool commit(const Link& link, const Key& key) NOEXCEPT; + bool commit(const memory& ptr, const Link& link, const Key& key) NOEXCEPT; + + /// Satellites (nonzero Column). + /// ----------------------------------------------------------------------- + + /// Get element from column at link using column base ptr (deserialize). + template + static bool get(const memory& ptr, const Link& link, + Element& element) NOEXCEPT; + + /// Get element from column at link. + template + bool get(const Link& link, Element& element) const NOEXCEPT; + + /// Put previously allocated element to column at link (guard required). + template + bool put(const Link& link, const Element& element) NOEXCEPT; + +protected: + /// memory parameter must be from start (i.e. from get_memory()). + /// Get first element matching key, from top link and whole table memory. + static Link first(const memory& ptr, const Link& link, + const Key& key) NOEXCEPT; + + /// memory parameter must be from start (i.e. from get_memory()). + /// Get element at link using memory object, false if deserialize error. + template = true> + static bool read(const memory& ptr, const Link& link, + Element& element) NOEXCEPT; + + /// memory parameter must be from start (i.e. from get_memory()). + /// Set and commit previously allocated element at link to key. + template = true> + bool write(const memory& ptr, const Link& link, const Key& key, + const Element& element) NOEXCEPT; + template = true> + bool write(Link& previous, const memory& ptr, const Link& link, + const Key& key, const Element& element) NOEXCEPT; + + /// Put element to column at raw iterator (unguarded memory access). + template + bool put(memory::iterator it, const Element& element) NOEXCEPT; + +private: + static constexpr auto is_slab = (RowSize == max_size_t); + static constexpr auto key_size = keys::size(); + static constexpr auto index_size = Link::size + key_size; + using head = database::hashhead; + using body = database::bodys; + + // Thread safe (index/top/push). + // Not thread safe (create/open/close/backup/restore). + head head_; + + // Thread safe. + body body_; +}; + +/// Spine is the keyed spine schema; Columns are satellite column tables. +template +using hash_maps = hashmaps; + +} // namespace database +} // namespace libbitcoin + +#define TEMPLATE template +#define CLASS hashmaps +#define ELEMENT_CONSTRAINT template > + +#include + +#undef CLASS +#undef TEMPLATE + +#endif diff --git a/test/primitives/hashmaps.cpp b/test/primitives/hashmaps.cpp new file mode 100644 index 000000000..ac492c8c6 --- /dev/null +++ b/test/primitives/hashmaps.cpp @@ -0,0 +1,295 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" +#include "../mocks/chunk_storage.hpp" + +BOOST_AUTO_TEST_SUITE(hashmaps_tests) + +using namespace system; +using link5 = linkage<5>; +using key1 = data_array<1>; + +constexpr auto buckets = 16_size; +constexpr auto head_size = add1(buckets) * link5::size; + +// Column zero is the keyed spine, column one an 8 byte satellite record. +constexpr auto spine_size = sizeof(uint32_t); +constexpr auto satellite_size = sizeof(uint64_t); +constexpr auto spine_row = link5::size + array_count + spine_size; + +using table = hashmaps; +using body_storages = test::chunk_storages; +static const body_storages::paths body_paths{ "spine", "satellite" }; + +static_assert(table::width<0> == spine_size); +static_assert(table::width<1> == satellite_size); + +class little_record +{ +public: + // record bytes or zero for slab (for template). + static constexpr size_t size = spine_size; + + // record count or bytes count for slab (for allocate). + static constexpr link5 count() NOEXCEPT { return 1; } + + bool from_data(database::reader& source) NOEXCEPT + { + value = source.read_little_endian(); + return source; + } + + bool to_data(database::finalizer& sink) const NOEXCEPT + { + sink.write_little_endian(value); + return sink; + } + + uint32_t value{ 0 }; +}; + +class little_satellite +{ +public: + static constexpr size_t size = satellite_size; + static constexpr link5 count() NOEXCEPT { return 1; } + + bool from_data(database::reader& source) NOEXCEPT + { + value = source.read_little_endian(); + return source; + } + + bool to_data(database::flipper& sink) const NOEXCEPT + { + sink.write_little_endian(value); + return sink; + } + + uint64_t value{ 0 }; +}; + +// construct/create +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(hashmaps__construct__empty__expected) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + const table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(body_store.buffer().empty()); + BOOST_REQUIRE_EQUAL(instance.count(), 0u); + BOOST_REQUIRE(!instance.get_fault()); +} + +BOOST_AUTO_TEST_CASE(hashmaps__create__empty__expected) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + BOOST_REQUIRE(instance.verify()); + BOOST_REQUIRE(instance.enabled()); + BOOST_REQUIRE_EQUAL(instance.buckets(), buckets); + BOOST_REQUIRE_EQUAL(instance.head_size(), head_size); + BOOST_REQUIRE_EQUAL(instance.count(), 0u); + BOOST_REQUIRE(!instance.get_fault()); +} + +// allocate +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(hashmaps__allocate__multiple__shared_row_count) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + + // One allocation expands all columns to the shared row count. + BOOST_REQUIRE_EQUAL(instance.allocate(2), 0u); + BOOST_REQUIRE_EQUAL(instance.count(), 2u); + BOOST_REQUIRE_EQUAL(instance.body_size(), 2u * (spine_row + satellite_size)); + BOOST_REQUIRE_EQUAL(instance.allocate(1), 2u); + BOOST_REQUIRE_EQUAL(instance.count(), 3u); + BOOST_REQUIRE(!instance.get_fault()); +} + +// spine +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(hashmaps__spine_put__multiple__expected) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + + constexpr key1 key_first{ 0x41 }; + constexpr key1 key_second{ 0x42 }; + + link5 link{}; + BOOST_REQUIRE(instance.put_link(link, key_first, little_record{ 0x04030201_u32 })); + BOOST_REQUIRE(!link.is_terminal()); + BOOST_REQUIRE_EQUAL(link, 0u); + + link = instance.put_link(key_second, little_record{ 0x08070605_u32 }); + BOOST_REQUIRE(!link.is_terminal()); + BOOST_REQUIRE_EQUAL(link, 1u); + + BOOST_REQUIRE(instance.exists(key_first)); + BOOST_REQUIRE(instance.exists(key_second)); + BOOST_REQUIRE_EQUAL(instance.first(key_first), 0u); + BOOST_REQUIRE_EQUAL(instance.first(key_second), 1u); + BOOST_REQUIRE_EQUAL(instance.get_key(0), key_first); + BOOST_REQUIRE_EQUAL(instance.get_key(1), key_second); + + little_record record{}; + BOOST_REQUIRE(instance.get(0, record)); + BOOST_REQUIRE_EQUAL(record.value, 0x04030201_u32); + BOOST_REQUIRE(instance.find(key_second, record)); + BOOST_REQUIRE_EQUAL(record.value, 0x08070605_u32); + + // This expectation relies on the fact of no hash table conflict between 0x41 and 0x42. + const data_chunk expected_spine + { + 0xff, 0xff, 0xff, 0xff, 0xff, + 0x41, + 0x01, 0x02, 0x03, 0x04, + + 0xff, 0xff, 0xff, 0xff, 0xff, + 0x42, + 0x05, 0x06, 0x07, 0x08 + }; + BOOST_REQUIRE_EQUAL(body_store.buffers_.at(0), expected_spine); + + // Spine put allocates the shared row, backfilling satellite columns. + const data_chunk expected_satellite(2u * satellite_size, 0x00); + BOOST_REQUIRE_EQUAL(body_store.buffers_.at(1), expected_satellite); + BOOST_REQUIRE(!instance.get_fault()); +} + +BOOST_AUTO_TEST_CASE(hashmaps__spine_put__duplicate_key__detected) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + + constexpr key1 key_twin{ 0x41 }; + + // Allocate before ptr acquisition (allocation must not follow accessor). + BOOST_REQUIRE_EQUAL(instance.allocate(2), 0u); + + const auto ptr = instance.get_memory(); + bool duplicate{ true }; + BOOST_REQUIRE(instance.put(duplicate, ptr, 0, key_twin, little_record{ 0x04030201_u32 })); + BOOST_REQUIRE(!duplicate); + BOOST_REQUIRE(instance.put(duplicate, ptr, 1, key_twin, little_record{ 0x08070605_u32 })); + BOOST_REQUIRE(duplicate); + BOOST_REQUIRE(!instance.get_fault()); +} + +BOOST_AUTO_TEST_CASE(hashmaps__spine_it__duplicate_key__iterated) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + + constexpr key1 key_twin{ 0x41 }; + BOOST_REQUIRE(instance.put(key_twin, little_record{ 0x04030201_u32 })); + BOOST_REQUIRE(instance.put(key_twin, little_record{ 0x08070605_u32 })); + + auto it = instance.it(key_twin); + BOOST_REQUIRE(it); + BOOST_REQUIRE_EQUAL(*it, 1u); + BOOST_REQUIRE(it.advance()); + BOOST_REQUIRE_EQUAL(*it, 0u); + BOOST_REQUIRE(!it.advance()); + it.reset(); + BOOST_REQUIRE(!instance.get_fault()); +} + +// satellites +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(hashmaps__satellite_get__terminal__false) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + + little_satellite satellite{}; + BOOST_REQUIRE(!instance.get<1>(link5::terminal, satellite)); + BOOST_REQUIRE(!instance.get_fault()); +} + +BOOST_AUTO_TEST_CASE(hashmaps__satellite_put__spine_allocated__expected) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + + // Spine put allocates the shared row across all columns. + constexpr key1 key_first{ 0x41 }; + link5 link{}; + BOOST_REQUIRE(instance.put_link(link, key_first, little_record{ 0x04030201_u32 })); + BOOST_REQUIRE_EQUAL(link, 0u); + + // Satellite put writes into the spine-allocated row (guard required). + { + const auto guard = instance.get_memory<1>(); + BOOST_REQUIRE(instance.put<1>(link, little_satellite{ 0x1122334455667788_u64 })); + } + + little_satellite satellite{}; + BOOST_REQUIRE(instance.get<1>(link, satellite)); + BOOST_REQUIRE_EQUAL(satellite.value, 0x1122334455667788_u64); + + const data_chunk expected_satellite + { + 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 + }; + BOOST_REQUIRE_EQUAL(body_store.buffers_.at(1), expected_satellite); + BOOST_REQUIRE(!instance.get_fault()); +} + +// close/restore +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(hashmaps__close__populated__verifies) +{ + test::chunk_storage head_store{}; + body_storages body_store{ body_paths }; + table instance{ head_store, body_store, buckets }; + BOOST_REQUIRE(instance.create()); + + constexpr key1 key_first{ 0x41 }; + BOOST_REQUIRE(instance.put(key_first, little_record{ 0x04030201_u32 })); + BOOST_REQUIRE(instance.close()); + BOOST_REQUIRE(instance.verify()); + BOOST_REQUIRE(instance.restore()); + BOOST_REQUIRE_EQUAL(instance.count(), 1u); + BOOST_REQUIRE(!instance.get_fault()); +} + +BOOST_AUTO_TEST_SUITE_END() From 656fc7487fc93c243d31c2ace352c36d4d2102c8 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 19 Jul 2026 19:00:49 -0400 Subject: [PATCH 2/2] Regenerate artifacts. --- builds/gnu/Makefile.am | 3 +++ .../libbitcoin-database-test.vcxproj | 1 + .../libbitcoin-database-test.vcxproj.filters | 3 +++ .../vs2022/libbitcoin-database/libbitcoin-database.vcxproj | 2 ++ .../libbitcoin-database/libbitcoin-database.vcxproj.filters | 6 ++++++ .../libbitcoin-database-test.vcxproj | 1 + .../libbitcoin-database-test.vcxproj.filters | 3 +++ .../vs2026/libbitcoin-database/libbitcoin-database.vcxproj | 2 ++ .../libbitcoin-database/libbitcoin-database.vcxproj.filters | 6 ++++++ include/bitcoin/database.hpp | 1 + include/bitcoin/database/primitives/primitives.hpp | 1 + 11 files changed, 29 insertions(+) diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index ad966609e..026a50572 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -105,6 +105,7 @@ include_bitcoin_database_impl_primitives_HEADERS = \ ${srcdir}/../../include/bitcoin/database/impl/primitives/body.ipp \ ${srcdir}/../../include/bitcoin/database/impl/primitives/hashhead.ipp \ ${srcdir}/../../include/bitcoin/database/impl/primitives/hashmap.ipp \ + ${srcdir}/../../include/bitcoin/database/impl/primitives/hashmaps.ipp \ ${srcdir}/../../include/bitcoin/database/impl/primitives/iterator.ipp \ ${srcdir}/../../include/bitcoin/database/impl/primitives/keys.ipp \ ${srcdir}/../../include/bitcoin/database/impl/primitives/linkage.ipp \ @@ -242,6 +243,7 @@ include_bitcoin_database_primitives_HEADERS = \ ${srcdir}/../../include/bitcoin/database/primitives/column.hpp \ ${srcdir}/../../include/bitcoin/database/primitives/hashhead.hpp \ ${srcdir}/../../include/bitcoin/database/primitives/hashmap.hpp \ + ${srcdir}/../../include/bitcoin/database/primitives/hashmaps.hpp \ ${srcdir}/../../include/bitcoin/database/primitives/iterator.hpp \ ${srcdir}/../../include/bitcoin/database/primitives/keys.hpp \ ${srcdir}/../../include/bitcoin/database/primitives/linkage.hpp \ @@ -364,6 +366,7 @@ test_libbitcoin_database_test_SOURCES = \ ${srcdir}/../../test/primitives/body.cpp \ ${srcdir}/../../test/primitives/hashhead.cpp \ ${srcdir}/../../test/primitives/hashmap.cpp \ + ${srcdir}/../../test/primitives/hashmaps.cpp \ ${srcdir}/../../test/primitives/iterator.cpp \ ${srcdir}/../../test/primitives/keys.cpp \ ${srcdir}/../../test/primitives/linkage.cpp \ diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj index a15e09cba..eaf2976b4 100644 --- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj @@ -138,6 +138,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters index df6a33f30..5bfd4c327 100644 --- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters @@ -114,6 +114,9 @@ src\primitives + + src\primitives + src\primitives diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj index 31ff28add..1af418049 100644 --- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj @@ -167,6 +167,7 @@ + @@ -230,6 +231,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters index c5d038085..7f4bd2660 100644 --- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters @@ -218,6 +218,9 @@ include\bitcoin\database\primitives + + include\bitcoin\database\primitives + include\bitcoin\database\primitives @@ -403,6 +406,9 @@ include\bitcoin\database\impl\primitives + + include\bitcoin\database\impl\primitives + include\bitcoin\database\impl\primitives diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj index a01439e5f..fca7a62c5 100644 --- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj @@ -138,6 +138,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters index df6a33f30..5bfd4c327 100644 --- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters @@ -114,6 +114,9 @@ src\primitives + + src\primitives + src\primitives diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj index f3b32a16b..2158ade4c 100644 --- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj @@ -167,6 +167,7 @@ + @@ -230,6 +231,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters index c5d038085..7f4bd2660 100644 --- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters @@ -218,6 +218,9 @@ include\bitcoin\database\primitives + + include\bitcoin\database\primitives + include\bitcoin\database\primitives @@ -403,6 +406,9 @@ include\bitcoin\database\impl\primitives + + include\bitcoin\database\impl\primitives + include\bitcoin\database\impl\primitives diff --git a/include/bitcoin/database.hpp b/include/bitcoin/database.hpp index 36f8ae717..895881585 100644 --- a/include/bitcoin/database.hpp +++ b/include/bitcoin/database.hpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/database/primitives/primitives.hpp b/include/bitcoin/database/primitives/primitives.hpp index b0a61d2aa..0c548cf0e 100644 --- a/include/bitcoin/database/primitives/primitives.hpp +++ b/include/bitcoin/database/primitives/primitives.hpp @@ -36,6 +36,7 @@ // tables #include #include +#include #include #include