87#include <shared_mutex>
93#include <unordered_map>
97#define WIN32_LEAN_AND_MEAN
117using Bytes = std::span<const std::byte>;
133inline constexpr uint8_t MAGIC[8] = {
'F',
'L',
'U',
'X',
'E',
'N',
'0',
'1'};
134inline constexpr uint8_t FLAG_LIVE = 0x00;
135inline constexpr uint8_t FLAG_TOMB = 0x01;
136inline constexpr uint8_t MAX_KEY = 255;
137inline constexpr size_t HEADER_SIZE = 6;
147inline void encode_header(uint8_t out[HEADER_SIZE],
148 const EntryHeader &h)
noexcept {
151 out[2] =
static_cast<uint8_t
>(h.val_len & 0xFFu);
152 out[3] =
static_cast<uint8_t
>((h.val_len >> 8) & 0xFFu);
153 out[4] =
static_cast<uint8_t
>((h.val_len >> 16) & 0xFFu);
154 out[5] =
static_cast<uint8_t
>((h.val_len >> 24) & 0xFFu);
158inline auto decode_header(
const uint8_t in[HEADER_SIZE])
noexcept
163 .val_len =
static_cast<uint32_t
>(in[2]) |
164 static_cast<uint32_t
>(in[3]) << 8 |
165 static_cast<uint32_t
>(in[4]) << 16 |
166 static_cast<uint32_t
>(in[5]) << 24,
177 using is_transparent = void;
179 auto operator()(std::string_view sv)
const noexcept ->
size_t {
180 return std::hash<std::string_view>{}(sv);
182 auto operator()(
const std::string &s)
const noexcept ->
size_t {
183 return std::hash<std::string_view>{}(s);
188 std::unordered_map<std::string, IndexEntry, StringHash, std::equal_to<>>;
194 HANDLE file_ = INVALID_HANDLE_VALUE;
195 HANDLE map_ =
nullptr;
199 uint8_t *ptr_ =
nullptr;
201 size_t file_size_ = 0;
202 std::atomic<bool> dirty_{
false};
206 MappedFile() =
default;
207 ~MappedFile() { close(); }
209 MappedFile(
const MappedFile &) =
delete;
210 auto operator=(
const MappedFile &) -> MappedFile & =
delete;
212 auto open(std::string_view path) ->
bool {
215 file_ = CreateFileA(path_.c_str(), GENERIC_READ | GENERIC_WRITE,
216 FILE_SHARE_READ,
nullptr, OPEN_ALWAYS,
217 FILE_ATTRIBUTE_NORMAL,
nullptr);
218 if (file_ == INVALID_HANDLE_VALUE) {
221 SetFilePointer(file_, 0,
nullptr, FILE_END);
223 fd_ = ::open(path_.c_str(), O_RDWR | O_CREAT | O_APPEND, 0644);
234 if (file_ != INVALID_HANDLE_VALUE) {
236 file_ = INVALID_HANDLE_VALUE;
246 auto remap() ->
bool {
251 dirty_.store(
false, std::memory_order_release);
256 map_ = CreateFileMappingA(file_,
nullptr, PAGE_READWRITE, 0, 0,
nullptr);
259 dirty_.store(
false, std::memory_order_release);
263 ptr_ =
static_cast<uint8_t *
>(
264 MapViewOfFile(map_, FILE_MAP_ALL_ACCESS, 0, 0, size_));
269 dirty_.store(
false, std::memory_order_release);
273 ptr_ =
static_cast<uint8_t *
>(
274 ::mmap(
nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
276 if (ptr_ == MAP_FAILED) {
278 dirty_.store(
false, std::memory_order_release);
282 dirty_.store(
false, std::memory_order_release);
286 auto append(
const void *data,
size_t len) ->
bool {
292 if (!WriteFile(file_, data,
static_cast<DWORD
>(len), &written,
nullptr) ||
293 written !=
static_cast<DWORD
>(len)) {
297 if (::write(fd_, data, len) !=
static_cast<ssize_t
>(len)) {
301 dirty_.store(
true, std::memory_order_relaxed);
311 [[nodiscard]]
auto sync() ->
bool {
313 return FlushFileBuffers(file_) != 0;
315 return ::fsync(fd_) == 0;
329 [[nodiscard]]
auto truncate(
size_t new_size) ->
bool {
333 li.QuadPart =
static_cast<LONGLONG
>(new_size);
334 if (!SetFilePointerEx(file_, li,
nullptr, FILE_BEGIN)) {
337 if (!SetEndOfFile(file_)) {
338 SetFilePointer(file_, 0,
nullptr, FILE_END);
341 SetFilePointer(file_, 0,
nullptr, FILE_END);
343 if (::ftruncate(fd_,
static_cast<off_t
>(new_size)) != 0) {
347 file_size_ = new_size;
348 dirty_.store(
true, std::memory_order_release);
377 auto rewrite(
const std::vector<uint8_t> &data) ->
bool {
378 const std::string tmp_path = path_ +
".tmp";
381 HANDLE tmp = CreateFileA(tmp_path.c_str(), GENERIC_WRITE, 0,
nullptr,
382 CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
nullptr);
383 if (tmp == INVALID_HANDLE_VALUE) {
387 const bool write_ok =
388 WriteFile(tmp, data.data(),
static_cast<DWORD
>(data.size()), &written,
390 written ==
static_cast<DWORD
>(data.size()) &&
391 FlushFileBuffers(tmp) != 0;
396 DeleteFileA(tmp_path.c_str());
401 ::open(tmp_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644);
405 const bool write_ok = ::write(tmp_fd, data.data(), data.size()) ==
406 static_cast<ssize_t
>(data.size()) &&
407 ::fsync(tmp_fd) == 0;
412 ::unlink(tmp_path.c_str());
418 if (file_ != INVALID_HANDLE_VALUE) {
420 file_ = INVALID_HANDLE_VALUE;
430 if (!ReplaceFileA(path_.c_str(), tmp_path.c_str(),
nullptr,
431 REPLACEFILE_IGNORE_MERGE_ERRORS,
nullptr,
nullptr)) {
433 DeleteFileA(tmp_path.c_str());
435 file_ = CreateFileA(path_.c_str(), GENERIC_READ | GENERIC_WRITE,
436 FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
437 FILE_ATTRIBUTE_NORMAL,
nullptr);
439 if (file_ == INVALID_HANDLE_VALUE) {
440 throw std::runtime_error(
441 "fluxen: failed to reopen database file after replace");
444 SetFilePointer(file_, 0,
nullptr, FILE_END);
446 throw std::runtime_error(
"fluxen: remap failed after replace failure");
451 if (::rename(tmp_path.c_str(), path_.c_str()) != 0) {
452 ::unlink(tmp_path.c_str());
453 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
455 throw std::runtime_error(
456 "fluxen: failed to reopen database file after rename");
459 throw std::runtime_error(
"fluxen: remap failed after rename failure");
466 file_ = CreateFileA(path_.c_str(), GENERIC_READ | GENERIC_WRITE,
467 FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
468 FILE_ATTRIBUTE_NORMAL,
nullptr);
469 if (file_ == INVALID_HANDLE_VALUE) {
470 throw std::runtime_error(
471 "fluxen: failed to reopen database file after successful replace");
473 SetFilePointer(file_, 0,
nullptr, FILE_END);
475 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
477 throw std::runtime_error(
478 "fluxen: failed to reopen database file after successful rename");
482 throw std::runtime_error(
"fluxen: remap failed after successful rename");
492 [[nodiscard]]
auto ptr() const noexcept -> const uint8_t * {
return ptr_; }
499 [[nodiscard]]
auto is_dirty() const noexcept ->
bool {
500 return dirty_.load(std::memory_order_acquire);
503 [[nodiscard]]
auto size() const noexcept ->
size_t {
return file_size_; }
510 UnmapViewOfFile(ptr_);
516 ::munmap(ptr_, size_);
522 [[nodiscard]]
auto file_size() const noexcept ->
size_t {
525 GetFileSizeEx(file_, &sz);
526 return static_cast<size_t>(sz.QuadPart);
530 return static_cast<size_t>(st.st_size);
559 std::vector<uint8_t> val;
562 std::vector<Op> ops_;
572 void put(std::string_view key, std::string_view value) {
573 if (key.empty() || key.size() > detail::MAX_KEY) {
574 throw std::runtime_error(
"fluxen: key must be between 1 and 255 bytes");
576 ops_.push_back({.key = std::string(key),
577 .val = std::vector<uint8_t>(value.begin(), value.end()),
578 .is_delete =
false});
601 template <
typename T>
602 requires(std::is_trivially_copyable_v<T> &&
603 !std::is_convertible_v<T, std::string_view>)
604 void put(std::string_view key,
const T &value) {
605 if (key.empty() || key.size() > detail::MAX_KEY) {
606 throw std::runtime_error(
"fluxen: key must be between 1 and 255 bytes");
608 const auto *p =
reinterpret_cast<const uint8_t *
>(&value);
609 ops_.push_back({.key = std::string(key),
610 .val = std::vector<uint8_t>(p, p +
sizeof(T)),
611 .is_delete =
false});
624 if (key.empty() || key.size() > detail::MAX_KEY) {
625 throw std::runtime_error(
"fluxen: key must be between 1 and 255 bytes");
627 ops_.push_back({.key = std::string(key), .val = {}, .is_delete =
true});
667 detail::IndexMap index_;
668 mutable detail::MappedFile file_;
669 mutable std::shared_mutex mu_;
670 mutable std::mutex sync_mutex_;
671 bool poisoned_ =
false;
673 void check_poisoned()
const {
675 throw std::runtime_error(
676 "fluxen: database is poisoned due to an unrecoverable I/O error");
701 explicit DB(std::string_view path) {
702 if (!file_.open(path)) {
703 throw std::runtime_error(
"fluxen: failed to open '" + std::string(path) +
707 if (file_.size() == 0) {
723 DB(
const DB &) =
delete;
724 auto operator=(
const DB &) ->
DB & =
delete;
748 void put(std::string_view key, std::string_view value) {
750 std::unique_lock lock(mu_);
751 append_entry(key,
reinterpret_cast<const uint8_t *
>(value.data()),
752 static_cast<uint32_t
>(value.size()),
false);
783 template <
typename T>
784 requires(std::is_trivially_copyable_v<T> &&
785 !std::is_convertible_v<T, std::string_view>)
786 void put(std::string_view key,
const T &value) {
788 std::unique_lock lock(mu_);
789 append_entry(key,
reinterpret_cast<const uint8_t *
>(&value),
790 static_cast<uint32_t
>(
sizeof(T)),
false);
828 template <
typename T = std::
string>
829 auto get(std::string_view key)
const -> std::optional<T> {
831 std::shared_lock lock(mu_);
834 auto it = index_.find(key);
835 if (it == index_.end()) {
839 const auto &entry = it->second;
841 if constexpr (std::is_same_v<T, std::string>) {
842 const auto *ptr = file_.ptr() + entry.val_offset;
843 return std::string(
reinterpret_cast<const char *
>(ptr), entry.val_len);
845 static_assert(std::is_trivially_copyable_v<T>,
846 "fluxen: T must be trivially copyable");
847 if (entry.val_len !=
static_cast<uint32_t
>(
sizeof(T))) {
851 std::memcpy(&result, file_.ptr() + entry.val_offset,
sizeof(T));
878 std::unique_lock lock(mu_);
879 append_entry(key,
nullptr, 0,
true);
894 [[nodiscard]]
auto has(std::string_view key)
const ->
bool {
896 std::shared_lock lock(mu_);
898 return index_.contains(key);
929 void each(
const std::function<
void(std::string_view,
Bytes)> &fn)
const {
931 std::shared_lock lock(mu_);
933 for (
const auto &[key, entry] : index_) {
935 reinterpret_cast<const std::byte *
>(file_.ptr() + entry.val_offset);
936 fn(key,
Bytes{ptr, entry.val_len});
970 const std::function<
void(std::string_view,
Bytes)> &fn)
const {
972 std::shared_lock lock(mu_);
974 for (
const auto &[key, entry] : index_) {
975 if (key.starts_with(pfx)) {
977 reinterpret_cast<const std::byte *
>(file_.ptr() + entry.val_offset);
978 fn(key,
Bytes{ptr, entry.val_len});
1038 if (result == rollback) {
1042 std::vector<uint8_t> batch;
1043 batch.reserve(tx.ops_.size() * 64);
1044 for (
const auto &op : tx.ops_) {
1045 detail::EntryHeader hdr{
1046 .flags = op.is_delete ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1047 .key_len =
static_cast<uint8_t
>(op.key.size()),
1048 .val_len = op.is_delete ? 0u :
static_cast<uint32_t
>(op.val.size()),
1050 uint8_t raw[detail::HEADER_SIZE];
1051 detail::encode_header(raw, hdr);
1052 batch.insert(batch.end(), raw, raw + detail::HEADER_SIZE);
1053 batch.insert(batch.end(), op.key.begin(), op.key.end());
1054 if (!op.is_delete) {
1055 batch.insert(batch.end(), op.val.begin(), op.val.end());
1059 std::unique_lock lock(mu_);
1061 const size_t size_before = file_.size();
1063 if (!file_.append(batch.data(), batch.size())) {
1064 throw std::runtime_error(
"fluxen: transaction append failed");
1067 if (!file_.sync()) {
1068 if (!file_.truncate(size_before)) {
1070 throw std::runtime_error(
1071 "fluxen: transaction fsync failed and truncation failed. Database "
1072 "file may contain a partial tail entry");
1074 throw std::runtime_error(
"fluxen: transaction fsync failed");
1078 for (
const auto &op : tx.ops_) {
1079 pos += detail::HEADER_SIZE + op.key.size();
1081 index_.erase(op.key);
1083 index_[op.key] = {.val_offset = size_before + pos,
1084 .val_len =
static_cast<uint32_t
>(op.val.size())};
1085 pos += op.val.size();
1129 std::unique_lock lock(mu_);
1131 if (file_.is_dirty() && !file_.remap()) {
1132 throw std::runtime_error(
"fluxen: remap failed before compaction");
1135 std::vector<uint8_t> buf;
1136 buf.reserve(file_.size());
1138 buf.insert(buf.end(), detail::MAGIC, detail::MAGIC +
sizeof(detail::MAGIC));
1140 detail::IndexMap new_index;
1141 for (
const auto &[key, entry] : index_) {
1142 detail::EntryHeader hdr{
1143 .flags = detail::FLAG_LIVE,
1144 .key_len =
static_cast<uint8_t
>(key.size()),
1145 .val_len = entry.val_len,
1148 uint8_t raw[detail::HEADER_SIZE];
1149 detail::encode_header(raw, hdr);
1151 size_t val_off = buf.size() + detail::HEADER_SIZE + key.size();
1153 buf.insert(buf.end(), raw, raw + detail::HEADER_SIZE);
1154 buf.insert(buf.end(), key.begin(), key.end());
1156 const auto *val_ptr = file_.ptr() + entry.val_offset;
1157 buf.insert(buf.end(), val_ptr, val_ptr + entry.val_len);
1159 new_index[key] = {.val_offset = val_off, .val_len = entry.val_len};
1162 if (!file_.rewrite(buf)) {
1166 index_ = std::move(new_index);
1182 std::shared_lock lock(mu_);
1183 return index_.size();
1200 std::shared_lock lock(mu_);
1201 return file_.size();
1207 if (!file_.append(detail::MAGIC,
sizeof(detail::MAGIC))) {
1208 throw std::runtime_error(
"fluxen: failed to write magic header");
1221 if (file_.size() <
sizeof(detail::MAGIC)) {
1222 throw std::runtime_error(
"fluxen: file too small to be valid");
1225 if (std::memcmp(file_.ptr(), detail::MAGIC,
sizeof(detail::MAGIC)) != 0) {
1226 throw std::runtime_error(
1227 "fluxen: bad magic. File was not created by fluxen");
1230 size_t pos =
sizeof(detail::MAGIC);
1231 size_t last_good_pos = pos;
1233 while (pos + detail::HEADER_SIZE <= file_.size()) {
1234 uint8_t raw[detail::HEADER_SIZE];
1235 std::memcpy(raw, file_.ptr() + pos, detail::HEADER_SIZE);
1236 detail::EntryHeader hdr = detail::decode_header(raw);
1237 pos += detail::HEADER_SIZE;
1239 if (pos + hdr.key_len + hdr.val_len > file_.size()) {
1243 std::string key(
reinterpret_cast<const char *
>(file_.ptr() + pos),
1247 if (hdr.flags == detail::FLAG_TOMB) {
1250 index_[key] = {.val_offset = pos, .val_len = hdr.val_len};
1254 last_good_pos = pos;
1257 if (last_good_pos < file_.size()) {
1258 if (!file_.truncate(last_good_pos)) {
1259 throw std::runtime_error(
1260 "fluxen: failed to truncate partial tail entry on open");
1274 void append_entry(std::string_view key,
const uint8_t *val, uint32_t val_len,
1276 if (key.empty() || key.size() > detail::MAX_KEY) {
1277 throw std::runtime_error(
"fluxen: key must be between 1 and 255 bytes");
1280 detail::EntryHeader hdr{
1281 .flags = tombstone ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1282 .key_len =
static_cast<uint8_t
>(key.size()),
1286 std::vector<uint8_t> buf;
1287 buf.resize(detail::HEADER_SIZE + key.size() + val_len);
1288 detail::encode_header(buf.data(), hdr);
1289 std::memcpy(buf.data() + detail::HEADER_SIZE, key.data(), key.size());
1290 if (val && val_len) {
1291 std::memcpy(buf.data() + detail::HEADER_SIZE + key.size(), val, val_len);
1294 const size_t size_before = file_.size();
1296 if (!file_.append(buf.data(), buf.size())) {
1297 if (!file_.truncate(size_before)) {
1299 throw std::runtime_error(
1300 "fluxen: append failed and truncation failed. Database file may "
1301 "contain a partial tail entry");
1303 throw std::runtime_error(
"fluxen: append failed");
1307 if (
auto it = index_.find(key); it != index_.end()) {
1311 index_[std::string(key)] = {.val_offset = file_.size() - val_len,
1312 .val_len = val_len};
1334 void ensure_mapped()
const {
1335 if (!file_.is_dirty()) {
1339 std::unique_lock sync_lock(sync_mutex_);
1340 if (file_.is_dirty() && !file_.remap()) {
1341 throw std::runtime_error(
"fluxen: remap failed");
A persistent key-value database backed by a single file.
Definition fluxen.hpp:665
auto has(std::string_view key) const -> bool
Returns true if the given key exists in the database.
Definition fluxen.hpp:894
auto file_size() const -> size_t
Returns the current size of the database file in bytes.
Definition fluxen.hpp:1198
auto key_count() const -> size_t
Returns the number of live keys currently stored.
Definition fluxen.hpp:1180
void prefix(std::string_view pfx, const std::function< void(std::string_view, Bytes)> &fn) const
Iterates over all keys that begin with the given prefix.
Definition fluxen.hpp:969
void transaction(const std::function< TxResult(Tx &)> &fn)
Executes a batch of operations atomically.
Definition fluxen.hpp:1034
void remove(std::string_view key)
Deletes the value stored under the given key.
Definition fluxen.hpp:876
~DB()=default
Closes the database and releases all file locks.
void put(std::string_view key, const T &value)
Stores a trivially copyable value under the given key.
Definition fluxen.hpp:786
auto compact() -> bool
Rewrites the database file retaining only live entries.
Definition fluxen.hpp:1127
DB(std::string_view path)
Opens or creates a database at the given path.
Definition fluxen.hpp:701
auto get(std::string_view key) const -> std::optional< T >
Retrieves the value stored under the given key.
Definition fluxen.hpp:829
void each(const std::function< void(std::string_view, Bytes)> &fn) const
Iterates over all live key-value pairs.
Definition fluxen.hpp:929
void put(std::string_view key, std::string_view value)
Stores a string value under the given key.
Definition fluxen.hpp:748
A staged batch of write operations, used inside DB::transaction().
Definition fluxen.hpp:553
void put(std::string_view key, std::string_view value)
Stage a string value to be written.
Definition fluxen.hpp:572
void remove(std::string_view key)
Stage a key deletion.
Definition fluxen.hpp:623
void put(std::string_view key, const T &value)
Stage a trivially copyable value to be written.
Definition fluxen.hpp:604
TxResult
Controls whether a transaction's operations are applied or discarded.
Definition fluxen.hpp:126
std::span< const std::byte > Bytes
A non-owning view of raw bytes in the memory-mapped file.
Definition fluxen.hpp:117