diff --git a/include/bitcoin/database/impl/primitives/nomaps.ipp b/include/bitcoin/database/impl/primitives/nomaps.ipp
index 121216c20..a17693b5f 100644
--- a/include/bitcoin/database/impl/primitives/nomaps.ipp
+++ b/include/bitcoin/database/impl/primitives/nomaps.ipp
@@ -1,217 +1,223 @@
-/**
- * Copyright (c) 2011-2026 libbitcoin developers
- *
- * 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_NOMAPS_IPP
-#define LIBBITCOIN_DATABASE_PRIMITIVES_NOMAPS_IPP
-
-#include
-#include
-
-namespace libbitcoin {
-namespace database {
-
-TEMPLATE
-CLASS::nomaps(storage& header, storage& body) NOEXCEPT
- : head_(header, 0),
- 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
-size_t CLASS::body_size() const NOEXCEPT
-{
- return body_.size();
-}
-
-TEMPLATE
-Link CLASS::count() const NOEXCEPT
-{
- return body_.count();
-}
-
-TEMPLATE
-Link CLASS::allocate(const Link& count) NOEXCEPT
-{
- return body_.allocate(count);
-}
-
-TEMPLATE
-bool CLASS::truncate(const Link& count) NOEXCEPT
-{
- return body_.truncate(count);
-}
-
-TEMPLATE
-bool CLASS::drop() NOEXCEPT
-{
- return body_.truncate(0) && backup();
-}
-
-// Faults.
-// ----------------------------------------------------------------------------
-
-TEMPLATE
-code CLASS::get_fault() const NOEXCEPT
-{
- const auto ec = head_.get_fault();
- return ec ? ec : body_.get_fault();
-}
-
-TEMPLATE
-size_t CLASS::get_space() const NOEXCEPT
-{
- return system::ceilinged_add(head_.get_space(), body_.get_space());
-}
-
-TEMPLATE
-code CLASS::reload() NOEXCEPT
-{
- return body_.reload();
-}
-
-// query interface
-// ----------------------------------------------------------------------------
-
-TEMPLATE
-memory CLASS::guard() const NOEXCEPT
-{
- return get_memory();
-}
-
-TEMPLATE
-template
-memory CLASS::get_memory() const NOEXCEPT
-{
- return body_.template get();
-}
-
-// static
-TEMPLATE
-template
-bool CLASS::get(const memory& ptr, const Link& link, Element& element) NOEXCEPT
-{
- 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_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);
-}
-
-// TODO: gets are not optimized for shared remap guard.
-TEMPLATE
-template
-bool CLASS::get(const Link& link, Element& element) const NOEXCEPT
-{
- return get(get_memory(), link, element);
-}
-
-// TODO: puts are optimized for shared remap guard (required).
-TEMPLATE
-template
-bool CLASS::put(const Link& link, const Element& element) NOEXCEPT
-{
- const auto ptr = body_.template get_raw(link);
- if (!put(ptr, element))
- return false;
-
- body_.complete(link, element.count());
- return true;
-}
-
-// protected (unguarded memory access)
-TEMPLATE
-template
-bool CLASS::put(memory::iterator it, const Element& element) NOEXCEPT
-{
- 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_sign_cast(bytes) };
- flipper sink{ stream };
-
- BC_DEBUG_ONLY(sink.set_limit(width * element.count());)
- return element.to_data(sink);
-}
-
-} // namespace database
-} // namespace libbitcoin
-
-#endif
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers
+ *
+ * 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_NOMAPS_IPP
+#define LIBBITCOIN_DATABASE_PRIMITIVES_NOMAPS_IPP
+
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::nomaps(storage& header, storage& body) NOEXCEPT
+ : head_(header, 0),
+ 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
+size_t CLASS::head_size() const NOEXCEPT
+{
+ return head_.size();
+}
+
+TEMPLATE
+size_t CLASS::body_size() const NOEXCEPT
+{
+ return body_.size();
+}
+
+TEMPLATE
+Link CLASS::count() const NOEXCEPT
+{
+ return body_.count();
+}
+
+TEMPLATE
+Link CLASS::allocate(const Link& count) NOEXCEPT
+{
+ return body_.allocate(count);
+}
+
+TEMPLATE
+bool CLASS::truncate(const Link& count) NOEXCEPT
+{
+ return body_.truncate(count);
+}
+
+TEMPLATE
+bool CLASS::drop() NOEXCEPT
+{
+ return body_.truncate(0) && backup();
+}
+
+// Faults.
+// ----------------------------------------------------------------------------
+
+TEMPLATE
+code CLASS::get_fault() const NOEXCEPT
+{
+ const auto ec = head_.get_fault();
+ return ec ? ec : body_.get_fault();
+}
+
+TEMPLATE
+size_t CLASS::get_space() const NOEXCEPT
+{
+ return system::ceilinged_add(head_.get_space(), body_.get_space());
+}
+
+TEMPLATE
+code CLASS::reload() NOEXCEPT
+{
+ return body_.reload();
+}
+
+// query interface
+// ----------------------------------------------------------------------------
+
+TEMPLATE
+memory CLASS::guard() const NOEXCEPT
+{
+ return get_memory();
+}
+
+TEMPLATE
+template
+memory CLASS::get_memory() const NOEXCEPT
+{
+ return body_.template get();
+}
+
+// static
+TEMPLATE
+template
+bool CLASS::get(const memory& ptr, const Link& link, Element& element) NOEXCEPT
+{
+ 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_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);
+}
+
+// TODO: gets are not optimized for shared remap guard.
+TEMPLATE
+template
+bool CLASS::get(const Link& link, Element& element) const NOEXCEPT
+{
+ return get(get_memory(), link, element);
+}
+
+// TODO: puts are optimized for shared remap guard (required).
+TEMPLATE
+template
+bool CLASS::put(const Link& link, const Element& element) NOEXCEPT
+{
+ const auto ptr = body_.template get_raw(link);
+ if (!put(ptr, element))
+ return false;
+
+ body_.complete(link, element.count());
+ return true;
+}
+
+// protected (unguarded memory access)
+TEMPLATE
+template
+bool CLASS::put(memory::iterator it, const Element& element) NOEXCEPT
+{
+ 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_sign_cast(bytes) };
+ flipper sink{ stream };
+
+ BC_DEBUG_ONLY(sink.set_limit(width * element.count());)
+ return element.to_data(sink);
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/store/store_backup.ipp b/include/bitcoin/database/impl/store/store_backup.ipp
index 061087734..fb9bca671 100644
--- a/include/bitcoin/database/impl/store/store_backup.ipp
+++ b/include/bitcoin/database/impl/store/store_backup.ipp
@@ -102,11 +102,13 @@ code CLASS::backup(const event_handler& handler, bool prune) NOEXCEPT
// Rename /temporary to /primary (atomic).
if ((ec = file::rename_ex(temporary, primary))) return ec;
- // Delete the rotated /secondary, superseded by the new /primary.
+ // Delete the rotated /secondary, superseded by the new /primary. Best
+ // effort, as the snapshot is committed (a remnant is deleted by the next
+ // backup or restore, and is unreachable while /primary exists).
if (file::is_directory(secondary))
{
- if ((ec = file::clear_directory_ex(secondary))) return ec;
- ec = file::remove_ex(secondary);
+ /* bool */ file::clear_directory(secondary);
+ /* bool */ file::remove(secondary);
}
return ec;
diff --git a/include/bitcoin/database/impl/store/store_restore.ipp b/include/bitcoin/database/impl/store/store_restore.ipp
index bb26f9513..a09daf495 100644
--- a/include/bitcoin/database/impl/store/store_restore.ipp
+++ b/include/bitcoin/database/impl/store/store_restore.ipp
@@ -59,20 +59,21 @@ code CLASS::restore(const event_handler& handler) NOEXCEPT
if (file::is_directory(primary))
{
- // Clear invalid /heads, recover from /primary, clone to /primary.
+ // Clear invalid /heads and clone /primary (retained) to /heads.
+ // The snapshot is never vacated, so no fault leaves /heads as the
+ // only copy (which the recreation below would otherwise clear).
ec = file::clear_directory_ex(heads);
if (!ec) ec = file::remove_ex(heads);
- if (!ec) ec = file::rename_ex(primary, heads);
- if (!ec) ec = file::copy_directory_ex(heads, primary);
+ if (!ec) ec = file::copy_directory_ex(primary, heads);
if (!ec) ec = file::discharge_directory_ex(primary);
}
else if (file::is_directory(secondary))
{
- // Clear invalid /heads, recover from /secondary, clone to /primary.
+ // Clone /secondary to /heads and promote it to /primary (atomic).
ec = file::clear_directory_ex(heads);
if (!ec) ec = file::remove_ex(heads);
- if (!ec) ec = file::rename_ex(secondary, heads);
- if (!ec) ec = file::copy_directory_ex(heads, primary);
+ if (!ec) ec = file::copy_directory_ex(secondary, heads);
+ if (!ec) ec = file::rename_ex(secondary, primary);
if (!ec) ec = file::discharge_directory_ex(primary);
}
else
diff --git a/include/bitcoin/database/primitives/nomaps.hpp b/include/bitcoin/database/primitives/nomaps.hpp
index 4af9cfd56..2d081f865 100644
--- a/include/bitcoin/database/primitives/nomaps.hpp
+++ b/include/bitcoin/database/primitives/nomaps.hpp
@@ -56,6 +56,7 @@ class nomaps
/// Sizing.
/// -----------------------------------------------------------------------
+ size_t head_size() const NOEXCEPT;
size_t body_size() const NOEXCEPT;
Link count() const NOEXCEPT;
Link allocate(const Link& count) NOEXCEPT;
diff --git a/test/store/store_restore.cpp b/test/store/store_restore.cpp
index 5d198fd9b..f42ea500d 100644
--- a/test/store/store_restore.cpp
+++ b/test/store/store_restore.cpp
@@ -231,4 +231,26 @@ BOOST_AUTO_TEST_CASE(store__restore__secondary_remnant__deleted)
BOOST_REQUIRE(!instance.close(test::events));
}
+BOOST_AUTO_TEST_CASE(store__restore__secondary_only__promoted_success)
+{
+ settings configuration{};
+ configuration.path = TEST_DIRECTORY;
+ const auto primary = configuration.path / schema::dir::primary;
+ const auto secondary = configuration.path / schema::dir::secondary;
+
+ test::map_store instance{ configuration };
+ BOOST_REQUIRE(!instance.create(test::events));
+ BOOST_REQUIRE(!instance.snapshot(test::events));
+ BOOST_REQUIRE(!instance.close(test::events));
+
+ // Simulate crash between backup rotation and /primary promotion.
+ BOOST_REQUIRE(file::rename(primary, secondary));
+ BOOST_REQUIRE(test::create(test::flush_lock_file(configuration.path)));
+ BOOST_REQUIRE(!instance.restore(test::events));
+ BOOST_REQUIRE(test::folder(primary));
+ BOOST_REQUIRE(!test::folder(secondary));
+ BOOST_REQUIRE(test::folder(configuration.path / schema::dir::heads));
+ BOOST_REQUIRE(!instance.close(test::events));
+}
+
BOOST_AUTO_TEST_SUITE_END()