fluxen 1.1.2
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 <filesystem>
85#include <functional>
86#include <mutex>
87#include <optional>
88#include <shared_mutex>
89#include <span>
90#include <stdexcept>
91#include <string>
92#include <string_view>
93#include <type_traits>
94#include <unordered_map>
95#include <vector>
96
97#ifdef _WIN32
98#define WIN32_LEAN_AND_MEAN
99#include <windows.h>
100#else
101#include <fcntl.h>
102#include <sys/mman.h>
103#include <sys/stat.h>
104#include <unistd.h>
105#endif
106
107namespace fs = std::filesystem;
108
109namespace fluxen {
110
111// --- public types ---
112
120using Bytes = std::span<const std::byte>;
121
129enum TxResult : uint8_t { commit, rollback };
130
134struct io_error : public std::runtime_error {
135 using std::runtime_error::runtime_error;
136};
137
138struct corrupt_error : public std::runtime_error {
139 using std::runtime_error::runtime_error;
140};
141
142struct poisoned_error : public io_error {
143 using io_error::io_error;
144};
145
146struct key_error : public std::runtime_error {
147 using std::runtime_error::runtime_error;
148};
149
150// --- internal ---
151
153namespace detail {
154
155inline constexpr uint8_t MAGIC[8] = {'F', 'L', 'U', 'X', 'E', 'N', '0', '1'};
156inline constexpr uint8_t FLAG_LIVE = 0x00;
157inline constexpr uint8_t FLAG_TOMB = 0x01;
158inline constexpr uint8_t MAX_KEY = 255;
159inline constexpr size_t HEADER_SIZE = 6; // on-disk entry header size
160
161/* On-disk entry header (6 bytes on disk) */
162struct EntryHeader {
163 uint8_t flags;
164 uint8_t key_len;
165 uint32_t val_len;
166};
167
168/* Serialize header into 6-byte buffer */
169inline void encode_header(uint8_t out[HEADER_SIZE],
170 const EntryHeader &h) noexcept {
171 out[0] = h.flags;
172 out[1] = h.key_len;
173 out[2] = static_cast<uint8_t>(h.val_len & 0xFFu);
174 out[3] = static_cast<uint8_t>((h.val_len >> 8) & 0xFFu);
175 out[4] = static_cast<uint8_t>((h.val_len >> 16) & 0xFFu);
176 out[5] = static_cast<uint8_t>((h.val_len >> 24) & 0xFFu);
177}
178
179/* Deserialise header from a 6-byte buffer */
180inline auto decode_header(const uint8_t in[HEADER_SIZE]) noexcept
181-> EntryHeader {
182 return {
183 .flags = in[0],
184 .key_len = in[1],
185 .val_len = static_cast<uint32_t>(in[2]) |
186 static_cast<uint32_t>(in[3]) << 8 |
187 static_cast<uint32_t>(in[4]) << 16 |
188 static_cast<uint32_t>(in[5]) << 24,
189 };
190}
191
192/* In-memory index entry (points into the mmap'd file) */
193struct IndexEntry {
194 size_t val_offset; // byte offset of value data in file
195 uint32_t val_len;
196};
197
198struct StringHash {
199 using is_transparent = void;
200
201 auto operator()(std::string_view sv) const noexcept -> size_t {
202 return std::hash<std::string_view>{}(sv);
203 }
204 auto operator()(const std::string &s) const noexcept -> size_t {
205 return std::hash<std::string_view>{}(s);
206 }
207};
208
209#ifdef _WIN32
210auto utf8_to_wstring(const std::string &utf8) -> std::wstring {
211 int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
212 if (len <= 1) {
213 return {};
214 }
215 std::wstring wide(len - 1, L'\0');
216 MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wide.data(), len);
217 return wide;
218}
219#endif
220
221using IndexMap =
222std::unordered_map<std::string, IndexEntry, StringHash, std::equal_to<>>;
223
224/* Cross-platform mmap wrapper */
225class MappedFile {
226private:
227#ifdef _WIN32
228 HANDLE file_ = INVALID_HANDLE_VALUE;
229 HANDLE map_ = nullptr;
230#else
231 int fd_ = -1;
232#endif
233 uint8_t *ptr_ = nullptr;
234 size_t size_ = 0;
235 size_t file_size_ = 0;
236 std::atomic<bool> dirty_{false};
237 fs::path path_;
238
239public:
240 MappedFile() = default;
241 ~MappedFile() { close(); }
242
243 MappedFile(const MappedFile &) = delete;
244 auto operator=(const MappedFile &) -> MappedFile & = delete;
245 MappedFile(const MappedFile &&) = delete;
246 auto operator=(MappedFile &&) -> MappedFile & = delete;
247
248 auto open(const std::string &path) -> bool {
249#ifdef _WIN32
250 path_ = utf8_to_wstring(path);
251 file_ = CreateFileW(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
252 OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
253 if (file_ == INVALID_HANDLE_VALUE) {
254 return false;
255 }
256 SetFilePointer(file_, 0, nullptr, FILE_END);
257#else
258 path_ = path;
259 fd_ = ::open(path_.c_str(), O_RDWR | O_CREAT | O_APPEND, 0644);
260 if (fd_ < 0) {
261 return false;
262 }
263#endif
264 return remap();
265 }
266
267 void close() {
268 unmap();
269#ifdef _WIN32
270 if (file_ != INVALID_HANDLE_VALUE) {
271 CloseHandle(file_);
272 file_ = INVALID_HANDLE_VALUE;
273 }
274#else
275 if (fd_ >= 0) {
276 ::close(fd_);
277 fd_ = -1;
278 }
279#endif
280 }
281
282 auto remap() -> bool {
283 unmap();
284 size_ = file_size();
285 file_size_ = size_;
286 if (size_ == 0) {
287 dirty_.store(false, std::memory_order_release);
288 return true;
289 }
290
291#ifdef _WIN32
292 map_ = CreateFileMappingW(file_, nullptr, PAGE_READWRITE, 0, 0, nullptr);
293
294 if (!map_) {
295 dirty_.store(false, std::memory_order_release);
296 return false;
297 }
298
299 ptr_ = static_cast<uint8_t *>(
300 MapViewOfFile(map_, FILE_MAP_ALL_ACCESS, 0, 0, size_));
301
302 if (!ptr_) {
303 CloseHandle(map_);
304 map_ = nullptr;
305 dirty_.store(false, std::memory_order_release);
306 return false;
307 }
308#else
309 ptr_ = static_cast<uint8_t *>(
310 ::mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
311
312 if (ptr_ == MAP_FAILED) {
313 ptr_ = nullptr;
314 dirty_.store(false, std::memory_order_release);
315 return false;
316 }
317#endif
318 dirty_.store(false, std::memory_order_release);
319 return true;
320 }
321
322 auto append(const void *data, size_t len) -> bool {
323 if (len == 0) {
324 return true;
325 }
326#ifdef _WIN32
327 DWORD written = 0;
328 if (!WriteFile(file_, data, static_cast<DWORD>(len), &written, nullptr) ||
329 written != static_cast<DWORD>(len)) {
330 return false;
331 }
332#else
333 if (::write(fd_, data, len) != static_cast<ssize_t>(len)) {
334 return false;
335 }
336#endif
337 dirty_.store(true, std::memory_order_relaxed);
338 file_size_ += len;
339 return true;
340 }
341
347 [[nodiscard]] auto sync() -> bool {
348#ifdef _WIN32
349 return FlushFileBuffers(file_) != 0;
350#else
351 return ::fsync(fd_) == 0;
352#endif
353 }
354
365 [[nodiscard]] auto truncate(size_t new_size) -> bool {
366#ifdef _WIN32
367 unmap();
368 LARGE_INTEGER li{};
369 li.QuadPart = static_cast<LONGLONG>(new_size);
370 if (!SetFilePointerEx(file_, li, nullptr, FILE_BEGIN)) {
371 return false;
372 }
373 if (!SetEndOfFile(file_)) {
374 SetFilePointer(file_, 0, nullptr, FILE_END);
375 return false;
376 }
377 SetFilePointer(file_, 0, nullptr, FILE_END);
378#else
379 if (::ftruncate(fd_, static_cast<off_t>(new_size)) != 0) {
380 return false;
381 }
382#endif
383 file_size_ = new_size;
384 dirty_.store(true, std::memory_order_release);
385 return true;
386 }
387
413 auto rewrite(const std::vector<uint8_t> &data) -> bool {
414 fs::path tmp_path = path_;
415 tmp_path += ".tmp";
416
417#ifdef _WIN32
418 HANDLE tmp = CreateFileW(tmp_path.c_str(), GENERIC_WRITE, 0, nullptr,
419 CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
420 if (tmp == INVALID_HANDLE_VALUE) {
421 return false;
422 }
423 DWORD written = 0;
424 const bool write_ok =
425 WriteFile(tmp, data.data(), static_cast<DWORD>(data.size()), &written,
426 nullptr) &&
427 written == static_cast<DWORD>(data.size()) &&
428 FlushFileBuffers(tmp) != 0;
429
430 CloseHandle(tmp);
431
432 if (!write_ok) {
433 DeleteFileW(tmp_path.c_str());
434 return false;
435 }
436#else
437 const int tmp_fd =
438 ::open(tmp_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644);
439 if (tmp_fd < 0) {
440 return false;
441 }
442 const bool write_ok = ::write(tmp_fd, data.data(), data.size()) ==
443 static_cast<ssize_t>(data.size()) &&
444 ::fsync(tmp_fd) == 0;
445
446 ::close(tmp_fd);
447
448 if (!write_ok) {
449 ::unlink(tmp_path.c_str());
450 return false;
451 }
452#endif
453 unmap();
454#ifdef _WIN32
455 if (file_ != INVALID_HANDLE_VALUE) {
456 CloseHandle(file_);
457 file_ = INVALID_HANDLE_VALUE;
458 }
459#else
460 if (fd_ >= 0) {
461 ::close(fd_);
462 fd_ = -1;
463 }
464#endif
465
466#ifdef _WIN32
467 if (!ReplaceFileW(path_.c_str(), tmp_path.c_str(), nullptr,
468 REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr)) {
469
470 DeleteFileW(tmp_path.c_str());
471
472 file_ =
473 CreateFileW(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
474 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
475
476 if (file_ == INVALID_HANDLE_VALUE) {
477 throw io_error("fluxen: failed to reopen database file after replace");
478 }
479
480 SetFilePointer(file_, 0, nullptr, FILE_END);
481 if (!remap()) {
482 throw io_error("fluxen: remap failed after replace failure");
483 }
484 return false;
485 }
486#else
487 if (::rename(tmp_path.c_str(), path_.c_str()) != 0) {
488 ::unlink(tmp_path.c_str());
489 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
490 if (fd_ < 0) {
491 throw io_error("fluxen: failed to reopen database file after rename");
492 }
493 if (!remap()) {
494 throw io_error("fluxen: remap failed after rename failure");
495 }
496 return false;
497 }
498#endif
499
500#ifdef _WIN32
501 file_ = CreateFileW(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
502 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
503 if (file_ == INVALID_HANDLE_VALUE) {
504 throw io_error(
505 "fluxen: failed to reopen database file after successful replace");
506 }
507 SetFilePointer(file_, 0, nullptr, FILE_END);
508#else
509 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
510 if (fd_ < 0) {
511 throw io_error(
512 "fluxen: failed to reopen database file after successful rename");
513 }
514#endif
515 if (!remap()) {
516 throw io_error("fluxen: remap failed after successful rename");
517 }
518 return true;
519 }
520
526 [[nodiscard]] auto ptr() const noexcept -> const uint8_t * { return ptr_; }
527
533 [[nodiscard]] auto is_dirty() const noexcept -> bool {
534 return dirty_.load(std::memory_order_acquire);
535 }
536
537 [[nodiscard]] auto size() const noexcept -> size_t { return file_size_; }
538
539private:
540 void unmap() {
541 if (!ptr_)
542 return;
543#ifdef _WIN32
544 UnmapViewOfFile(ptr_);
545 if (map_) {
546 CloseHandle(map_);
547 map_ = nullptr;
548 }
549#else
550 ::munmap(ptr_, size_);
551#endif
552 ptr_ = nullptr;
553 size_ = 0;
554 }
555
556 [[nodiscard]] auto file_size() const noexcept -> size_t {
557#ifdef _WIN32
558 LARGE_INTEGER sz{};
559 GetFileSizeEx(file_, &sz);
560 return static_cast<size_t>(sz.QuadPart);
561#else
562 struct stat st{};
563 ::fstat(fd_, &st);
564 return static_cast<size_t>(st.st_size);
565#endif
566 }
567};
568} // namespace detail
570
571// --- transaction ---
572
573class DB; // forward declaration
574
587class Tx {
588private:
589 friend class DB;
590
591 struct Op {
592 std::string key;
593 std::vector<uint8_t> val;
594 bool is_delete;
595 };
596 std::vector<Op> ops_;
597
598public:
606 void put(std::string_view key, std::string_view value) {
607 if (key.empty() || key.size() > detail::MAX_KEY) {
608 throw key_error("fluxen: key must be between 1 and 255 bytes");
609 }
610 ops_.push_back({.key = std::string(key),
611 .val = std::vector<uint8_t>(value.begin(), value.end()),
612 .is_delete = false});
613 }
614
635 template <typename T>
636 requires(std::is_trivially_copyable_v<T> &&
637 !std::is_convertible_v<T, std::string_view>)
638 void put(std::string_view key, const T &value) {
639 if (key.empty() || key.size() > detail::MAX_KEY) {
640 throw key_error("fluxen: key must be between 1 and 255 bytes");
641 }
642 const auto *p = reinterpret_cast<const uint8_t *>(&value);
643 ops_.push_back({.key = std::string(key),
644 .val = std::vector<uint8_t>(p, p + sizeof(T)),
645 .is_delete = false});
646 }
647
657 void remove(std::string_view key) {
658 if (key.empty() || key.size() > detail::MAX_KEY) {
659 throw key_error("fluxen: key must be between 1 and 255 bytes");
660 }
661 ops_.push_back({.key = std::string(key), .val = {}, .is_delete = true});
662 }
663};
664
665// --- DB ---
666
701class DB {
702private:
703 detail::IndexMap index_;
704 mutable detail::MappedFile file_;
705 mutable std::shared_mutex mu_;
706 mutable std::mutex sync_mutex_;
707 bool poisoned_ = false;
708
709 void check_poisoned() const {
710 if (poisoned_) {
711 throw poisoned_error(
712 "fluxen: database is poisoned due to an unrecoverable I/O error");
713 }
714 }
715
716public:
737 explicit DB(const std::string &path) {
738 if (!file_.open(path)) {
739 throw io_error("fluxen: failed to open '" + path + "'");
740 }
741
742 if (file_.size() == 0) {
743 init_file();
744 } else {
745 load_index();
746 }
747 }
748
756 ~DB() = default;
758 DB(const DB &) = delete;
759 auto operator=(const DB &) -> DB & = delete;
761
762 // --- WRITE ---
763
784 void put(std::string_view key, std::string_view value) {
785 check_poisoned();
786 std::unique_lock lock(mu_);
787 append_entry(key, reinterpret_cast<const uint8_t *>(value.data()),
788 static_cast<uint32_t>(value.size()), false);
789 }
790
820 template <typename T>
821 requires(std::is_trivially_copyable_v<T> &&
822 !std::is_convertible_v<T, std::string_view>)
823 void put(std::string_view key, const T &value) {
824 check_poisoned();
825 std::unique_lock lock(mu_);
826 append_entry(key, reinterpret_cast<const uint8_t *>(&value),
827 static_cast<uint32_t>(sizeof(T)), false);
828 }
829
830 // --- READ ---
831
866 template <typename T = std::string>
867 auto get(std::string_view key) const -> std::optional<T> {
868 check_poisoned();
869 std::shared_lock lock(mu_);
870 ensure_mapped();
871
872 auto it = index_.find(key);
873 if (it == index_.end()) {
874 return std::nullopt;
875 }
876
877 const auto &entry = it->second;
878
879 if constexpr (std::is_same_v<T, std::string>) {
880 const auto *ptr = file_.ptr() + entry.val_offset;
881 return std::string(reinterpret_cast<const char *>(ptr), entry.val_len);
882 } else {
883 static_assert(std::is_trivially_copyable_v<T>,
884 "fluxen: T must be trivially copyable");
885 if (entry.val_len != static_cast<uint32_t>(sizeof(T))) {
886 return std::nullopt;
887 }
888 T result;
889 std::memcpy(&result, file_.ptr() + entry.val_offset, sizeof(T));
890 return result;
891 }
892 }
893
894 // --- DELETE ---
895
915 void remove(std::string_view key) {
916 check_poisoned();
917 std::unique_lock lock(mu_);
918 append_entry(key, nullptr, 0, true);
919 }
920
921 // --- QUERY ---
922
934 [[nodiscard]] auto has(std::string_view key) const -> bool {
935 check_poisoned();
936 std::shared_lock lock(mu_);
937 ensure_mapped();
938 return index_.contains(key);
939 }
940
970 void each(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 auto *ptr =
976 reinterpret_cast<const std::byte *>(file_.ptr() + entry.val_offset);
977 fn(key, Bytes{ptr, entry.val_len});
978 }
979 }
980
1011 void prefix(std::string_view pfx,
1012 const std::function<void(std::string_view, Bytes)> &fn) const {
1013 check_poisoned();
1014 std::shared_lock lock(mu_);
1015 ensure_mapped();
1016 for (const auto &[key, entry] : index_) {
1017 if (key.starts_with(pfx)) {
1018 auto *ptr =
1019 reinterpret_cast<const std::byte *>(file_.ptr() + entry.val_offset);
1020 fn(key, Bytes{ptr, entry.val_len});
1021 }
1022 }
1023 }
1024
1025 // --- TRANSACTION ---
1026
1076 void transaction(const std::function<TxResult(Tx &)> &fn) {
1077 check_poisoned();
1078 Tx tx;
1079 TxResult result = fn(tx);
1080 if (result == rollback) {
1081 return;
1082 }
1083
1084 std::vector<uint8_t> batch;
1085 batch.reserve(tx.ops_.size() * 64);
1086 for (const auto &op : tx.ops_) {
1087 detail::EntryHeader hdr{
1088 .flags = op.is_delete ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1089 .key_len = static_cast<uint8_t>(op.key.size()),
1090 .val_len = op.is_delete ? 0u : static_cast<uint32_t>(op.val.size()),
1091 };
1092 uint8_t raw[detail::HEADER_SIZE];
1093 detail::encode_header(raw, hdr);
1094 batch.insert(batch.end(), raw, raw + detail::HEADER_SIZE);
1095 batch.insert(batch.end(), op.key.begin(), op.key.end());
1096 if (!op.is_delete) {
1097 batch.insert(batch.end(), op.val.begin(), op.val.end());
1098 }
1099 }
1100
1101 std::unique_lock lock(mu_);
1102
1103 const size_t size_before = file_.size();
1104
1105 if (!file_.append(batch.data(), batch.size())) {
1106 throw io_error("fluxen: transaction append failed");
1107 }
1108
1109 if (!file_.sync()) {
1110 if (!file_.truncate(size_before)) {
1111 poisoned_ = true;
1112 throw poisoned_error(
1113 "fluxen: transaction fsync failed and truncation failed. Database "
1114 "file may contain a partial tail entry");
1115 }
1116 throw io_error("fluxen: transaction fsync failed");
1117 }
1118
1119 size_t pos = 0;
1120 for (const auto &op : tx.ops_) {
1121 pos += detail::HEADER_SIZE + op.key.size();
1122 if (op.is_delete) {
1123 index_.erase(op.key);
1124 } else {
1125 index_[op.key] = {.val_offset = size_before + pos,
1126 .val_len = static_cast<uint32_t>(op.val.size())};
1127 pos += op.val.size();
1128 }
1129 }
1130 }
1131
1132 // --- MAINTENANCE ---
1133
1169 [[nodiscard]] auto compact() -> bool {
1170 check_poisoned();
1171 std::unique_lock lock(mu_);
1172
1173 if (file_.is_dirty() && !file_.remap()) {
1174 throw io_error("fluxen: remap failed before compaction");
1175 }
1176
1177 std::vector<uint8_t> buf;
1178 buf.reserve(file_.size());
1179
1180 buf.insert(buf.end(), detail::MAGIC, detail::MAGIC + sizeof(detail::MAGIC));
1181
1182 detail::IndexMap new_index;
1183 for (const auto &[key, entry] : index_) {
1184 detail::EntryHeader hdr{
1185 .flags = detail::FLAG_LIVE,
1186 .key_len = static_cast<uint8_t>(key.size()),
1187 .val_len = entry.val_len,
1188 };
1189
1190 uint8_t raw[detail::HEADER_SIZE];
1191 detail::encode_header(raw, hdr);
1192
1193 size_t val_off = buf.size() + detail::HEADER_SIZE + key.size();
1194
1195 buf.insert(buf.end(), raw, raw + detail::HEADER_SIZE);
1196 buf.insert(buf.end(), key.begin(), key.end());
1197
1198 const auto *val_ptr = file_.ptr() + entry.val_offset;
1199 buf.insert(buf.end(), val_ptr, val_ptr + entry.val_len);
1200
1201 new_index[key] = {.val_offset = val_off, .val_len = entry.val_len};
1202 }
1203
1204 if (!file_.rewrite(buf)) {
1205 return false;
1206 }
1207
1208 index_ = std::move(new_index);
1209 return true;
1210 }
1211
1212 // --- DIAGNOSTICS ---
1213
1222 [[nodiscard]] auto key_count() const -> size_t {
1223 check_poisoned();
1224 std::shared_lock lock(mu_);
1225 return index_.size();
1226 }
1227
1240 [[nodiscard]] auto file_size() const -> size_t {
1241 check_poisoned();
1242 std::shared_lock lock(mu_);
1243 return file_.size();
1244 }
1245
1246private:
1248 void init_file() {
1249 if (!file_.append(detail::MAGIC, sizeof(detail::MAGIC))) {
1250 throw corrupt_error("fluxen: failed to write magic header");
1251 }
1252 }
1253
1262 void load_index() {
1263 if (file_.size() < sizeof(detail::MAGIC)) {
1264 throw corrupt_error("fluxen: file too small to be valid");
1265 }
1266
1267 if (std::memcmp(file_.ptr(), detail::MAGIC, sizeof(detail::MAGIC)) != 0) {
1268 throw corrupt_error("fluxen: bad magic. File was not created by fluxen");
1269 }
1270
1271 size_t pos = sizeof(detail::MAGIC);
1272 size_t last_good_pos = pos;
1273
1274 while (pos + detail::HEADER_SIZE <= file_.size()) {
1275 uint8_t raw[detail::HEADER_SIZE];
1276 std::memcpy(raw, file_.ptr() + pos, detail::HEADER_SIZE);
1277 detail::EntryHeader hdr = detail::decode_header(raw);
1278 pos += detail::HEADER_SIZE;
1279
1280 if (pos + hdr.key_len + hdr.val_len > file_.size()) {
1281 break;
1282 }
1283
1284 std::string key(reinterpret_cast<const char *>(file_.ptr() + pos),
1285 hdr.key_len);
1286 pos += hdr.key_len;
1287
1288 if (hdr.flags == detail::FLAG_TOMB) {
1289 index_.erase(key);
1290 } else {
1291 index_[key] = {.val_offset = pos, .val_len = hdr.val_len};
1292 }
1293
1294 pos += hdr.val_len;
1295 last_good_pos = pos;
1296 }
1297
1298 if (last_good_pos < file_.size()) {
1299 if (!file_.truncate(last_good_pos)) {
1300 throw corrupt_error(
1301 "fluxen: failed to truncate partial tail entry on open");
1302 }
1303 }
1304 }
1305
1315 void append_entry(std::string_view key, const uint8_t *val, uint32_t val_len,
1316 bool tombstone) {
1317 if (key.empty() || key.size() > detail::MAX_KEY) {
1318 throw key_error("fluxen: key must be between 1 and 255 bytes");
1319 }
1320
1321 detail::EntryHeader hdr{
1322 .flags = tombstone ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1323 .key_len = static_cast<uint8_t>(key.size()),
1324 .val_len = val_len,
1325 };
1326
1327 std::vector<uint8_t> buf;
1328 buf.resize(detail::HEADER_SIZE + key.size() + val_len);
1329 detail::encode_header(buf.data(), hdr);
1330 std::memcpy(buf.data() + detail::HEADER_SIZE, key.data(), key.size());
1331 if (val && val_len) {
1332 std::memcpy(buf.data() + detail::HEADER_SIZE + key.size(), val, val_len);
1333 }
1334
1335 const size_t size_before = file_.size();
1336
1337 if (!file_.append(buf.data(), buf.size())) {
1338 if (!file_.truncate(size_before)) {
1339 poisoned_ = true;
1340 throw poisoned_error(
1341 "fluxen: append failed and truncation failed. Database file may "
1342 "contain a partial tail entry");
1343 }
1344 throw io_error("fluxen: append failed");
1345 }
1346
1347 if (tombstone) {
1348 if (auto it = index_.find(key); it != index_.end()) {
1349 index_.erase(it);
1350 }
1351 } else {
1352 index_[std::string(key)] = {.val_offset = file_.size() - val_len,
1353 .val_len = val_len};
1354 }
1355 }
1356
1375 void ensure_mapped() const {
1376 if (!file_.is_dirty()) {
1377 return;
1378 }
1379
1380 std::unique_lock sync_lock(sync_mutex_);
1381 if (file_.is_dirty() && !file_.remap()) {
1382 throw io_error("fluxen: remap failed");
1383 }
1384 }
1385};
1386
1387} // namespace fluxen
A persistent key-value database backed by a single file.
Definition fluxen.hpp:701
auto has(std::string_view key) const -> bool
Returns true if the given key exists in the database.
Definition fluxen.hpp:934
auto file_size() const -> size_t
Returns the current size of the database file in bytes.
Definition fluxen.hpp:1240
auto key_count() const -> size_t
Returns the number of live keys currently stored.
Definition fluxen.hpp:1222
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:1011
void transaction(const std::function< TxResult(Tx &)> &fn)
Executes a batch of operations atomically.
Definition fluxen.hpp:1076
void remove(std::string_view key)
Deletes the value stored under the given key.
Definition fluxen.hpp:915
~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:823
auto compact() -> bool
Rewrites the database file retaining only live entries.
Definition fluxen.hpp:1169
DB(const std::string &path)
Opens or creates a database at the given path.
Definition fluxen.hpp:737
auto get(std::string_view key) const -> std::optional< T >
Retrieves the value stored under the given key.
Definition fluxen.hpp:867
void each(const std::function< void(std::string_view, Bytes)> &fn) const
Iterates over all live key-value pairs.
Definition fluxen.hpp:970
void put(std::string_view key, std::string_view value)
Stores a string value under the given key.
Definition fluxen.hpp:784
A staged batch of write operations, used inside DB::transaction().
Definition fluxen.hpp:587
void put(std::string_view key, std::string_view value)
Stage a string value to be written.
Definition fluxen.hpp:606
void remove(std::string_view key)
Stage a key deletion.
Definition fluxen.hpp:657
void put(std::string_view key, const T &value)
Stage a trivially copyable value to be written.
Definition fluxen.hpp:638
TxResult
Controls whether a transaction's operations are applied or discarded.
Definition fluxen.hpp:129
std::span< const std::byte > Bytes
A non-owning view of raw bytes in the memory-mapped file.
Definition fluxen.hpp:120
Definition fluxen.hpp:138
fluxen's error hierarchy
Definition fluxen.hpp:134
Definition fluxen.hpp:146
Definition fluxen.hpp:142