Initial commit: Crafter.DB
Header-only C++26 module library for typed, file-backed tables with schema reflection, migrations, queries, and crash-safe commits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
bfd0cc261b
34 changed files with 7506 additions and 0 deletions
317
implementations/Crafter.DB-File.cpp
Normal file
317
implementations/Crafter.DB-File.cpp
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
|
||||
#define _GNU_SOURCE 1
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
module Crafter.DB:File_impl;
|
||||
import :File;
|
||||
import :Error;
|
||||
import std;
|
||||
|
||||
using namespace Crafter::DB;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
std::error_code Errno() noexcept {
|
||||
return std::error_code(errno, std::generic_category());
|
||||
}
|
||||
|
||||
[[noreturn]] void Fail(std::string_view what, const fs::path& path) {
|
||||
throw IOError(what, Errno(), path);
|
||||
}
|
||||
}
|
||||
|
||||
// --- File -------------------------------------------------------------------
|
||||
|
||||
File::File(File&& other) noexcept : descriptor(other.descriptor), path(std::move(other.path)) {
|
||||
other.descriptor = -1;
|
||||
}
|
||||
|
||||
File& File::operator=(File&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Close();
|
||||
descriptor = other.descriptor;
|
||||
path = std::move(other.path);
|
||||
other.descriptor = -1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
File::~File() {
|
||||
Close();
|
||||
}
|
||||
|
||||
File File::Open(const fs::path& path, bool writable) {
|
||||
File file;
|
||||
file.path = path;
|
||||
// O_NOFOLLOW: a private cache file must not be swapped for a symlink to
|
||||
// somewhere we would then happily write 0600 data into.
|
||||
const std::int32_t flags = (writable ? O_RDWR : O_RDONLY) | O_CLOEXEC | O_NOFOLLOW;
|
||||
file.descriptor = ::open(path.c_str(), flags);
|
||||
if (file.descriptor == -1) Fail("cannot open", path);
|
||||
return file;
|
||||
}
|
||||
|
||||
File File::CreateExclusive(const fs::path& path, fs::perms permissions) {
|
||||
File file;
|
||||
file.path = path;
|
||||
file.descriptor = ::open(path.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, static_cast<mode_t>(permissions));
|
||||
if (file.descriptor == -1) Fail("cannot create", path);
|
||||
return file;
|
||||
}
|
||||
|
||||
void File::LockExclusive() {
|
||||
if (::flock(descriptor, LOCK_EX | LOCK_NB) == -1) {
|
||||
if (errno == EWOULDBLOCK) throw LockedError(std::format("{}: another Table holds this file open for writing", path.string()));
|
||||
Fail("cannot lock", path);
|
||||
}
|
||||
}
|
||||
|
||||
void File::LockShared() {
|
||||
if (::flock(descriptor, LOCK_SH | LOCK_NB) == -1) {
|
||||
if (errno == EWOULDBLOCK) throw LockedError(std::format("{}: another Table holds this file open for writing", path.string()));
|
||||
Fail("cannot lock", path);
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t File::Size() const {
|
||||
struct stat info{};
|
||||
if (::fstat(descriptor, &info) == -1) Fail("cannot stat", path);
|
||||
return static_cast<std::uint64_t>(info.st_size);
|
||||
}
|
||||
|
||||
void File::Truncate(std::uint64_t length) {
|
||||
if (::ftruncate(descriptor, static_cast<off_t>(length)) == -1) Fail("cannot truncate", path);
|
||||
}
|
||||
|
||||
void File::Allocate(std::uint64_t offset, std::uint64_t length) {
|
||||
if (::fallocate(descriptor, 0, static_cast<off_t>(offset), static_cast<off_t>(length)) == 0) return;
|
||||
if (errno != EOPNOTSUPP && errno != ENOSYS) Fail("cannot allocate space", path);
|
||||
// Filesystems without fallocate (FUSE, 9p, exFAT): posix_fallocate falls
|
||||
// back to writing zeros, which equally guarantees the blocks exist.
|
||||
const std::int32_t result = ::posix_fallocate(descriptor, static_cast<off_t>(offset), static_cast<off_t>(length));
|
||||
if (result != 0) throw IOError("cannot allocate space", std::error_code(result, std::generic_category()), path);
|
||||
}
|
||||
|
||||
void File::ReadAt(std::uint64_t offset, std::span<std::byte> out) const {
|
||||
while (!out.empty()) {
|
||||
const ssize_t got = ::pread(descriptor, out.data(), out.size(), static_cast<off_t>(offset));
|
||||
if (got == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
Fail("read failed", path);
|
||||
}
|
||||
if (got == 0) throw CorruptError(std::format("{}: file ends {} bytes short", path.string(), out.size()));
|
||||
out = out.subspan(static_cast<std::size_t>(got));
|
||||
offset += static_cast<std::uint64_t>(got);
|
||||
}
|
||||
}
|
||||
|
||||
void File::WriteAt(std::uint64_t offset, std::span<const std::byte> in) {
|
||||
while (!in.empty()) {
|
||||
const ssize_t put = ::pwrite(descriptor, in.data(), in.size(), static_cast<off_t>(offset));
|
||||
if (put == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
Fail("write failed", path);
|
||||
}
|
||||
in = in.subspan(static_cast<std::size_t>(put));
|
||||
offset += static_cast<std::uint64_t>(put);
|
||||
}
|
||||
}
|
||||
|
||||
void File::SyncData() {
|
||||
if (::fdatasync(descriptor) == -1) Fail("fdatasync failed", path);
|
||||
}
|
||||
|
||||
void File::Sync() {
|
||||
if (::fsync(descriptor) == -1) Fail("fsync failed", path);
|
||||
}
|
||||
|
||||
void File::Close() noexcept {
|
||||
if (descriptor != -1) {
|
||||
::close(descriptor);
|
||||
descriptor = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// --- MappedRegion -----------------------------------------------------------
|
||||
|
||||
MappedRegion::MappedRegion(MappedRegion&& other) noexcept : base(other.base), reserved(other.reserved), mapped(other.mapped), dontDump(other.dontDump) {
|
||||
other.base = nullptr;
|
||||
other.reserved = 0;
|
||||
other.mapped = 0;
|
||||
}
|
||||
|
||||
MappedRegion& MappedRegion::operator=(MappedRegion&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Release();
|
||||
base = other.base;
|
||||
reserved = other.reserved;
|
||||
mapped = other.mapped;
|
||||
dontDump = other.dontDump;
|
||||
other.base = nullptr;
|
||||
other.reserved = 0;
|
||||
other.mapped = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
MappedRegion::~MappedRegion() {
|
||||
Release();
|
||||
}
|
||||
|
||||
void MappedRegion::Reserve(std::uint64_t bytes) {
|
||||
Release();
|
||||
// PROT_NONE + MAP_NORESERVE costs no memory and no commit charge: it only
|
||||
// claims the addresses, so later MAP_FIXED calls cannot collide with
|
||||
// anything else the process maps.
|
||||
void* address = ::mmap(nullptr, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (address == MAP_FAILED) throw IOError(std::format("cannot reserve {} bytes of address space", bytes), Errno(), fs::path());
|
||||
base = static_cast<std::byte*>(address);
|
||||
reserved = bytes;
|
||||
mapped = 0;
|
||||
}
|
||||
|
||||
void MappedRegion::Map(const File& file, std::uint64_t fileOffset, std::uint64_t length, bool writable) {
|
||||
if (mapped + length > reserved) {
|
||||
throw IOError(std::format("table needs more than the {} bytes of address space reserved per region; reopen with a larger OpenOptions::reserveBytes", reserved), std::make_error_code(std::errc::not_enough_memory), file.Path());
|
||||
}
|
||||
void* address = ::mmap(base + mapped, length, writable ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED | MAP_FIXED, file.Descriptor(), static_cast<off_t>(fileOffset));
|
||||
if (address == MAP_FAILED) Fail("cannot map", file.Path());
|
||||
if (dontDump) ::madvise(address, length, MADV_DONTDUMP);
|
||||
mapped += length;
|
||||
}
|
||||
|
||||
// MADV_DONTDUMP only. MADV_DONTFORK was considered and rejected: it turns any
|
||||
// touch of an inherited table in a forked child into SIGSEGV. The mapping is
|
||||
// MAP_SHARED, so a child reads consistently; writes are refused by the
|
||||
// owner-process check in TableFile.
|
||||
void MappedRegion::ExcludeFromCoreDumps() {
|
||||
dontDump = true;
|
||||
if (mapped != 0) ::madvise(base, mapped, MADV_DONTDUMP);
|
||||
}
|
||||
|
||||
void MappedRegion::WillNeed(std::uint64_t length) {
|
||||
if (length != 0) ::madvise(base, std::min(length, mapped), MADV_WILLNEED);
|
||||
}
|
||||
|
||||
void MappedRegion::Release() noexcept {
|
||||
if (base != nullptr) {
|
||||
::munmap(base, reserved);
|
||||
base = nullptr;
|
||||
reserved = 0;
|
||||
mapped = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- MappedPages ------------------------------------------------------------
|
||||
|
||||
MappedPages::MappedPages(MappedPages&& other) noexcept : data(other.data), length(other.length) {
|
||||
other.data = nullptr;
|
||||
other.length = 0;
|
||||
}
|
||||
|
||||
MappedPages& MappedPages::operator=(MappedPages&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Release();
|
||||
data = other.data;
|
||||
length = other.length;
|
||||
other.data = nullptr;
|
||||
other.length = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
MappedPages::~MappedPages() {
|
||||
Release();
|
||||
}
|
||||
|
||||
void MappedPages::Map(const File& file, std::uint64_t fileOffset, std::uint64_t mapLength, bool writable) {
|
||||
Release();
|
||||
void* address = ::mmap(nullptr, mapLength, writable ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED, file.Descriptor(), static_cast<off_t>(fileOffset));
|
||||
if (address == MAP_FAILED) Fail("cannot map header", file.Path());
|
||||
data = static_cast<std::byte*>(address);
|
||||
length = mapLength;
|
||||
}
|
||||
|
||||
void MappedPages::Sync() {
|
||||
if (data != nullptr && ::msync(data, length, MS_SYNC) == -1) throw IOError("msync failed", Errno(), fs::path());
|
||||
}
|
||||
|
||||
void MappedPages::Release() noexcept {
|
||||
if (data != nullptr) {
|
||||
::munmap(data, length);
|
||||
data = nullptr;
|
||||
length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Os ---------------------------------------------------------------------
|
||||
|
||||
void Os::CreateDirectories(const fs::path& directory, fs::perms permissions) {
|
||||
if (directory.empty()) return;
|
||||
fs::path current;
|
||||
for (const fs::path& part : directory) {
|
||||
current /= part;
|
||||
if (current == directory.root_path() || current.empty()) continue;
|
||||
if (::mkdir(current.c_str(), static_cast<mode_t>(permissions)) == -1 && errno != EEXIST) Fail("cannot create directory", current);
|
||||
}
|
||||
}
|
||||
|
||||
void Os::Rename(const fs::path& from, const fs::path& to) {
|
||||
if (::rename(from.c_str(), to.c_str()) == -1) Fail(std::format("cannot rename to {}", to.string()), from);
|
||||
}
|
||||
|
||||
bool Os::RenameNoReplace(const fs::path& from, const fs::path& to) {
|
||||
if (::renameat2(AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE) == 0) return true;
|
||||
if (errno == EEXIST) return false;
|
||||
Fail(std::format("cannot rename to {}", to.string()), from);
|
||||
}
|
||||
|
||||
void Os::HardLink(const fs::path& from, const fs::path& to) {
|
||||
if (::link(from.c_str(), to.c_str()) == -1) Fail(std::format("cannot link to {}", to.string()), from);
|
||||
}
|
||||
|
||||
bool Os::Remove(const fs::path& path) noexcept {
|
||||
return ::unlink(path.c_str()) == 0;
|
||||
}
|
||||
|
||||
void Os::SyncDirectory(const fs::path& directory) {
|
||||
const fs::path dir = directory.empty() ? fs::path(".") : directory;
|
||||
const std::int32_t fd = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd == -1) Fail("cannot open directory", dir);
|
||||
const std::int32_t result = ::fsync(fd);
|
||||
::close(fd);
|
||||
if (result == -1) Fail("fsync of directory failed", dir);
|
||||
}
|
||||
|
||||
void Os::RandomBytes(std::span<std::byte> out) {
|
||||
std::random_device device;
|
||||
for (std::size_t i = 0; i < out.size(); i += 4) {
|
||||
const std::uint32_t value = device();
|
||||
std::memcpy(out.data() + i, &value, std::min<std::size_t>(4, out.size() - i));
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t Os::UnixNanos() noexcept {
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
std::uint64_t Os::PageSize() noexcept {
|
||||
const long size = ::sysconf(_SC_PAGESIZE);
|
||||
return size > 0 ? static_cast<std::uint64_t>(size) : 4096;
|
||||
}
|
||||
|
||||
std::int64_t Os::ProcessId() noexcept {
|
||||
return static_cast<std::int64_t>(::getpid());
|
||||
}
|
||||
Loading…
Reference in a new issue