fluxen 1.1.1
Single-header embedded key-value store for C++20
Loading...
Searching...
No Matches
fluxen.hpp
Go to the documentation of this file.
1
76
77#pragma once
78
79#include <atomic>
80#include <cassert>
81#include <cstddef>
82#include <cstdint>
83#include <cstring>
84#include <functional>
85#include <mutex>
86#include <optional>
87#include <shared_mutex>
88#include <span>
89#include <stdexcept>
90#include <string>
91#include <string_view>
92#include <type_traits>
93#include <unordered_map>
94#include <vector>
95
96#ifdef _WIN32
97#define WIN32_LEAN_AND_MEAN
98#include <windows.h>
99#else
100#include <fcntl.h>
101#include <sys/mman.h>
102#include <sys/stat.h>
103#include <unistd.h>
104#endif
105
106namespace fluxen {
107
108// --- public types ---
109
117using Bytes = std::span<const std::byte>;
118
126enum TxResult : uint8_t { commit, rollback };
127
128// --- internal ---
129
131namespace detail {
132
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; // on-disk entry header size
138
139/* On-disk entry header (6 bytes on disk) */
140struct EntryHeader {
141 uint8_t flags;
142 uint8_t key_len;
143 uint32_t val_len;
144};
145
146/* Serialize header into 6-byte buffer */
147inline void encode_header(uint8_t out[HEADER_SIZE],
148 const EntryHeader &h) noexcept {
149 out[0] = h.flags;
150 out[1] = h.key_len;
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);
155}
156
157/* Deserialise header from a 6-byte buffer */
158inline auto decode_header(const uint8_t in[HEADER_SIZE]) noexcept
159 -> EntryHeader {
160 return {
161 .flags = in[0],
162 .key_len = in[1],
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,
167 };
168}
169
170/* In-memory index entry (points into the mmap'd file) */
171struct IndexEntry {
172 size_t val_offset; // byte offset of value data in file
173 uint32_t val_len;
174};
175
176struct StringHash {
177 using is_transparent = void;
178
179 auto operator()(std::string_view sv) const noexcept -> size_t {
180 return std::hash<std::string_view>{}(sv);
181 }
182 auto operator()(const std::string &s) const noexcept -> size_t {
183 return std::hash<std::string_view>{}(s);
184 }
185};
186
187using IndexMap =
188 std::unordered_map<std::string, IndexEntry, StringHash, std::equal_to<>>;
189
190/* Cross-platform mmap wrapper */
191class MappedFile {
192private:
193#ifdef _WIN32
194 HANDLE file_ = INVALID_HANDLE_VALUE;
195 HANDLE map_ = nullptr;
196#else
197 int fd_ = -1;
198#endif
199 uint8_t *ptr_ = nullptr;
200 size_t size_ = 0;
201 size_t file_size_ = 0;
202 std::atomic<bool> dirty_{false};
203 std::string path_;
204
205public:
206 MappedFile() = default;
207 ~MappedFile() { close(); }
208
209 MappedFile(const MappedFile &) = delete;
210 auto operator=(const MappedFile &) -> MappedFile & = delete;
211
212 auto open(std::string_view path) -> bool {
213 path_ = path;
214#ifdef _WIN32
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) {
219 return false;
220 }
221 SetFilePointer(file_, 0, nullptr, FILE_END);
222#else
223 fd_ = ::open(path_.c_str(), O_RDWR | O_CREAT | O_APPEND, 0644);
224 if (fd_ < 0) {
225 return false;
226 }
227#endif
228 return remap();
229 }
230
231 void close() {
232 unmap();
233#ifdef _WIN32
234 if (file_ != INVALID_HANDLE_VALUE) {
235 CloseHandle(file_);
236 file_ = INVALID_HANDLE_VALUE;
237 }
238#else
239 if (fd_ >= 0) {
240 ::close(fd_);
241 fd_ = -1;
242 }
243#endif
244 }
245
246 auto remap() -> bool {
247 unmap();
248 size_ = file_size();
249 file_size_ = size_;
250 if (size_ == 0) {
251 dirty_.store(false, std::memory_order_release);
252 return true;
253 }
254
255#ifdef _WIN32
256 map_ = CreateFileMappingA(file_, nullptr, PAGE_READWRITE, 0, 0, nullptr);
257
258 if (!map_) {
259 dirty_.store(false, std::memory_order_release);
260 return false;
261 }
262
263 ptr_ = static_cast<uint8_t *>(
264 MapViewOfFile(map_, FILE_MAP_ALL_ACCESS, 0, 0, size_));
265
266 if (!ptr_) {
267 CloseHandle(map_);
268 map_ = nullptr;
269 dirty_.store(false, std::memory_order_release);
270 return false;
271 }
272#else
273 ptr_ = static_cast<uint8_t *>(
274 ::mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
275
276 if (ptr_ == MAP_FAILED) {
277 ptr_ = nullptr;
278 dirty_.store(false, std::memory_order_release);
279 return false;
280 }
281#endif
282 dirty_.store(false, std::memory_order_release);
283 return true;
284 }
285
286 auto append(const void *data, size_t len) -> bool {
287 if (len == 0) {
288 return true;
289 }
290#ifdef _WIN32
291 DWORD written = 0;
292 if (!WriteFile(file_, data, static_cast<DWORD>(len), &written, nullptr) ||
293 written != static_cast<DWORD>(len)) {
294 return false;
295 }
296#else
297 if (::write(fd_, data, len) != static_cast<ssize_t>(len)) {
298 return false;
299 }
300#endif
301 dirty_.store(true, std::memory_order_relaxed);
302 file_size_ += len;
303 return true;
304 }
305
311 [[nodiscard]] auto sync() -> bool {
312#ifdef _WIN32
313 return FlushFileBuffers(file_) != 0;
314#else
315 return ::fsync(fd_) == 0;
316#endif
317 }
318
329 [[nodiscard]] auto truncate(size_t new_size) -> bool {
330#ifdef _WIN32
331 unmap();
332 LARGE_INTEGER li{};
333 li.QuadPart = static_cast<LONGLONG>(new_size);
334 if (!SetFilePointerEx(file_, li, nullptr, FILE_BEGIN)) {
335 return false;
336 }
337 if (!SetEndOfFile(file_)) {
338 SetFilePointer(file_, 0, nullptr, FILE_END);
339 return false;
340 }
341 SetFilePointer(file_, 0, nullptr, FILE_END);
342#else
343 if (::ftruncate(fd_, static_cast<off_t>(new_size)) != 0) {
344 return false;
345 }
346#endif
347 file_size_ = new_size;
348 dirty_.store(true, std::memory_order_release);
349 return true;
350 }
351
377 auto rewrite(const std::vector<uint8_t> &data) -> bool {
378 const std::string tmp_path = path_ + ".tmp";
379
380#ifdef _WIN32
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) {
384 return false;
385 }
386 DWORD written = 0;
387 const bool write_ok =
388 WriteFile(tmp, data.data(), static_cast<DWORD>(data.size()), &written,
389 nullptr) &&
390 written == static_cast<DWORD>(data.size()) &&
391 FlushFileBuffers(tmp) != 0;
392
393 CloseHandle(tmp);
394
395 if (!write_ok) {
396 DeleteFileA(tmp_path.c_str());
397 return false;
398 }
399#else
400 const int tmp_fd =
401 ::open(tmp_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644);
402 if (tmp_fd < 0) {
403 return false;
404 }
405 const bool write_ok = ::write(tmp_fd, data.data(), data.size()) ==
406 static_cast<ssize_t>(data.size()) &&
407 ::fsync(tmp_fd) == 0;
408
409 ::close(tmp_fd);
410
411 if (!write_ok) {
412 ::unlink(tmp_path.c_str());
413 return false;
414 }
415#endif
416 unmap();
417#ifdef _WIN32
418 if (file_ != INVALID_HANDLE_VALUE) {
419 CloseHandle(file_);
420 file_ = INVALID_HANDLE_VALUE;
421 }
422#else
423 if (fd_ >= 0) {
424 ::close(fd_);
425 fd_ = -1;
426 }
427#endif
428
429#ifdef _WIN32
430 if (!ReplaceFileA(path_.c_str(), tmp_path.c_str(), nullptr,
431 REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr)) {
432
433 DeleteFileA(tmp_path.c_str());
434
435 file_ = CreateFileA(path_.c_str(), GENERIC_READ | GENERIC_WRITE,
436 FILE_SHARE_READ, nullptr, OPEN_EXISTING,
437 FILE_ATTRIBUTE_NORMAL, nullptr);
438
439 if (file_ == INVALID_HANDLE_VALUE) {
440 throw std::runtime_error(
441 "fluxen: failed to reopen database file after replace");
442 }
443
444 SetFilePointer(file_, 0, nullptr, FILE_END);
445 if (!remap()) {
446 throw std::runtime_error("fluxen: remap failed after replace failure");
447 }
448 return false;
449 }
450#else
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);
454 if (fd_ < 0) {
455 throw std::runtime_error(
456 "fluxen: failed to reopen database file after rename");
457 }
458 if (!remap()) {
459 throw std::runtime_error("fluxen: remap failed after rename failure");
460 }
461 return false;
462 }
463#endif
464
465#ifdef _WIN32
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");
472 }
473 SetFilePointer(file_, 0, nullptr, FILE_END);
474#else
475 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
476 if (fd_ < 0) {
477 throw std::runtime_error(
478 "fluxen: failed to reopen database file after successful rename");
479 }
480#endif
481 if (!remap()) {
482 throw std::runtime_error("fluxen: remap failed after successful rename");
483 }
484 return true;
485 }
486
492 [[nodiscard]] auto ptr() const noexcept -> const uint8_t * { return ptr_; }
493
499 [[nodiscard]] auto is_dirty() const noexcept -> bool {
500 return dirty_.load(std::memory_order_acquire);
501 }
502
503 [[nodiscard]] auto size() const noexcept -> size_t { return file_size_; }
504
505private:
506 void unmap() {
507 if (!ptr_)
508 return;
509#ifdef _WIN32
510 UnmapViewOfFile(ptr_);
511 if (map_) {
512 CloseHandle(map_);
513 map_ = nullptr;
514 }
515#else
516 ::munmap(ptr_, size_);
517#endif
518 ptr_ = nullptr;
519 size_ = 0;
520 }
521
522 [[nodiscard]] auto file_size() const noexcept -> size_t {
523#ifdef _WIN32
524 LARGE_INTEGER sz{};
525 GetFileSizeEx(file_, &sz);
526 return static_cast<size_t>(sz.QuadPart);
527#else
528 struct stat st{};
529 ::fstat(fd_, &st);
530 return static_cast<size_t>(st.st_size);
531#endif
532 }
533};
534} // namespace detail
536
537// --- transaction ---
538
539class DB; // forward declaration
540
553class Tx {
554private:
555 friend class DB;
556
557 struct Op {
558 std::string key;
559 std::vector<uint8_t> val;
560 bool is_delete;
561 };
562 std::vector<Op> ops_;
563
564public:
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");
575 }
576 ops_.push_back({.key = std::string(key),
577 .val = std::vector<uint8_t>(value.begin(), value.end()),
578 .is_delete = false});
579 }
580
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");
607 }
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});
612 }
613
623 void remove(std::string_view key) {
624 if (key.empty() || key.size() > detail::MAX_KEY) {
625 throw std::runtime_error("fluxen: key must be between 1 and 255 bytes");
626 }
627 ops_.push_back({.key = std::string(key), .val = {}, .is_delete = true});
628 }
629};
630
631// --- DB ---
632
665class DB {
666private:
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;
672
673 void check_poisoned() const {
674 if (poisoned_) {
675 throw std::runtime_error(
676 "fluxen: database is poisoned due to an unrecoverable I/O error");
677 }
678 }
679
680public:
701 explicit DB(std::string_view path) {
702 if (!file_.open(path)) {
703 throw std::runtime_error("fluxen: failed to open '" + std::string(path) +
704 "'");
705 }
706
707 if (file_.size() == 0) {
708 init_file();
709 } else {
710 load_index();
711 }
712 }
713
721 ~DB() = default;
723 DB(const DB &) = delete;
724 auto operator=(const DB &) -> DB & = delete;
726
727 // --- WRITE ---
728
748 void put(std::string_view key, std::string_view value) {
749 check_poisoned();
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);
753 }
754
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) {
787 check_poisoned();
788 std::unique_lock lock(mu_);
789 append_entry(key, reinterpret_cast<const uint8_t *>(&value),
790 static_cast<uint32_t>(sizeof(T)), false);
791 }
792
793 // --- READ ---
794
828 template <typename T = std::string>
829 auto get(std::string_view key) const -> std::optional<T> {
830 check_poisoned();
831 std::shared_lock lock(mu_);
832 ensure_mapped();
833
834 auto it = index_.find(key);
835 if (it == index_.end()) {
836 return std::nullopt;
837 }
838
839 const auto &entry = it->second;
840
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);
844 } else {
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))) {
848 return std::nullopt;
849 }
850 T result;
851 std::memcpy(&result, file_.ptr() + entry.val_offset, sizeof(T));
852 return result;
853 }
854 }
855
856 // --- DELETE ---
857
876 void remove(std::string_view key) {
877 check_poisoned();
878 std::unique_lock lock(mu_);
879 append_entry(key, nullptr, 0, true);
880 }
881
882 // --- QUERY ---
883
894 [[nodiscard]] auto has(std::string_view key) const -> bool {
895 check_poisoned();
896 std::shared_lock lock(mu_);
897 ensure_mapped();
898 return index_.contains(key);
899 }
900
929 void each(const std::function<void(std::string_view, Bytes)> &fn) const {
930 check_poisoned();
931 std::shared_lock lock(mu_);
932 ensure_mapped();
933 for (const auto &[key, entry] : index_) {
934 auto *ptr =
935 reinterpret_cast<const std::byte *>(file_.ptr() + entry.val_offset);
936 fn(key, Bytes{ptr, entry.val_len});
937 }
938 }
939
969 void prefix(std::string_view pfx,
970 const std::function<void(std::string_view, Bytes)> &fn) const {
971 check_poisoned();
972 std::shared_lock lock(mu_);
973 ensure_mapped();
974 for (const auto &[key, entry] : index_) {
975 if (key.starts_with(pfx)) {
976 auto *ptr =
977 reinterpret_cast<const std::byte *>(file_.ptr() + entry.val_offset);
978 fn(key, Bytes{ptr, entry.val_len});
979 }
980 }
981 }
982
983 // --- TRANSACTION ---
984
1034 void transaction(const std::function<TxResult(Tx &)> &fn) {
1035 check_poisoned();
1036 Tx tx;
1037 TxResult result = fn(tx);
1038 if (result == rollback) {
1039 return;
1040 }
1041
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()),
1049 };
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());
1056 }
1057 }
1058
1059 std::unique_lock lock(mu_);
1060
1061 const size_t size_before = file_.size();
1062
1063 if (!file_.append(batch.data(), batch.size())) {
1064 throw std::runtime_error("fluxen: transaction append failed");
1065 }
1066
1067 if (!file_.sync()) {
1068 if (!file_.truncate(size_before)) {
1069 poisoned_ = true;
1070 throw std::runtime_error(
1071 "fluxen: transaction fsync failed and truncation failed. Database "
1072 "file may contain a partial tail entry");
1073 }
1074 throw std::runtime_error("fluxen: transaction fsync failed");
1075 }
1076
1077 size_t pos = 0;
1078 for (const auto &op : tx.ops_) {
1079 pos += detail::HEADER_SIZE + op.key.size();
1080 if (op.is_delete) {
1081 index_.erase(op.key);
1082 } else {
1083 index_[op.key] = {.val_offset = size_before + pos,
1084 .val_len = static_cast<uint32_t>(op.val.size())};
1085 pos += op.val.size();
1086 }
1087 }
1088 }
1089
1090 // --- MAINTENANCE ---
1091
1127 [[nodiscard]] auto compact() -> bool {
1128 check_poisoned();
1129 std::unique_lock lock(mu_);
1130
1131 if (file_.is_dirty() && !file_.remap()) {
1132 throw std::runtime_error("fluxen: remap failed before compaction");
1133 }
1134
1135 std::vector<uint8_t> buf;
1136 buf.reserve(file_.size());
1137
1138 buf.insert(buf.end(), detail::MAGIC, detail::MAGIC + sizeof(detail::MAGIC));
1139
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,
1146 };
1147
1148 uint8_t raw[detail::HEADER_SIZE];
1149 detail::encode_header(raw, hdr);
1150
1151 size_t val_off = buf.size() + detail::HEADER_SIZE + key.size();
1152
1153 buf.insert(buf.end(), raw, raw + detail::HEADER_SIZE);
1154 buf.insert(buf.end(), key.begin(), key.end());
1155
1156 const auto *val_ptr = file_.ptr() + entry.val_offset;
1157 buf.insert(buf.end(), val_ptr, val_ptr + entry.val_len);
1158
1159 new_index[key] = {.val_offset = val_off, .val_len = entry.val_len};
1160 }
1161
1162 if (!file_.rewrite(buf)) {
1163 return false;
1164 }
1165
1166 index_ = std::move(new_index);
1167 return true;
1168 }
1169
1170 // --- DIAGNOSTICS ---
1171
1180 [[nodiscard]] auto key_count() const -> size_t {
1181 check_poisoned();
1182 std::shared_lock lock(mu_);
1183 return index_.size();
1184 }
1185
1198 [[nodiscard]] auto file_size() const -> size_t {
1199 check_poisoned();
1200 std::shared_lock lock(mu_);
1201 return file_.size();
1202 }
1203
1204private:
1206 void init_file() {
1207 if (!file_.append(detail::MAGIC, sizeof(detail::MAGIC))) {
1208 throw std::runtime_error("fluxen: failed to write magic header");
1209 }
1210 }
1211
1220 void load_index() {
1221 if (file_.size() < sizeof(detail::MAGIC)) {
1222 throw std::runtime_error("fluxen: file too small to be valid");
1223 }
1224
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");
1228 }
1229
1230 size_t pos = sizeof(detail::MAGIC);
1231 size_t last_good_pos = pos;
1232
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;
1238
1239 if (pos + hdr.key_len + hdr.val_len > file_.size()) {
1240 break;
1241 }
1242
1243 std::string key(reinterpret_cast<const char *>(file_.ptr() + pos),
1244 hdr.key_len);
1245 pos += hdr.key_len;
1246
1247 if (hdr.flags == detail::FLAG_TOMB) {
1248 index_.erase(key);
1249 } else {
1250 index_[key] = {.val_offset = pos, .val_len = hdr.val_len};
1251 }
1252
1253 pos += hdr.val_len;
1254 last_good_pos = pos;
1255 }
1256
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");
1261 }
1262 }
1263 }
1264
1274 void append_entry(std::string_view key, const uint8_t *val, uint32_t val_len,
1275 bool tombstone) {
1276 if (key.empty() || key.size() > detail::MAX_KEY) {
1277 throw std::runtime_error("fluxen: key must be between 1 and 255 bytes");
1278 }
1279
1280 detail::EntryHeader hdr{
1281 .flags = tombstone ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1282 .key_len = static_cast<uint8_t>(key.size()),
1283 .val_len = val_len,
1284 };
1285
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);
1292 }
1293
1294 const size_t size_before = file_.size();
1295
1296 if (!file_.append(buf.data(), buf.size())) {
1297 if (!file_.truncate(size_before)) {
1298 poisoned_ = true;
1299 throw std::runtime_error(
1300 "fluxen: append failed and truncation failed. Database file may "
1301 "contain a partial tail entry");
1302 }
1303 throw std::runtime_error("fluxen: append failed");
1304 }
1305
1306 if (tombstone) {
1307 if (auto it = index_.find(key); it != index_.end()) {
1308 index_.erase(it);
1309 }
1310 } else {
1311 index_[std::string(key)] = {.val_offset = file_.size() - val_len,
1312 .val_len = val_len};
1313 }
1314 }
1315
1334 void ensure_mapped() const {
1335 if (!file_.is_dirty()) {
1336 return;
1337 }
1338
1339 std::unique_lock sync_lock(sync_mutex_);
1340 if (file_.is_dirty() && !file_.remap()) {
1341 throw std::runtime_error("fluxen: remap failed");
1342 }
1343 }
1344};
1345
1346} // namespace fluxen
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