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
174
interfaces/Crafter.DB-Reflect.cppm
Normal file
174
interfaces/Crafter.DB-Reflect.cppm
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Compile-time introspection of aggregates without P2996 (clang 23 has no
|
||||
// <meta>). Three ingredients, all verified on the toolchain this library
|
||||
// targets: structured-binding packs give member count and per-member access,
|
||||
// std::source_location::function_name() leaks the member name when a pointer
|
||||
// to a member of an extern object is used as a template argument, and address
|
||||
// comparison on a constexpr instance maps a pointer-to-member to its index.
|
||||
// When a clang ships <meta>, this partition is the only thing to replace.
|
||||
export module Crafter.DB:Reflect;
|
||||
import std;
|
||||
|
||||
namespace Crafter::DB::Reflect {
|
||||
template<class T>
|
||||
struct CountHelper {
|
||||
template<class U>
|
||||
static consteval auto Count(U& u) {
|
||||
auto&& [...m] = u;
|
||||
return std::integral_constant<std::size_t, sizeof...(m)>{};
|
||||
}
|
||||
static constexpr std::size_t value = decltype(Count(std::declval<T&>()))::value;
|
||||
};
|
||||
|
||||
// A constant-initialised instance whose member addresses are turned into
|
||||
// text by function_name(). Only its address is ever used, in a constant
|
||||
// expression, so nothing reads or writes it at run time; no_destroy keeps
|
||||
// the compiler from registering a destructor for it, which lets the linker
|
||||
// drop the object entirely. (An undefined `extern` declaration would be
|
||||
// cheaper still, but clang warns about it at every instantiation.)
|
||||
template<class T>
|
||||
[[clang::no_destroy]] inline const T external{};
|
||||
|
||||
template<auto P>
|
||||
consteval std::string_view RawName() {
|
||||
return std::source_location::current().function_name();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
consteval std::string_view RawTypeName() {
|
||||
return std::source_location::current().function_name();
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Crafter::DB {
|
||||
// True for class types whose non-static data members can be bound by a
|
||||
// structured binding: public members, no bases carrying members, no
|
||||
// user-declared constructors. Tables and nested column groups must be this.
|
||||
template<class T>
|
||||
concept Aggregate = std::is_aggregate_v<T> && !std::is_array_v<T> && requires(T& t) { Reflect::CountHelper<T>::value; };
|
||||
|
||||
template<Aggregate T>
|
||||
inline constexpr std::size_t memberCount = Reflect::CountHelper<T>::value;
|
||||
|
||||
// Reference to the I-th member of an aggregate lvalue.
|
||||
template<std::size_t I, class T>
|
||||
constexpr decltype(auto) Member(T&& t) noexcept {
|
||||
auto&& [...m] = t;
|
||||
return (m...[I]);
|
||||
}
|
||||
|
||||
template<std::size_t I, class T>
|
||||
using MemberType = std::remove_cvref_t<decltype(Member<I>(std::declval<T&>()))>;
|
||||
|
||||
// The declared name of the I-th member, e.g. "iban". Parsed out of
|
||||
// "std::string_view Crafter::DB::Reflect::RawName() [P = &external.iban]".
|
||||
template<std::size_t I, Aggregate T>
|
||||
consteval std::string_view MemberName() {
|
||||
constexpr std::string_view raw = Reflect::RawName<&Member<I>(Reflect::external<T>)>();
|
||||
const std::size_t end = raw.rfind(']');
|
||||
const std::size_t dot = raw.rfind('.', end);
|
||||
return raw.substr(dot + 1, end - dot - 1);
|
||||
}
|
||||
|
||||
// The type's qualified name as the compiler spells it ("Nostro::Transaction").
|
||||
// Used in diagnostics only, so the exact spelling is not load-bearing.
|
||||
template<class T>
|
||||
consteval std::string_view TypeName() {
|
||||
constexpr std::string_view raw = Reflect::RawTypeName<T>();
|
||||
const std::size_t end = raw.rfind(']');
|
||||
const std::size_t eq = raw.rfind("T = ", end);
|
||||
return raw.substr(eq + 4, end - eq - 4);
|
||||
}
|
||||
|
||||
template<class Ptr>
|
||||
struct MemberPointerTraits;
|
||||
|
||||
template<class C, class M>
|
||||
struct MemberPointerTraits<M C::*> {
|
||||
using Class = C;
|
||||
using Type = M;
|
||||
};
|
||||
|
||||
template<auto Ptr>
|
||||
using MemberClass = typename MemberPointerTraits<decltype(Ptr)>::Class;
|
||||
|
||||
template<auto Ptr>
|
||||
using MemberTypeOf = typename MemberPointerTraits<decltype(Ptr)>::Type;
|
||||
|
||||
inline constexpr std::size_t noMember = static_cast<std::size_t>(-1);
|
||||
|
||||
// Equality of two pointers-to-member that may point into different member
|
||||
// types (comparing those directly is ill-formed): false unless the types
|
||||
// match and the pointers are equal.
|
||||
template<auto A, auto B>
|
||||
consteval bool SameMemberPointer() {
|
||||
if constexpr (std::is_same_v<std::remove_cv_t<decltype(A)>, std::remove_cv_t<decltype(B)>>) return A == B;
|
||||
else return false;
|
||||
}
|
||||
|
||||
// Index of the member a pointer-to-member designates, or noMember. Works
|
||||
// by comparing the address of each bound member with the address the
|
||||
// pointer selects on the same constexpr instance.
|
||||
template<Aggregate T, auto Ptr>
|
||||
consteval std::size_t MemberIndex() {
|
||||
T t{};
|
||||
std::size_t index = noMember;
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
((void)[&] {
|
||||
if constexpr (std::is_same_v<MemberType<I, T>, std::remove_cvref_t<decltype(t.*Ptr)>>) {
|
||||
if (std::addressof(Member<I>(t)) == std::addressof(t.*Ptr)) index = I;
|
||||
}
|
||||
}(), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
return index;
|
||||
}
|
||||
|
||||
// Runtime counterpart of MemberIndex for a pointer that is not a template
|
||||
// argument. Every call site passes a literal `&T::member`, and the probe
|
||||
// object is constant-initialised (no guard to check), so after inlining the
|
||||
// comparison chain folds to a constant.
|
||||
template<Aggregate T, class M>
|
||||
std::size_t MemberIndexOf(M T::* ptr) noexcept {
|
||||
const T& instance = Reflect::external<T>;
|
||||
std::size_t index = noMember;
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
((void)[&] {
|
||||
if constexpr (std::is_same_v<MemberType<I, T>, M>) {
|
||||
if (std::addressof(Member<I>(instance)) == std::addressof(instance.*ptr)) index = I;
|
||||
}
|
||||
}(), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
return index;
|
||||
}
|
||||
|
||||
// Calls f.template operator()<I>(member) for every member, in order.
|
||||
template<Aggregate T, class F>
|
||||
constexpr void ForEachMember(T& t, F&& f) {
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(f.template operator()<I>(Member<I>(t)), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
}
|
||||
}
|
||||
|
||||
// Self-tests: a compiler whose function_name() text changes shape fails here,
|
||||
// at the library's build, instead of producing garbage column names.
|
||||
namespace Crafter::DB::Reflect {
|
||||
struct Probe {
|
||||
std::int32_t alpha;
|
||||
double beta;
|
||||
std::string gamma;
|
||||
std::chrono::system_clock::time_point delta;
|
||||
};
|
||||
static_assert(Aggregate<Probe>);
|
||||
static_assert(memberCount<Probe> == 4);
|
||||
static_assert(MemberName<0, Probe>() == "alpha");
|
||||
static_assert(MemberName<2, Probe>() == "gamma");
|
||||
static_assert(MemberName<3, Probe>() == "delta");
|
||||
static_assert(MemberIndex<Probe, &Probe::gamma>() == 2);
|
||||
static_assert(MemberIndex<Probe, &Probe::delta>() == 3);
|
||||
static_assert(std::is_same_v<MemberType<1, Probe>, double>);
|
||||
static_assert(std::is_same_v<MemberTypeOf<&Probe::gamma>, std::string>);
|
||||
static_assert(TypeName<Probe>().ends_with("Probe"));
|
||||
}
|
||||
Loading…
Reference in a new issue