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:
Jorijn van der Graaf 2026-10-17 04:16:51 +02:00
commit bfd0cc261b
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
34 changed files with 7506 additions and 0 deletions

View file

@ -0,0 +1,98 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// One writer per file: a second writer in another process or in this one
// gets LockedError, readers are refused while a writer holds the file,
// readers coexist with each other, and a read-only table refuses writes.
#include <sys/wait.h>
#include <unistd.h>
import Crafter.DB;
import std;
using namespace Crafter;
namespace fs = std::filesystem;
namespace {
int failures = 0;
void Check(bool condition, std::string_view what) {
if (!condition) {
std::println("FAIL: {}", what);
++failures;
}
}
struct Item {
std::string name;
static constexpr std::uint32_t version = 1;
};
// Returns the child's exit code: 0 = opened fine, 3 = LockedError, 4 = other.
int ChildOpens(const fs::path& path, bool readOnly) {
const pid_t pid = fork();
if (pid == 0) {
try {
DB::Table<Item> table(path, { .mode = DB::OpenMode::OpenExisting, .readOnly = readOnly });
_exit(0);
} catch (const DB::LockedError&) {
_exit(3);
} catch (...) {
_exit(4);
}
}
int status = 0;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
}
int main() {
const fs::path dir = fs::temp_directory_path() / "crafter-db-lock";
fs::remove_all(dir);
const fs::path path = dir / "items.cdb";
{
DB::Table<Item> writer(path);
writer.Append({ "one" });
Check(ChildOpens(path, false) == 3, "second writer in another process gets LockedError");
Check(ChildOpens(path, true) == 3, "reader in another process is refused while a writer holds the file");
bool threw = false;
try {
DB::Table<Item> second(path);
} catch (const DB::LockedError&) {
threw = true;
}
Check(threw, "second Table on the same file in this process gets LockedError");
}
{
DB::Table<Item> reader(path, { .readOnly = true });
Check(ChildOpens(path, true) == 0, "readers coexist across processes");
Check(ChildOpens(path, false) == 3, "writer is refused while a reader holds the file");
bool threw = false;
try {
reader.Append({ "two" });
} catch (const DB::ReadOnlyError&) {
threw = true;
}
Check(threw, "Append on a read-only table throws ReadOnlyError");
threw = false;
try {
reader.Erase(0);
} catch (const DB::ReadOnlyError&) {
threw = true;
}
Check(threw, "Erase on a read-only table throws ReadOnlyError");
Check(reader.Size() == 1 && reader[0][&Item::name] == "one", "read-only table reads");
}
Check(ChildOpens(path, false) == 0, "writer opens once everyone is gone");
fs::remove_all(dir);
if (failures == 0) std::println("ok");
return failures == 0 ? 0 : 1;
}