87#include <shared_mutex>
93#include <unordered_map>
97#define WIN32_LEAN_AND_MEAN
117using Bytes = std::span<const std::byte>;
132 using std::runtime_error::runtime_error;
136 using std::runtime_error::runtime_error;
140 using io_error::io_error;
144 using std::runtime_error::runtime_error;
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;
166inline void encode_header(uint8_t out[HEADER_SIZE],
167 const EntryHeader &h)
noexcept {
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);
177inline EntryHeader decode_header(
const uint8_t in[HEADER_SIZE])
noexcept {
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,
195 using is_transparent = void;
197 size_t operator()(std::string_view sv)
const noexcept {
198 return std::hash<std::string_view>{}(sv);
203std::wstring utf8_to_wstring(std::string_view utf8) {
208 int len = MultiByteToWideChar(CP_UTF8, 0, utf8.data(),
209 static_cast<int>(utf8.size()),
nullptr, 0);
214 std::wstring wide(len, L
'\0');
215 MultiByteToWideChar(CP_UTF8, 0, utf8.data(),
static_cast<int>(utf8.size()),
223 std::unordered_map<std::string, IndexEntry, StringHash, std::equal_to<>>;
229 HANDLE file_ = INVALID_HANDLE_VALUE;
230 HANDLE map_ =
nullptr;
234 uint8_t *ptr_ =
nullptr;
236 size_t file_size_ = 0;
237 std::atomic<bool> dirty_{
false};
238 std::filesystem::path path_;
241 MappedFile() =
default;
242 ~MappedFile() { close(); }
244 MappedFile(
const MappedFile &) =
delete;
245 MappedFile &operator=(
const MappedFile &) =
delete;
246 MappedFile(
const MappedFile &&) =
delete;
247 MappedFile &operator=(MappedFile &&) =
delete;
249 bool open(std::string_view path) {
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) {
257 SetFilePointer(file_, 0,
nullptr, FILE_END);
260 fd_ = ::open(path_.c_str(), O_RDWR | O_CREAT | O_APPEND, 0644);
271 if (file_ != INVALID_HANDLE_VALUE) {
273 file_ = INVALID_HANDLE_VALUE;
288 dirty_.store(
false, std::memory_order_release);
293 map_ = CreateFileMappingW(file_,
nullptr, PAGE_READWRITE, 0, 0,
nullptr);
296 dirty_.store(
false, std::memory_order_release);
300 ptr_ =
static_cast<uint8_t *
>(
301 MapViewOfFile(map_, FILE_MAP_ALL_ACCESS, 0, 0, size_));
306 dirty_.store(
false, std::memory_order_release);
310 ptr_ =
static_cast<uint8_t *
>(
311 ::mmap(
nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
313 if (ptr_ == MAP_FAILED) {
315 dirty_.store(
false, std::memory_order_release);
319 dirty_.store(
false, std::memory_order_release);
323 bool append(
const void *data,
size_t len) {
329 if (!WriteFile(file_, data,
static_cast<DWORD
>(len), &written,
nullptr) ||
330 written !=
static_cast<DWORD
>(len)) {
334 if (::write(fd_, data, len) !=
static_cast<ssize_t
>(len)) {
338 dirty_.store(
true, std::memory_order_relaxed);
348 [[nodiscard]]
bool sync() {
350 return FlushFileBuffers(file_) != 0;
352 return ::fsync(fd_) == 0;
366 [[nodiscard]]
bool truncate(
size_t new_size) {
370 li.QuadPart =
static_cast<LONGLONG
>(new_size);
371 if (!SetFilePointerEx(file_, li,
nullptr, FILE_BEGIN)) {
374 if (!SetEndOfFile(file_)) {
375 SetFilePointer(file_, 0,
nullptr, FILE_END);
378 SetFilePointer(file_, 0,
nullptr, FILE_END);
380 if (::ftruncate(fd_,
static_cast<off_t
>(new_size)) != 0) {
384 file_size_ = new_size;
385 dirty_.store(
true, std::memory_order_release);
414 bool rewrite(
const std::vector<uint8_t> &data) {
415 std::filesystem::path tmp_path = path_;
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) {
425 const bool write_ok =
426 WriteFile(tmp, data.data(),
static_cast<DWORD
>(data.size()), &written,
428 written ==
static_cast<DWORD
>(data.size()) &&
429 FlushFileBuffers(tmp) != 0;
434 DeleteFileW(tmp_path.c_str());
439 ::open(tmp_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644);
443 const bool write_ok = ::write(tmp_fd, data.data(), data.size()) ==
444 static_cast<ssize_t
>(data.size()) &&
445 ::fsync(tmp_fd) == 0;
450 ::unlink(tmp_path.c_str());
456 if (file_ != INVALID_HANDLE_VALUE) {
458 file_ = INVALID_HANDLE_VALUE;
468 if (!ReplaceFileW(path_.c_str(), tmp_path.c_str(),
nullptr,
469 REPLACEFILE_IGNORE_MERGE_ERRORS,
nullptr,
nullptr)) {
471 DeleteFileW(tmp_path.c_str());
474 CreateFileW(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
nullptr,
475 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
nullptr);
477 if (file_ == INVALID_HANDLE_VALUE) {
478 throw io_error(
"fluxen: failed to reopen database file after replace");
481 SetFilePointer(file_, 0,
nullptr, FILE_END);
483 throw io_error(
"fluxen: remap failed after replace failure");
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);
492 throw io_error(
"fluxen: failed to reopen database file after rename");
495 throw io_error(
"fluxen: remap failed after rename failure");
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) {
506 "fluxen: failed to reopen database file after successful replace");
508 SetFilePointer(file_, 0,
nullptr, FILE_END);
510 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
513 "fluxen: failed to reopen database file after successful rename");
517 throw io_error(
"fluxen: remap failed after successful rename");
527 [[nodiscard]]
const uint8_t *ptr() const noexcept {
return ptr_; }
534 [[nodiscard]]
bool is_dirty() const noexcept {
535 return dirty_.load(std::memory_order_acquire);
538 [[nodiscard]]
size_t size() const noexcept {
return file_size_; }
545 UnmapViewOfFile(ptr_);
551 ::munmap(ptr_, size_);
557 [[nodiscard]]
size_t file_size() const noexcept {
560 GetFileSizeEx(file_, &sz);
561 return static_cast<size_t>(sz.QuadPart);
565 return static_cast<size_t>(st.st_size);
594 std::vector<uint8_t> val;
597 std::vector<Op> ops_;
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");
611 ops_.push_back({.key = std::string(key),
612 .val = std::vector<uint8_t>(value.begin(), value.end()),
613 .is_delete =
false});
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");
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});
659 if (key.empty() || key.size() > detail::MAX_KEY) {
660 throw key_error(
"fluxen: key must be between 1 and 255 bytes");
662 ops_.push_back({.key = std::string(key), .val = {}, .is_delete =
true});
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;
710 void check_poisoned()
const {
713 "fluxen: database is poisoned due to an unrecoverable I/O error");
738 explicit DB(std::string_view path) {
739 if (!file_.open(path)) {
740 throw io_error(
"fluxen: failed to open '" + std::string(path) +
"'");
743 if (file_.size() == 0) {
759 DB(
const DB &) =
delete;
760 DB &operator=(
const DB &) =
delete;
785 void put(std::string_view key, std::string_view value) {
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);
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) {
826 std::unique_lock lock(mu_);
827 append_entry(key,
reinterpret_cast<const uint8_t *
>(&value),
828 static_cast<uint32_t
>(
sizeof(T)),
false);
867 template <
typename T = std::
string>
868 std::optional<T>
get(std::string_view key)
const {
870 std::shared_lock lock(mu_);
873 auto it = index_.find(key);
874 if (it == index_.end()) {
878 const auto &entry = it->second;
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);
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))) {
890 std::memcpy(&result, file_.ptr() + entry.val_offset,
sizeof(T));
918 std::unique_lock lock(mu_);
919 append_entry(key,
nullptr, 0,
true);
935 [[nodiscard]]
bool has(std::string_view key)
const {
937 std::shared_lock lock(mu_);
939 return index_.contains(key);
971 void each(
const std::function<
void(std::string_view,
Bytes)> &fn)
const {
973 std::shared_lock lock(mu_);
975 for (
const auto &[key, entry] : index_) {
977 reinterpret_cast<const std::byte *
>(file_.ptr() + entry.val_offset);
978 fn(key,
Bytes{ptr, entry.val_len});
1013 const std::function<
void(std::string_view,
Bytes)> &fn)
const {
1015 std::shared_lock lock(mu_);
1017 for (
const auto &[key, entry] : index_) {
1018 if (key.starts_with(pfx)) {
1020 reinterpret_cast<const std::byte *
>(file_.ptr() + entry.val_offset);
1021 fn(key,
Bytes{ptr, entry.val_len});
1082 if (result == rollback) {
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()),
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());
1103 std::unique_lock lock(mu_);
1105 const size_t size_before = file_.size();
1107 if (!file_.append(batch.data(), batch.size())) {
1108 throw io_error(
"fluxen: transaction append failed");
1111 if (!file_.sync()) {
1112 if (!file_.truncate(size_before)) {
1115 "fluxen: transaction fsync failed and truncation failed. Database "
1116 "file may contain a partial tail entry");
1118 throw io_error(
"fluxen: transaction fsync failed");
1122 for (
const auto &op : tx.ops_) {
1123 pos += detail::HEADER_SIZE + op.key.size();
1125 index_.erase(op.key);
1127 index_[op.key] = {.val_offset = size_before + pos,
1128 .val_len =
static_cast<uint32_t
>(op.val.size())};
1129 pos += op.val.size();
1173 std::unique_lock lock(mu_);
1175 if (file_.is_dirty() && !file_.remap()) {
1176 throw io_error(
"fluxen: remap failed before compaction");
1179 std::vector<uint8_t> buf;
1180 buf.reserve(file_.size());
1182 buf.insert(buf.end(), detail::MAGIC, detail::MAGIC +
sizeof(detail::MAGIC));
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,
1192 uint8_t raw[detail::HEADER_SIZE];
1193 detail::encode_header(raw, hdr);
1195 size_t val_off = buf.size() + detail::HEADER_SIZE + key.size();
1197 buf.insert(buf.end(), raw, raw + detail::HEADER_SIZE);
1198 buf.insert(buf.end(), key.begin(), key.end());
1200 const auto *val_ptr = file_.ptr() + entry.val_offset;
1201 buf.insert(buf.end(), val_ptr, val_ptr + entry.val_len);
1203 new_index[key] = {.val_offset = val_off, .val_len = entry.val_len};
1206 if (!file_.rewrite(buf)) {
1210 index_ = std::move(new_index);
1226 std::shared_lock lock(mu_);
1227 return index_.size();
1244 std::shared_lock lock(mu_);
1245 return file_.size();
1251 if (!file_.append(detail::MAGIC,
sizeof(detail::MAGIC))) {
1252 throw corrupt_error(
"fluxen: failed to write magic header");
1265 if (file_.size() <
sizeof(detail::MAGIC)) {
1266 throw corrupt_error(
"fluxen: file too small to be valid");
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");
1273 size_t pos =
sizeof(detail::MAGIC);
1274 size_t last_good_pos = pos;
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;
1282 if (pos + hdr.key_len + hdr.val_len > file_.size()) {
1286 std::string key(
reinterpret_cast<const char *
>(file_.ptr() + pos),
1290 if (hdr.flags == detail::FLAG_TOMB) {
1293 index_[key] = {.val_offset = pos, .val_len = hdr.val_len};
1297 last_good_pos = pos;
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");
1317 void append_entry(std::string_view key,
const uint8_t *val, uint32_t val_len,
1319 if (key.empty() || key.size() > detail::MAX_KEY) {
1320 throw key_error(
"fluxen: key must be between 1 and 255 bytes");
1323 detail::EntryHeader hdr{
1324 .flags = tombstone ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1325 .key_len =
static_cast<uint8_t
>(key.size()),
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);
1337 const size_t size_before = file_.size();
1339 if (!file_.append(buf.data(), buf.size())) {
1340 if (!file_.truncate(size_before)) {
1342 throw poisoned_error(
1343 "fluxen: append failed and truncation failed. Database file may "
1344 "contain a partial tail entry");
1346 throw io_error(
"fluxen: append failed");
1350 if (
auto it = index_.find(key); it != index_.end()) {
1354 index_[std::string(key)] = {.val_offset = file_.size() - val_len,
1355 .val_len = val_len};
1377 void ensure_mapped()
const {
1378 if (!file_.is_dirty()) {
1382 std::unique_lock sync_lock(sync_mutex_);
1383 if (file_.is_dirty() && !file_.remap()) {
1384 throw io_error(
"fluxen: remap failed");
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