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