Crafter.DB/interfaces/Crafter.DB-File.cppm
Jorijn van der Graaf bfd0cc261b
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>
2026-10-17 04:16:51 +02:00

142 lines
6.4 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Everything that talks to the operating system: file descriptors, advisory
// locks, preallocation, the reserved virtual ranges that keep row pointers
// stable while a table grows, durability calls and the atomic create/replace
// dance. Nothing here knows what a table is; the Storage partition does.
export module Crafter.DB:File;
import std;
import :Error;
export namespace Crafter::DB {
enum class OpenMode : std::uint8_t {
OpenOrCreate, // open the table, create it when missing (default)
OpenExisting, // IOError when missing
CreateNew, // IOError when present
};
enum class Durability : std::uint8_t {
OnFlush, // durable after Flush() and on destruction (default)
EveryAppend, // Flush() after every Append/Emplace/Update/Set/Erase
};
struct OpenOptions {
OpenMode mode = OpenMode::OpenOrCreate;
bool readOnly = false; // PROT_READ mapping, shared lock, no migration, no writes
bool createDirectories = true; // create missing parent directories with directoryPermissions
std::filesystem::perms filePermissions = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write; // 0600
std::filesystem::perms directoryPermissions = std::filesystem::perms::owner_all; // 0700
Durability durability = Durability::OnFlush;
bool migrate = true; // run the Previous chain on an older file; false throws MigrationError instead
bool backupBeforeMigrate = true; // keep the pre-migration file as <file>.v<old>.bak
std::uint64_t reserveBytes = std::uint64_t(32) << 30; // virtual address space reserved per region
bool excludeFromCoreDumps = true; // MADV_DONTDUMP on the mappings
bool prefetch = false; // MADV_WILLNEED over the committed data at open (cold-cache batch jobs)
};
// An open file descriptor. Every operation throws IOError on failure.
class File {
public:
File() = default;
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept;
File& operator=(File&& other) noexcept;
~File();
static File Open(const std::filesystem::path& path, bool writable);
static File CreateExclusive(const std::filesystem::path& path, std::filesystem::perms permissions);
bool IsOpen() const noexcept { return descriptor != -1; }
std::int32_t Descriptor() const noexcept { return descriptor; }
const std::filesystem::path& Path() const noexcept { return path; }
void SetPath(std::filesystem::path newPath) { path = std::move(newPath); }
// flock(LOCK_NB). Advisory, per open file description, released on close.
void LockExclusive();
void LockShared();
std::uint64_t Size() const;
void Truncate(std::uint64_t length);
// fallocate: real blocks, so a full disk is an error here and never a
// SIGBUS on first touch of a mapped page.
void Allocate(std::uint64_t offset, std::uint64_t length);
void ReadAt(std::uint64_t offset, std::span<std::byte> out) const;
void WriteAt(std::uint64_t offset, std::span<const std::byte> in);
void SyncData();
void Sync();
void Close() noexcept;
private:
std::int32_t descriptor = -1;
std::filesystem::path path;
};
// A reserved virtual address range into which file extents are mapped
// back to back, so the region reads as one contiguous array that grows
// without ever moving.
class MappedRegion {
public:
MappedRegion() = default;
MappedRegion(const MappedRegion&) = delete;
MappedRegion& operator=(const MappedRegion&) = delete;
MappedRegion(MappedRegion&& other) noexcept;
MappedRegion& operator=(MappedRegion&& other) noexcept;
~MappedRegion();
void Reserve(std::uint64_t bytes);
// Maps [fileOffset, fileOffset + length) at Base() + Mapped().
void Map(const File& file, std::uint64_t fileOffset, std::uint64_t length, bool writable);
void ExcludeFromCoreDumps();
void WillNeed(std::uint64_t length);
void Release() noexcept;
std::byte* Base() const noexcept { return base; }
std::uint64_t Mapped() const noexcept { return mapped; }
std::uint64_t Reserved() const noexcept { return reserved; }
private:
std::byte* base = nullptr;
std::uint64_t reserved = 0;
std::uint64_t mapped = 0;
bool dontDump = false;
};
// A plain shared mapping of a few pages (the header).
class MappedPages {
public:
MappedPages() = default;
MappedPages(const MappedPages&) = delete;
MappedPages& operator=(const MappedPages&) = delete;
MappedPages(MappedPages&& other) noexcept;
MappedPages& operator=(MappedPages&& other) noexcept;
~MappedPages();
void Map(const File& file, std::uint64_t fileOffset, std::uint64_t length, bool writable);
void Sync();
void Release() noexcept;
std::byte* Data() const noexcept { return data; }
std::uint64_t Length() const noexcept { return length; }
private:
std::byte* data = nullptr;
std::uint64_t length = 0;
};
namespace Os {
// mkdir -p with an explicit mode for every created component, so a
// private cache directory is never world-readable, not even briefly.
void CreateDirectories(const std::filesystem::path& directory, std::filesystem::perms permissions);
void Rename(const std::filesystem::path& from, const std::filesystem::path& to);
// Atomic rename that fails instead of replacing; false when `to` exists.
bool RenameNoReplace(const std::filesystem::path& from, const std::filesystem::path& to);
void HardLink(const std::filesystem::path& from, const std::filesystem::path& to);
bool Remove(const std::filesystem::path& path) noexcept;
void SyncDirectory(const std::filesystem::path& directory);
void RandomBytes(std::span<std::byte> out);
std::uint64_t UnixNanos() noexcept;
std::uint64_t PageSize() noexcept;
std::int64_t ProcessId() noexcept;
}
}