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
187
tests/ShouldFormatCustomTypes/main.cpp
Normal file
187
tests/ShouldFormatCustomTypes/main.cpp
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Every built-in member shape and both user formatter shapes round-trip:
|
||||
// fixed (a class stored as basis points), variable-length inheriting from the
|
||||
// std::string formatter, optionals of scalars and strings, nested aggregates
|
||||
// with strings, scalar arrays, byte arrays, FixedString, chrono durations,
|
||||
// bool, double, std::byte and blobs.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class Percent {
|
||||
public:
|
||||
Percent() = default;
|
||||
explicit Percent(double value) : basisPoints(static_cast<std::int32_t>(std::lround(value * 100))) {}
|
||||
double Value() const noexcept { return basisPoints / 100.0; }
|
||||
std::int32_t BasisPoints() const noexcept { return basisPoints; }
|
||||
friend bool operator==(const Percent&, const Percent&) = default;
|
||||
|
||||
private:
|
||||
std::int32_t basisPoints = 0;
|
||||
};
|
||||
|
||||
class Tags {
|
||||
public:
|
||||
Tags() = default;
|
||||
explicit Tags(std::vector<std::string> values) : values(std::move(values)) {}
|
||||
const std::vector<std::string>& Values() const noexcept { return values; }
|
||||
std::string Joined() const {
|
||||
std::string out;
|
||||
for (const std::string& v : values) {
|
||||
if (!out.empty()) out += ',';
|
||||
out += v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static Tags Split(std::string_view text) {
|
||||
std::vector<std::string> values;
|
||||
for (const auto part : std::views::split(text, ',')) {
|
||||
if (!part.empty()) values.emplace_back(part.begin(), part.end());
|
||||
}
|
||||
return Tags(std::move(values));
|
||||
}
|
||||
friend bool operator==(const Tags&, const Tags&) = default;
|
||||
|
||||
private:
|
||||
std::vector<std::string> values;
|
||||
};
|
||||
}
|
||||
|
||||
template<>
|
||||
struct Crafter::DB::Formatter<Percent> {
|
||||
using Stored = std::int32_t;
|
||||
static Stored Encode(const Percent& value) noexcept { return value.BasisPoints(); }
|
||||
static Percent Decode(const Stored& stored) noexcept { return Percent(stored / 100.0); }
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Crafter::DB::Formatter<Tags> : Crafter::DB::Formatter<std::string> {
|
||||
static Var Encode(const Tags& value, HeapWriter& heap) { return Formatter<std::string>::Encode(value.Joined(), heap); }
|
||||
static Tags Materialize(StringRef view) { return Tags::Split(view.View()); }
|
||||
};
|
||||
|
||||
namespace {
|
||||
enum class Currency : std::uint16_t { Euro, Dollar };
|
||||
|
||||
struct Money {
|
||||
std::int64_t minor;
|
||||
Currency currency;
|
||||
friend bool operator==(const Money&, const Money&) = default;
|
||||
};
|
||||
|
||||
struct Party {
|
||||
std::string name;
|
||||
DB::FixedString<11> bic;
|
||||
std::optional<std::string> note;
|
||||
friend bool operator==(const Party&, const Party&) = default;
|
||||
};
|
||||
|
||||
struct Everything {
|
||||
bool flag;
|
||||
std::byte raw;
|
||||
double ratio;
|
||||
std::chrono::milliseconds delay;
|
||||
std::chrono::duration<double> seconds;
|
||||
Percent fee;
|
||||
Tags tags;
|
||||
std::optional<std::int32_t> maybe;
|
||||
std::optional<std::string> maybeText;
|
||||
Money money;
|
||||
Party party;
|
||||
std::array<std::uint16_t, 3> triple;
|
||||
std::array<char, 4> code;
|
||||
DB::FixedString<34> iban;
|
||||
std::vector<std::uint32_t> ids;
|
||||
|
||||
static constexpr std::uint32_t version = 7;
|
||||
friend bool operator==(const Everything&, const Everything&) = default;
|
||||
};
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path dir = fs::temp_directory_path() / "crafter-db-formatters";
|
||||
fs::remove_all(dir);
|
||||
const fs::path path = dir / "e.cdb";
|
||||
|
||||
const Everything a{ true, std::byte{ 7 }, 0.25, std::chrono::milliseconds(1500), std::chrono::duration<double>(2.5), Percent(12.5), Tags({ "food", "monthly" }), 42, std::string("present"), { -1234, Currency::Dollar }, { "Shop", DB::FixedString<11>("ABNANL2AXXX"), std::string("vip customer with a long note") }, { 1, 2, 3 }, { 'E', 'U', 'R', '\0' }, DB::FixedString<34>("NL91ABNA0417164300"), { 10, 20, 30, 40, 50 } };
|
||||
const Everything b{ false, std::byte{ 0 }, -1.5, std::chrono::milliseconds(0), std::chrono::duration<double>(0), Percent(0), Tags(std::vector<std::string>{}), std::nullopt, std::nullopt, { 0, Currency::Euro }, { "", DB::FixedString<11>(""), std::nullopt }, { 0, 0, 0 }, { 'U', 'S', 'D', '\0' }, DB::FixedString<34>(""), {} };
|
||||
|
||||
{
|
||||
DB::Table<Everything> table(path);
|
||||
table.Append(a);
|
||||
table.Append(b);
|
||||
|
||||
const DB::Row<Everything> ra = table[0];
|
||||
Check(ra[&Everything::flag] == true, "bool");
|
||||
Check(ra[&Everything::raw] == std::byte{ 7 }, "std::byte");
|
||||
Check(ra[&Everything::ratio] == 0.25, "double");
|
||||
Check(ra[&Everything::delay] == std::chrono::milliseconds(1500), "duration");
|
||||
Check(ra[&Everything::seconds] == std::chrono::duration<double>(2.5), "floating duration");
|
||||
static_assert(DB::Layout<Everything>::columns[DB::Layout<Everything>::Column(DB::LeafStart<Everything, 4>())].kind == DB::Kind::Float64, "a floating chrono rep keeps its Float kind");
|
||||
static_assert(DB::Layout<Everything>::columns[DB::Layout<Everything>::Column(DB::LeafStart<Everything, 4>())].paramDen == 1, "but records its period");
|
||||
Check(ra[&Everything::fee] == Percent(12.5), "fixed user formatter decodes by value");
|
||||
Check(ra[&Everything::tags] == "food,monthly", "var user formatter view is the raw encoding");
|
||||
Check(ra.Get<&Everything::maybe>() == std::optional<std::int32_t>(42), "optional scalar engaged");
|
||||
Check(ra[&Everything::maybeText].has_value() && *ra[&Everything::maybeText] == "present", "optional string engaged (view)");
|
||||
Check(ra[&Everything::money] == Money{ -1234, Currency::Dollar }, "nested aggregate by value");
|
||||
const Party party = ra[&Everything::party];
|
||||
Check(party == a.party, "nested aggregate with strings, FixedString and optional");
|
||||
Check(ra[&Everything::triple] == std::array<std::uint16_t, 3>{ 1, 2, 3 }, "scalar array");
|
||||
Check(ra[&Everything::code] == std::array<char, 4>{ 'E', 'U', 'R', '\0' }, "char array");
|
||||
Check(ra[&Everything::iban].View() == "NL91ABNA0417164300", "FixedString view");
|
||||
Check(std::span<const std::uint32_t>(ra[&Everything::ids]).size() == 5, "blob");
|
||||
|
||||
const DB::Row<Everything> rb = table[1];
|
||||
Check(!rb[&Everything::maybe].has_value(), "optional scalar empty");
|
||||
Check(!rb[&Everything::maybeText].has_value(), "optional string empty");
|
||||
Check(rb[&Everything::party].note == std::nullopt, "nested optional empty");
|
||||
Check(rb[&Everything::iban].View().empty(), "empty FixedString");
|
||||
|
||||
Check(table.Materialize() == std::vector<Everything>{ a, b }, "Materialize round trip of everything");
|
||||
|
||||
table.Set<&Everything::maybe>(1, std::optional<std::int32_t>(-9));
|
||||
Check(rb.Get<&Everything::maybe>() == std::optional<std::int32_t>(-9), "Set an optional");
|
||||
table.Set<&Everything::fee>(1, Percent(1.0));
|
||||
Check(rb[&Everything::fee] == Percent(1.0), "Set through a user formatter");
|
||||
table.Set<&Everything::party>(1, Party{ "Changed", DB::FixedString<11>("RABONL2U"), std::nullopt });
|
||||
Check(rb[&Everything::party].name == "Changed", "Set a nested aggregate");
|
||||
|
||||
const std::string shown = std::format("{}", ra);
|
||||
Check(shown.find("fee=") != std::string::npos && shown.find("money={minor=-1234, currency=1}") != std::string::npos && shown.find("maybeText=\"present\"") != std::string::npos, std::format("formatter: {}", shown));
|
||||
}
|
||||
|
||||
{
|
||||
const DB::Table<Everything> table(path, { .readOnly = true });
|
||||
Check(table[0].Materialize() == a, "row a survives reopen");
|
||||
const Everything bb = table[1].Materialize();
|
||||
Check(bb.maybe == -9 && bb.fee == Percent(1.0) && bb.party.name == "Changed", "updated row b survives reopen");
|
||||
const DB::Inspection info = DB::TableFile::Inspect(path);
|
||||
bool found = false;
|
||||
for (std::size_t i = 0; i < info.schema.columns.size(); i++) {
|
||||
if (info.schema.Name(i) == "party.bic") found = info.schema.columns[i].kind == DB::Kind::FixedBytes && info.schema.columns[i].size == 11;
|
||||
}
|
||||
Check(found, "nested column 'party.bic' described as 11 fixed bytes");
|
||||
}
|
||||
|
||||
static_assert(DB::Layout<Everything>::columns[1].offset == 1 || DB::Layout<Everything>::columns[1].size == 1, "1-byte columns follow the flags byte");
|
||||
static_assert(DB::Layout<Everything>::rowAlign == 8);
|
||||
|
||||
fs::remove_all(dir);
|
||||
if (failures == 0) std::println("ok");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue