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/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/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
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()