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

171
tests/ShouldQuery/main.cpp Normal file
View file

@ -0,0 +1,171 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Queries: lambda filters with Key and string_view, column expressions,
// binary search on the ordered column (Range/From/After/Until/Before,
// Lower/UpperBound at the boundaries, equal keys, tombstoned slots), chaining
// Range().Where(), bound extraction from a combined expression, ranges
// composition, and the OrderError checks on Update/Set.
import Crafter.DB;
import std;
using namespace Crafter;
namespace fs = std::filesystem;
using TimePoint = std::chrono::system_clock::time_point;
namespace {
int failures = 0;
void Check(bool condition, std::string_view what) {
if (!condition) {
std::println("FAIL: {}", what);
++failures;
}
}
template<typename T>
void CheckEqual(const T& actual, const T& expected, std::string_view what) {
if (!(actual == expected)) {
std::println("FAIL: {} — expected '{}', got '{}'", what, expected, actual);
++failures;
}
}
enum class Currency : std::uint16_t { Euro };
struct Transaction {
std::string name;
std::string bic;
std::string iban;
std::string description;
Currency currency;
std::int64_t amount;
TimePoint timePoint;
static constexpr std::uint32_t version = 1;
static constexpr auto orderedBy = &Transaction::timePoint;
};
inline constexpr DB::Column<&Transaction::iban> iban{};
inline constexpr DB::Column<&Transaction::description> description{};
inline constexpr DB::Column<&Transaction::amount> amount{};
inline constexpr DB::Column<&Transaction::timePoint> timePoint{};
TimePoint At(std::int64_t seconds) {
return TimePoint(std::chrono::duration_cast<TimePoint::duration>(std::chrono::seconds(seconds)));
}
std::vector<std::uint64_t> Ids(auto&& query) {
std::vector<std::uint64_t> ids;
for (auto row : query) ids.push_back(row.Id());
return ids;
}
template<class Query>
std::vector<std::uint64_t> IdsOf(Query query) {
std::vector<std::uint64_t> ids;
for (auto row : query) ids.push_back(row.Id());
return ids;
}
}
int main() {
const fs::path dir = fs::temp_directory_path() / "crafter-db-query";
fs::remove_all(dir);
DB::Table<Transaction> table(dir / "q.cdb");
const std::string_view ibanA = "NL91ABNA0417164300";
const std::string_view ibanB = "NL91ABNA0417164301"; // same length, same 4-byte prefix
const std::string_view ibanC = "DE89370400440532013000";
// id key
table.Append({ "a", "ABNANL2A", std::string(ibanA), "rent", Currency::Euro, -100000, At(10) }); // 0
table.Append({ "b", "ABNANL2A", std::string(ibanB), "rent", Currency::Euro, -200, At(20) }); // 1
table.Append({ "c", "RABONL2U", std::string(ibanC), "coffee", Currency::Euro, -350, At(20) }); // 2 equal key
table.Append({ "d", "RABONL2U", std::string(ibanA), "salary", Currency::Euro, 300000, At(30) }); // 3
table.Append({ "e", "INGBNL2A", std::string(ibanC), "rent ", Currency::Euro, -100000, At(40) }); // 4 trailing space: same length as "rent" + 1
table.Append({ "f", "INGBNL2A", std::string(ibanA), "RENT", Currency::Euro, -5, At(50) }); // 5
// --- lambda filters ---------------------------------------------------
const DB::Key keyA(ibanA);
CheckEqual(IdsOf(table.Where([&](DB::Row<Transaction> r) { return r[&Transaction::iban] == keyA; })), std::vector<std::uint64_t>{ 0, 3, 5 }, "lambda with Key");
CheckEqual(IdsOf(table.Where([&](DB::Row<Transaction> r) { return r[&Transaction::iban] == ibanB; })), std::vector<std::uint64_t>{ 1 }, "lambda with string_view, prefix-equal neighbour rejected");
CheckEqual(IdsOf(table.Where([](DB::Row<Transaction> r) { return r[&Transaction::description] == "rent"; })), std::vector<std::uint64_t>{ 0, 1 }, "description equality is exact (case and length)");
CheckEqual(table.Where([](DB::Row<Transaction> r) { return r[&Transaction::amount] < 0; }).Count(), std::uint64_t(5), "Count");
Check(table.Where(amount > 0).First().has_value() && table.Where(amount > 0).First()->Id() == 3, "First");
Check(!table.Where(amount > 1'000'000).Any(), "Any on no match");
// --- column expressions -------------------------------------------------
CheckEqual(IdsOf(table.Where(iban == ibanA)), std::vector<std::uint64_t>{ 0, 3, 5 }, "expression ==");
CheckEqual(IdsOf(table.Where(iban != ibanA)), std::vector<std::uint64_t>{ 1, 2, 4 }, "expression !=");
CheckEqual(IdsOf(table.Where(iban == ibanA && amount > 0)), std::vector<std::uint64_t>{ 3 }, "expression &&");
CheckEqual(IdsOf(table.Where(description == "coffee" || description == "salary")), std::vector<std::uint64_t>{ 2, 3 }, "expression ||");
CheckEqual(IdsOf(table.Where(!(amount < 0))), std::vector<std::uint64_t>{ 3 }, "expression !");
CheckEqual(IdsOf(table.Where(description < "d")), std::vector<std::uint64_t>{ 2, 5 }, "string ordering compare ('RENT' and 'coffee' sort before 'd')");
// --- ordered column -----------------------------------------------------
CheckEqual<std::uint64_t>(table.LowerBound<&Transaction::timePoint>(At(20)), 1, "LowerBound on equal keys");
CheckEqual<std::uint64_t>(table.UpperBound<&Transaction::timePoint>(At(20)), 3, "UpperBound on equal keys");
CheckEqual<std::uint64_t>(table.LowerBound<&Transaction::timePoint>(At(5)), 0, "LowerBound below all");
CheckEqual<std::uint64_t>(table.LowerBound<&Transaction::timePoint>(At(55)), 6, "LowerBound above all");
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(40))), std::vector<std::uint64_t>{ 1, 2, 3, 4 }, "Range inclusive both ends");
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(21), At(29))), std::vector<std::uint64_t>{}, "Range with no rows");
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(40), At(20))), std::vector<std::uint64_t>{}, "Range with inverted bounds is empty");
CheckEqual(IdsOf(table.From<&Transaction::timePoint>(At(30))), std::vector<std::uint64_t>{ 3, 4, 5 }, "From");
CheckEqual(IdsOf(table.After<&Transaction::timePoint>(At(30))), std::vector<std::uint64_t>{ 4, 5 }, "After");
CheckEqual(IdsOf(table.Until<&Transaction::timePoint>(At(20))), std::vector<std::uint64_t>{ 0, 1, 2 }, "Until");
CheckEqual(IdsOf(table.Before<&Transaction::timePoint>(At(20))), std::vector<std::uint64_t>{ 0 }, "Before");
// --- range + property ---------------------------------------------------
const std::vector<std::uint64_t> chained = IdsOf(table.Range<&Transaction::timePoint>(At(20), At(50)).Where(iban == ibanA));
CheckEqual(chained, std::vector<std::uint64_t>{ 3, 5 }, "Range().Where(expression)");
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(50)).Where([&](DB::Row<Transaction> r) { return r[&Transaction::iban] == keyA; })), chained, "Range().Where(lambda)");
CheckEqual(IdsOf(table.Where(timePoint >= At(20) && timePoint <= At(50) && iban == ibanA)), chained, "combined expression extracts bounds");
CheckEqual(IdsOf(table.Where(timePoint > At(20) && timePoint < At(50))), std::vector<std::uint64_t>{ 3, 4 }, "exclusive bounds");
CheckEqual(IdsOf(table.Where(timePoint == At(20))), std::vector<std::uint64_t>{ 1, 2 }, "equality bound");
CheckEqual(IdsOf(table.Where(timePoint == At(20) || iban == ibanA)), std::vector<std::uint64_t>{ 0, 1, 2, 3, 5 }, "|| disables bound extraction but stays correct");
// --- ranges composition -------------------------------------------------
std::uint64_t taken = 0;
for (DB::Row<Transaction> row : table | std::views::filter([](DB::Row<Transaction> r) { return r[&Transaction::amount] < 0; }) | std::views::take(2)) taken += row.Id() + 1;
CheckEqual<std::uint64_t>(taken, 1 + 2, "views::filter | views::take over the table");
const std::int64_t total = std::ranges::fold_left(table.Select<&Transaction::amount>(), std::int64_t(0), std::plus{});
CheckEqual<std::int64_t>(total, -100000 - 200 - 350 + 300000 - 100000 - 5, "Select + fold_left");
CheckEqual<std::size_t>(table.Slots().size(), 6, "Slots size");
Check(std::ranges::lower_bound(table.Slots(), At(30), {}, [](DB::Row<Transaction> r) { return r[&Transaction::timePoint]; }) - table.Slots().begin() == 3, "std::ranges::lower_bound over Slots()");
// --- tombstones in the search -------------------------------------------
table.Erase(2);
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(20))), std::vector<std::uint64_t>{ 1 }, "Range skips tombstones but still finds neighbours");
CheckEqual<std::uint64_t>(table.UpperBound<&Transaction::timePoint>(At(20)), 3, "UpperBound unchanged by a tombstone (keys survive)");
CheckEqual(IdsOf(table.Where(timePoint == At(20))), std::vector<std::uint64_t>{ 1 }, "bound extraction with a tombstone");
// --- order checks on Update/Set -----------------------------------------
bool threw = false;
try {
table.Update(3, { "d", "RABONL2U", std::string(ibanA), "salary", Currency::Euro, 300000, At(15) }); // would sort before row 1/2
} catch (const DB::OrderError&) {
threw = true;
}
Check(threw, "Update that breaks the order throws");
threw = false;
try {
table.Set<&Transaction::timePoint>(3, At(45)); // would sort after row 4
} catch (const DB::OrderError&) {
threw = true;
}
Check(threw, "Set that breaks the order throws");
table.Set<&Transaction::timePoint>(3, At(35));
Check(table[3][&Transaction::timePoint] == At(35), "Set within the neighbours succeeds");
table.Set<&Transaction::description>(3, "wage");
Check(table[3][&Transaction::description] == "wage", "Set a string column");
CheckEqual<std::uint64_t>(table.Stats().heapGarbage, 22, "short strings leave no heap garbage (only the erased row's 22-byte iban is garbage)");
table.Set<&Transaction::description>(3, "a description longer than twelve");
Check(table[3][&Transaction::description] == "a description longer than twelve", "Set a long string column");
table.Set<&Transaction::description>(3, "short");
CheckEqual<std::uint64_t>(table.Stats().heapGarbage, 22 + 32, "replacing a heap value orphans its bytes");
fs::remove_all(dir);
if (failures == 0) std::println("ok");
return failures == 0 ? 0 : 1;
}