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
512
interfaces/Crafter.DB-Schema.cppm
Normal file
512
interfaces/Crafter.DB-Schema.cppm
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// From a C++ struct to a row layout, entirely at compile time: members are
|
||||
// classified by Shape, nested aggregates are flattened into dotted column
|
||||
// names, columns are placed by ascending alignment after the flags byte at
|
||||
// offset 0, and the result is hashed into the schema fingerprint the file
|
||||
// header carries. The same partition holds the table-level declarations
|
||||
// (version, orderedBy, Previous, Migrate) and the diagnostics for misuse.
|
||||
export module Crafter.DB:Schema;
|
||||
import std;
|
||||
import :Reflect;
|
||||
import :Format;
|
||||
import :Formatter;
|
||||
|
||||
export namespace Crafter::DB {
|
||||
enum class Shape : std::uint8_t { Scalar, FixedBytes, ScalarArray, Optional, Fixed, Var, Aggregate, Pointer, Unsupported };
|
||||
|
||||
template<class T> struct OptionalTraits : std::false_type {};
|
||||
template<class M> struct OptionalTraits<std::optional<M>> : std::true_type { using Type = M; };
|
||||
|
||||
template<class T> struct StdArrayTraits : std::false_type {};
|
||||
template<class E, std::size_t N> struct StdArrayTraits<std::array<E, N>> : std::true_type {
|
||||
using Element = E;
|
||||
static constexpr std::size_t size = N;
|
||||
};
|
||||
|
||||
template<class T> struct FixedStringTraits : std::false_type {};
|
||||
template<std::size_t N> struct FixedStringTraits<FixedString<N>> : std::true_type { static constexpr std::size_t size = N; };
|
||||
|
||||
template<class T>
|
||||
consteval Shape ShapeOf() {
|
||||
if constexpr (std::is_pointer_v<T> || std::is_member_pointer_v<T> || std::is_reference_v<T> || std::is_null_pointer_v<T>) return Shape::Pointer;
|
||||
else if constexpr (Scalar<T>) return Shape::Scalar;
|
||||
else if constexpr (FixedStringTraits<T>::value) return Shape::FixedBytes;
|
||||
else if constexpr (StdArrayTraits<T>::value) {
|
||||
if constexpr (ByteLike<typename StdArrayTraits<T>::Element>) return Shape::FixedBytes;
|
||||
else if constexpr (Scalar<typename StdArrayTraits<T>::Element>) return Shape::ScalarArray;
|
||||
else return Shape::Unsupported;
|
||||
}
|
||||
else if constexpr (OptionalTraits<T>::value) return Shape::Optional;
|
||||
else if constexpr (VarFormatter<T>) return Shape::Var;
|
||||
else if constexpr (FixedFormatter<T>) return Shape::Fixed;
|
||||
else if constexpr (Aggregate<T>) return Shape::Aggregate;
|
||||
else return Shape::Unsupported;
|
||||
}
|
||||
|
||||
// Number of row columns a member of type T occupies.
|
||||
template<class T>
|
||||
consteval std::size_t LeafCount() {
|
||||
constexpr Shape shape = ShapeOf<T>();
|
||||
if constexpr (shape == Shape::Scalar || shape == Shape::FixedBytes || shape == Shape::Var) return 1;
|
||||
else if constexpr (shape == Shape::ScalarArray) return StdArrayTraits<T>::size;
|
||||
else if constexpr (shape == Shape::Optional) return 1 + LeafCount<typename OptionalTraits<T>::Type>();
|
||||
else if constexpr (shape == Shape::Fixed) return LeafCount<typename Formatter<T>::Stored>();
|
||||
else if constexpr (shape == Shape::Aggregate) {
|
||||
return []<std::size_t... I>(std::index_sequence<I...>) { return (std::size_t(0) + ... + LeafCount<MemberType<I, T>>()); }(std::make_index_sequence<memberCount<T>>{});
|
||||
}
|
||||
else return 1; // unsupported shapes are rejected by CheckTable; count 1 keeps indices sane meanwhile
|
||||
}
|
||||
|
||||
// First leaf of member I of aggregate T.
|
||||
template<class T, std::size_t I>
|
||||
consteval std::size_t LeafStart() {
|
||||
return []<std::size_t... J>(std::index_sequence<J...>) { return (std::size_t(0) + ... + LeafCount<MemberType<J, T>>()); }(std::make_index_sequence<I>{});
|
||||
}
|
||||
|
||||
// A compile-time string with value semantics (std::string cannot cross
|
||||
// from consteval into a constexpr variable). Overflow is a build error,
|
||||
// never a silent truncation: a truncated column name would alias another
|
||||
// column, a truncated diagnostic would lose its hint.
|
||||
template<std::size_t Capacity>
|
||||
struct FixedText {
|
||||
std::array<char, Capacity> chars{};
|
||||
std::uint32_t length = 0;
|
||||
|
||||
constexpr void Append(std::string_view text) {
|
||||
if (length + text.size() > chars.size()) throw "Crafter.DB: a column name or diagnostic exceeds the fixed buffer; shorten the member path";
|
||||
for (const char c : text) chars[length++] = c;
|
||||
}
|
||||
|
||||
constexpr std::string_view View() const noexcept { return { chars.data(), length }; }
|
||||
constexpr std::size_t size() const noexcept { return length; }
|
||||
constexpr const char* data() const noexcept { return chars.data(); }
|
||||
};
|
||||
|
||||
using FixedName = FixedText<256>; // a dotted column path
|
||||
using MessageText = FixedText<1024>; // a static_assert message
|
||||
|
||||
inline constexpr std::uint32_t noLeaf = 0xFFFFFFFF;
|
||||
|
||||
struct LeafInfo {
|
||||
FixedName name;
|
||||
Kind kind = Kind::UInt8;
|
||||
std::uint8_t flags = 0;
|
||||
std::uint16_t size = 1;
|
||||
std::uint16_t align = 1;
|
||||
std::uint32_t paramNum = 0;
|
||||
std::uint32_t paramDen = 0;
|
||||
std::uint32_t variant = 0;
|
||||
std::uint32_t presenceLeaf = noLeaf;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
consteval void CollectLeaves(std::vector<LeafInfo>& out, const FixedName& name, std::uint32_t presenceLeaf) {
|
||||
constexpr Shape shape = ShapeOf<T>();
|
||||
const std::uint8_t optionalFlag = presenceLeaf == noLeaf ? 0 : columnFlagOptional;
|
||||
if constexpr (shape == Shape::Scalar) {
|
||||
LeafInfo leaf{ name, ScalarKind<T>(), optionalFlag, sizeof(T), alignof(T) };
|
||||
if constexpr (std::is_enum_v<T>) leaf.flags |= columnFlagEnum;
|
||||
leaf.presenceLeaf = presenceLeaf;
|
||||
out.push_back(leaf);
|
||||
} else if constexpr (shape == Shape::FixedBytes) {
|
||||
constexpr std::size_t n = [] {
|
||||
if constexpr (FixedStringTraits<T>::value) return FixedStringTraits<T>::size;
|
||||
else return StdArrayTraits<T>::size;
|
||||
}();
|
||||
static_assert(n > 0 && n <= 0xFFFF, "fixed byte columns must be 1..65535 bytes");
|
||||
LeafInfo leaf{ name, Kind::FixedBytes, optionalFlag, static_cast<std::uint16_t>(n), 1, static_cast<std::uint32_t>(n), 0 };
|
||||
leaf.presenceLeaf = presenceLeaf;
|
||||
out.push_back(leaf);
|
||||
} else if constexpr (shape == Shape::ScalarArray) {
|
||||
for (std::size_t i = 0; i < StdArrayTraits<T>::size; i++) {
|
||||
FixedName element = name;
|
||||
element.Append("[");
|
||||
char digits[20]{};
|
||||
std::size_t len = 0;
|
||||
std::size_t v = i;
|
||||
do { digits[len++] = static_cast<char>('0' + v % 10); v /= 10; } while (v != 0);
|
||||
for (std::size_t k = 0; k < len; k++) element.Append(std::string_view(&digits[len - 1 - k], 1));
|
||||
element.Append("]");
|
||||
CollectLeaves<typename StdArrayTraits<T>::Element>(out, element, presenceLeaf);
|
||||
}
|
||||
} else if constexpr (shape == Shape::Optional) {
|
||||
FixedName presence = name;
|
||||
presence.Append("?");
|
||||
LeafInfo leaf{ presence, Kind::UInt8, static_cast<std::uint8_t>(columnFlagPresence | optionalFlag), 1, 1 };
|
||||
leaf.presenceLeaf = presenceLeaf;
|
||||
const std::uint32_t index = static_cast<std::uint32_t>(out.size());
|
||||
out.push_back(leaf);
|
||||
CollectLeaves<typename OptionalTraits<T>::Type>(out, name, index);
|
||||
} else if constexpr (shape == Shape::Var) {
|
||||
LeafInfo leaf{ name, Kind::String, optionalFlag, sizeof(Var), alignof(Var) };
|
||||
if constexpr (HasFormatterKind<T>) leaf.kind = Formatter<T>::kind;
|
||||
if constexpr (HasFormatterPeriod<T>) {
|
||||
leaf.paramNum = Formatter<T>::paramNum;
|
||||
leaf.paramDen = Formatter<T>::paramDen;
|
||||
}
|
||||
if constexpr (HasFormatterVariant<T>) leaf.variant = Formatter<T>::variant;
|
||||
leaf.presenceLeaf = presenceLeaf;
|
||||
out.push_back(leaf);
|
||||
} else if constexpr (shape == Shape::Fixed) {
|
||||
const std::size_t first = out.size();
|
||||
using S = typename Formatter<T>::Stored;
|
||||
CollectLeaves<S>(out, name, presenceLeaf);
|
||||
if constexpr (HasFormatterKind<T>) {
|
||||
static_assert(LeafCount<S>() == 1, "a Formatter may only override `kind` when Stored is a single column");
|
||||
// A chrono type with a floating rep keeps its Float kind so the
|
||||
// bytes are read back as what they are; the period still records
|
||||
// that it is a time.
|
||||
if constexpr (std::is_integral_v<S>) out[first].kind = Formatter<T>::kind;
|
||||
}
|
||||
if constexpr (HasFormatterPeriod<T>) {
|
||||
out[first].paramNum = Formatter<T>::paramNum;
|
||||
out[first].paramDen = Formatter<T>::paramDen;
|
||||
}
|
||||
if constexpr (HasFormatterVariant<T>) out[first].variant = Formatter<T>::variant;
|
||||
} else if constexpr (shape == Shape::Aggregate) {
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
((void)[&] {
|
||||
FixedName member = name;
|
||||
if (member.length != 0) member.Append(".");
|
||||
member.Append(MemberName<I, T>());
|
||||
CollectLeaves<MemberType<I, T>>(out, member, presenceLeaf);
|
||||
}(), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
} else {
|
||||
LeafInfo leaf{ name, Kind::UInt8, optionalFlag, 1, 1 };
|
||||
out.push_back(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
consteval auto MakeLeaves() {
|
||||
std::vector<LeafInfo> leaves;
|
||||
CollectLeaves<T>(leaves, FixedName{}, noLeaf);
|
||||
std::array<LeafInfo, LeafCount<T>()> out{};
|
||||
for (std::size_t i = 0; i < out.size(); i++) out[i] = leaves[i];
|
||||
return out;
|
||||
}
|
||||
|
||||
// One placed column: a leaf with its row offset, or the flags byte.
|
||||
struct ColumnInfo {
|
||||
FixedName name;
|
||||
Kind kind = Kind::Flags;
|
||||
std::uint8_t flags = 0;
|
||||
std::uint16_t size = 1;
|
||||
std::uint16_t align = 1;
|
||||
std::uint16_t offset = 0;
|
||||
std::uint16_t presenceOffset = noColumn;
|
||||
std::uint32_t paramNum = 0;
|
||||
std::uint32_t paramDen = 0;
|
||||
std::uint32_t variant = 0;
|
||||
};
|
||||
|
||||
template<std::size_t LeafCountValue>
|
||||
struct Arrangement {
|
||||
std::array<ColumnInfo, LeafCountValue + 1> columns{};
|
||||
std::array<std::uint16_t, LeafCountValue> leafOffset{};
|
||||
std::array<std::uint16_t, LeafCountValue> leafColumn{};
|
||||
std::uint32_t rowSize = 0;
|
||||
std::uint16_t rowAlign = 1;
|
||||
};
|
||||
|
||||
template<std::size_t N>
|
||||
consteval Arrangement<N> Arrange(const std::array<LeafInfo, N>& leaves) {
|
||||
Arrangement<N> result;
|
||||
result.columns[0].name.Append("flags");
|
||||
std::array<std::size_t, N> order{};
|
||||
for (std::size_t i = 0; i < N; i++) order[i] = i;
|
||||
std::ranges::stable_sort(order, [&](std::size_t a, std::size_t b) { return leaves[a].align < leaves[b].align; });
|
||||
std::uint32_t cursor = 1;
|
||||
std::uint16_t rowAlign = 1;
|
||||
for (std::size_t k = 0; k < N; k++) {
|
||||
const LeafInfo& leaf = leaves[order[k]];
|
||||
const std::uint32_t align = leaf.align;
|
||||
cursor = (cursor + align - 1) / align * align;
|
||||
if (cursor + leaf.size > 0xFFFF) throw "a row must fit in 65535 bytes";
|
||||
ColumnInfo& column = result.columns[k + 1];
|
||||
column.name = leaf.name;
|
||||
column.kind = leaf.kind;
|
||||
column.flags = leaf.flags;
|
||||
column.size = leaf.size;
|
||||
column.align = leaf.align;
|
||||
column.offset = static_cast<std::uint16_t>(cursor);
|
||||
column.paramNum = leaf.paramNum;
|
||||
column.paramDen = leaf.paramDen;
|
||||
column.variant = leaf.variant;
|
||||
result.leafOffset[order[k]] = static_cast<std::uint16_t>(cursor);
|
||||
result.leafColumn[order[k]] = static_cast<std::uint16_t>(k + 1);
|
||||
cursor += leaf.size;
|
||||
if (leaf.align > rowAlign) rowAlign = leaf.align;
|
||||
}
|
||||
for (std::size_t k = 0; k < N; k++) {
|
||||
const LeafInfo& leaf = leaves[order[k]];
|
||||
if (leaf.presenceLeaf != noLeaf) result.columns[k + 1].presenceOffset = result.leafOffset[leaf.presenceLeaf];
|
||||
}
|
||||
result.rowAlign = rowAlign;
|
||||
result.rowSize = (cursor + rowAlign - 1) / rowAlign * rowAlign;
|
||||
return result;
|
||||
}
|
||||
|
||||
template<std::size_t N>
|
||||
consteval std::uint64_t FingerprintOf(const Arrangement<N>& arrangement, std::uint16_t orderedColumn) {
|
||||
std::uint64_t hash = Fnv1a(fnvOffset, "crafter-db-v1");
|
||||
hash = Fnv1a(hash, arrangement.rowSize);
|
||||
hash = Fnv1a(hash, arrangement.rowAlign);
|
||||
hash = Fnv1a(hash, orderedColumn);
|
||||
for (const ColumnInfo& column : arrangement.columns) {
|
||||
hash = Fnv1a(hash, column.name.View());
|
||||
hash = Fnv1a(hash, static_cast<std::uint64_t>(column.kind));
|
||||
hash = Fnv1a(hash, column.offset);
|
||||
hash = Fnv1a(hash, column.size);
|
||||
hash = Fnv1a(hash, column.flags);
|
||||
hash = Fnv1a(hash, column.paramNum);
|
||||
hash = Fnv1a(hash, column.paramDen);
|
||||
hash = Fnv1a(hash, column.variant);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
template<std::size_t N>
|
||||
consteval std::size_t NamePoolLength(const Arrangement<N>& arrangement) {
|
||||
std::size_t total = 0;
|
||||
for (const ColumnInfo& column : arrangement.columns) total += column.name.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
// --- table-level declarations ---------------------------------------------
|
||||
|
||||
template<class T>
|
||||
concept HasVersion = requires { { T::version } -> std::convertible_to<std::uint32_t>; } && std::is_integral_v<std::remove_cv_t<decltype(T::version)>>;
|
||||
|
||||
template<class T>
|
||||
concept HasOrderedBy = requires { T::orderedBy; } && std::is_member_object_pointer_v<std::remove_cv_t<decltype(T::orderedBy)>>;
|
||||
|
||||
template<class T>
|
||||
concept HasPrevious = requires { typename T::Previous; } && Aggregate<typename T::Previous>;
|
||||
|
||||
template<class T>
|
||||
concept HasMigrate = HasPrevious<T> && requires(typename T::Previous&& previous) { { T::Migrate(std::move(previous)) } -> std::same_as<T>; };
|
||||
|
||||
// What `orderedBy` may point at: a type whose on-disk column is a single
|
||||
// integer with a total order. Floats are out (NaN breaks the search),
|
||||
// strings and fixed byte arrays are out (no binary search over them).
|
||||
template<class M>
|
||||
concept OrderableKey = (std::is_integral_v<M> && !std::is_same_v<M, bool>) || std::is_enum_v<M>
|
||||
|| (FixedFormatter<M> && HasFormatterKind<M> && std::is_integral_v<typename Formatter<M>::Stored> && std::totally_ordered<M>);
|
||||
|
||||
// What a table struct must be. The diagnostics live in CheckTable.
|
||||
template<class T>
|
||||
concept TableType = Aggregate<T> && HasVersion<T> && std::is_default_constructible_v<T>;
|
||||
|
||||
// Everything the engine knows about T's row at compile time.
|
||||
template<Aggregate T>
|
||||
struct Layout {
|
||||
static constexpr std::size_t leafCount = LeafCount<T>();
|
||||
static constexpr std::size_t columnCount = leafCount + 1;
|
||||
static constexpr std::array<LeafInfo, leafCount> leaves = MakeLeaves<T>();
|
||||
static constexpr Arrangement<leafCount> arrangement = Arrange(leaves);
|
||||
static constexpr const std::array<ColumnInfo, columnCount>& columns = arrangement.columns;
|
||||
static constexpr std::uint32_t rowSize = arrangement.rowSize;
|
||||
static constexpr std::uint16_t rowAlign = arrangement.rowAlign;
|
||||
static_assert(columnCount <= 0xFFFF, "Crafter.DB: a table may have at most 65535 columns");
|
||||
static_assert(NamePoolLength(arrangement) <= 0xFFFF, "Crafter.DB: the column names of a table may total at most 65535 bytes");
|
||||
|
||||
// Row offset of a leaf (leaf indices follow declaration order, depth first).
|
||||
static constexpr std::uint16_t Offset(std::size_t leaf) noexcept { return arrangement.leafOffset[leaf]; }
|
||||
static constexpr std::uint16_t Column(std::size_t leaf) noexcept { return arrangement.leafColumn[leaf]; }
|
||||
|
||||
static constexpr std::uint32_t version = [] {
|
||||
if constexpr (HasVersion<T>) return static_cast<std::uint32_t>(T::version);
|
||||
else return std::uint32_t(0);
|
||||
}();
|
||||
|
||||
static constexpr std::size_t orderedMember = [] {
|
||||
if constexpr (HasOrderedBy<T>) return MemberIndex<T, T::orderedBy>();
|
||||
else return noMember;
|
||||
}();
|
||||
|
||||
static constexpr std::uint16_t orderedColumn = [] {
|
||||
if constexpr (HasOrderedBy<T>) return Column(LeafStart<T, MemberIndex<T, T::orderedBy>()>());
|
||||
else return noColumn;
|
||||
}();
|
||||
|
||||
static constexpr std::uint64_t fingerprint = FingerprintOf(arrangement, orderedColumn);
|
||||
|
||||
static constexpr bool hasHeap = std::ranges::any_of(columns, [](const ColumnInfo& c) { return c.kind == Kind::String || c.kind == Kind::Blob; });
|
||||
};
|
||||
|
||||
// The file-side description of Layout<T>, byte-compatible with what
|
||||
// SchemaInfo::Parse reads back; MakeSchemaInfo<T>().Fingerprint() equals
|
||||
// Layout<T>::fingerprint by construction (a test pins this).
|
||||
template<Aggregate T>
|
||||
SchemaInfo MakeSchemaInfo() {
|
||||
SchemaInfo info;
|
||||
info.rowSize = Layout<T>::rowSize;
|
||||
info.rowAlign = Layout<T>::rowAlign;
|
||||
for (const ColumnInfo& column : Layout<T>::columns) {
|
||||
ColumnDescriptor d{};
|
||||
d.offset = column.offset;
|
||||
d.size = column.size;
|
||||
d.kind = column.kind;
|
||||
d.flags = column.flags;
|
||||
d.presenceOffset = column.presenceOffset;
|
||||
d.nameOffset = static_cast<std::uint16_t>(info.namePool.size());
|
||||
d.nameLength = static_cast<std::uint16_t>(column.name.length);
|
||||
d.paramNum = column.paramNum;
|
||||
d.paramDen = column.paramDen;
|
||||
d.variant = column.variant;
|
||||
info.namePool.append(column.name.View());
|
||||
info.columns.push_back(d);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// --- diagnostics ---------------------------------------------------------
|
||||
|
||||
consteval MessageText Message(std::initializer_list<std::string_view> parts) {
|
||||
MessageText message;
|
||||
for (const std::string_view part : parts) message.Append(part);
|
||||
return message;
|
||||
}
|
||||
|
||||
template<class T, std::size_t I>
|
||||
consteval MessageText MemberMessage(std::string_view problem, std::string_view hint) {
|
||||
return Message({ "Crafter.DB: member '", MemberName<I, T>(), "' of ", TypeName<T>(), " ", problem, " ", hint });
|
||||
}
|
||||
|
||||
template<class T>
|
||||
consteval bool CheckMembers();
|
||||
|
||||
template<class T, std::size_t I>
|
||||
consteval bool CheckMember() {
|
||||
using M = MemberType<I, T>;
|
||||
constexpr Shape shape = ShapeOf<M>();
|
||||
static_assert(shape != Shape::Pointer, MemberMessage<T, I>("is a pointer or reference and cannot be stored;", "store a RowId, an index or a blob instead."));
|
||||
static_assert(shape != Shape::Unsupported, MemberMessage<T, I>("has a type with no Crafter::DB::Formatter;", "specialise Crafter::DB::Formatter for it or make it an aggregate of storable members."));
|
||||
if constexpr (shape == Shape::Aggregate) return CheckMembers<M>();
|
||||
else if constexpr (shape == Shape::Optional) {
|
||||
using P = typename OptionalTraits<M>::Type;
|
||||
static_assert(ShapeOf<P>() != Shape::Pointer && ShapeOf<P>() != Shape::Unsupported, MemberMessage<T, I>("is a std::optional of an unsupported type;", "specialise Crafter::DB::Formatter for the payload type."));
|
||||
static_assert(ShapeOf<P>() != Shape::Optional, MemberMessage<T, I>("is a std::optional of a std::optional;", "nested optionals have no distinct column names; wrap the inner one in a struct."));
|
||||
if constexpr (ShapeOf<P>() == Shape::Aggregate) return CheckMembers<P>();
|
||||
} else if constexpr (shape == Shape::Fixed) {
|
||||
using S = typename Formatter<M>::Stored;
|
||||
static_assert(ShapeOf<S>() != Shape::Pointer && ShapeOf<S>() != Shape::Unsupported && ShapeOf<S>() != Shape::Var && ShapeOf<S>() != Shape::Optional, MemberMessage<T, I>("has a Formatter whose Stored type is not storable;", "Stored must be a scalar, enum, std::array or aggregate of those."));
|
||||
if constexpr (ShapeOf<S>() == Shape::Aggregate) return CheckMembers<S>();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
consteval bool CheckMembers() {
|
||||
return []<std::size_t... I>(std::index_sequence<I...>) { return (true && ... && CheckMember<T, I>()); }(std::make_index_sequence<memberCount<T>>{});
|
||||
}
|
||||
|
||||
// Instantiated by Table<T>; every rule a table struct must follow is
|
||||
// reported here with the table and member name in the message.
|
||||
template<class T>
|
||||
consteval bool CheckTable() {
|
||||
static_assert(Aggregate<T>, Message({ "Crafter.DB: ", TypeName<T>(), " must be an aggregate: public data members only, no base classes with members, no user-declared constructors. Replace constructors with designated initialisers; Table::Emplace builds rows from fields." }));
|
||||
static_assert(HasVersion<T>, Message({ "Crafter.DB: ", TypeName<T>(), " must declare `static constexpr std::uint32_t version = 1;`" }));
|
||||
static_assert(std::is_default_constructible_v<T>, Message({ "Crafter.DB: ", TypeName<T>(), " must be default constructible (rows are decoded into a value-initialised instance)." }));
|
||||
if constexpr (Aggregate<T>) {
|
||||
CheckMembers<T>();
|
||||
if constexpr (HasOrderedBy<T>) {
|
||||
static_assert(std::is_same_v<MemberClass<T::orderedBy>, T>, Message({ "Crafter.DB: ", TypeName<T>(), "::orderedBy must point to a member of ", TypeName<T>() }));
|
||||
static_assert(OrderableKey<MemberTypeOf<T::orderedBy>>, Message({ "Crafter.DB: ", TypeName<T>(), "::orderedBy must name an integer, enum or integral chrono member; floats (NaN has no order), strings, byte arrays and aggregates cannot be ordered." }));
|
||||
}
|
||||
if constexpr (HasPrevious<T>) {
|
||||
static_assert(HasVersion<typename T::Previous>, Message({ "Crafter.DB: ", TypeName<T>(), "::Previous must itself declare `version`." }));
|
||||
static_assert(Layout<typename T::Previous>::version < Layout<T>::version, Message({ "Crafter.DB: ", TypeName<T>(), "::Previous must have a lower version than ", TypeName<T>(), "; the Previous chain is strictly decreasing." }));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Crafter::DB::SchemaTests {
|
||||
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;
|
||||
std::chrono::system_clock::time_point timePoint;
|
||||
|
||||
static constexpr std::uint32_t version = 1;
|
||||
static constexpr auto orderedBy = &Transaction::timePoint;
|
||||
};
|
||||
|
||||
static_assert(TableType<Transaction>);
|
||||
static_assert(CheckTable<Transaction>());
|
||||
static_assert(Layout<Transaction>::leafCount == 7);
|
||||
static_assert(Layout<Transaction>::rowSize == 88);
|
||||
static_assert(Layout<Transaction>::rowAlign == 8);
|
||||
static_assert(Layout<Transaction>::columns[0].kind == Kind::Flags && Layout<Transaction>::columns[0].offset == 0);
|
||||
static_assert(Layout<Transaction>::Offset(4) == 2); // currency: the only 2-byte column, right after the flags byte
|
||||
static_assert(Layout<Transaction>::Offset(0) == 8); // name
|
||||
static_assert(Layout<Transaction>::Offset(2) == 40); // iban
|
||||
static_assert(Layout<Transaction>::Offset(5) == 72); // amount
|
||||
static_assert(Layout<Transaction>::Offset(6) == 80); // timePoint
|
||||
static_assert(Layout<Transaction>::columns[Layout<Transaction>::Column(6)].kind == Kind::TimePoint);
|
||||
static_assert(Layout<Transaction>::columns[Layout<Transaction>::Column(6)].paramDen == 1000000);
|
||||
static_assert(Layout<Transaction>::columns[Layout<Transaction>::Column(4)].flags == columnFlagEnum);
|
||||
static_assert(Layout<Transaction>::columns[Layout<Transaction>::Column(2)].name.View() == "iban");
|
||||
static_assert(Layout<Transaction>::orderedColumn == Layout<Transaction>::Column(6));
|
||||
static_assert(Layout<Transaction>::hasHeap);
|
||||
|
||||
struct Money {
|
||||
std::int64_t minor;
|
||||
Currency currency;
|
||||
};
|
||||
|
||||
struct Nested {
|
||||
std::optional<std::string> note;
|
||||
Money money;
|
||||
std::array<std::uint16_t, 3> tags;
|
||||
FixedString<11> bic;
|
||||
std::vector<std::uint32_t> ids;
|
||||
static constexpr std::uint32_t version = 2;
|
||||
};
|
||||
|
||||
static_assert(CheckTable<Nested>());
|
||||
static_assert(Layout<Nested>::leafCount == 2 + 2 + 3 + 1 + 1);
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(0)].name.View() == "note?");
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(1)].name.View() == "note");
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(1)].flags == columnFlagOptional);
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(1)].presenceOffset == Layout<Nested>::Offset(0));
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(2)].name.View() == "money.minor");
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(5)].name.View() == "tags[1]");
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(7)].kind == Kind::FixedBytes && Layout<Nested>::columns[Layout<Nested>::Column(7)].size == 11);
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(8)].kind == Kind::Blob && Layout<Nested>::columns[Layout<Nested>::Column(8)].paramDen == 4);
|
||||
static_assert(Layout<Nested>::columns[Layout<Nested>::Column(8)].variant == static_cast<std::uint32_t>(Kind::UInt32));
|
||||
|
||||
struct FloatBlob {
|
||||
std::vector<float> ids;
|
||||
static constexpr std::uint32_t version = 1;
|
||||
};
|
||||
struct IntBlob {
|
||||
std::vector<std::int32_t> ids;
|
||||
static constexpr std::uint32_t version = 1;
|
||||
};
|
||||
static_assert(Layout<FloatBlob>::fingerprint != Layout<IntBlob>::fingerprint, "blob element type must be part of the fingerprint");
|
||||
|
||||
struct Unordered {
|
||||
std::int64_t at;
|
||||
std::string text;
|
||||
static constexpr std::uint32_t version = 1;
|
||||
};
|
||||
struct Ordered {
|
||||
std::int64_t at;
|
||||
std::string text;
|
||||
static constexpr std::uint32_t version = 1;
|
||||
static constexpr auto orderedBy = &Ordered::at;
|
||||
};
|
||||
static_assert(Layout<Unordered>::fingerprint != Layout<Ordered>::fingerprint, "orderedBy must be part of the fingerprint");
|
||||
}
|
||||
Loading…
Reference in a new issue