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
335
interfaces/Crafter.DB-Codec.cppm
Normal file
335
interfaces/Crafter.DB-Codec.cppm
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Typed encode/decode against a Layout<T>. Every function here is a template
|
||||
// over (table type, first leaf index, member type), so column offsets are
|
||||
// compile-time constants and a scalar read is one load. Leaf indices follow
|
||||
// declaration order, depth first, exactly as Schema enumerates them.
|
||||
export module Crafter.DB:Codec;
|
||||
import std;
|
||||
import :Reflect;
|
||||
import :Format;
|
||||
import :Formatter;
|
||||
import :Schema;
|
||||
import :Storage;
|
||||
import :Error;
|
||||
|
||||
export namespace Crafter::DB {
|
||||
// What a Row hands out for a member of type M: the formatter's View for
|
||||
// variable-length members, an optional of that for std::optional, and the
|
||||
// value itself for everything else.
|
||||
template<class M>
|
||||
consteval auto ViewTypeOf() {
|
||||
constexpr Shape shape = ShapeOf<M>();
|
||||
if constexpr (shape == Shape::Var) return std::type_identity<typename Formatter<M>::View>{};
|
||||
else if constexpr (shape == Shape::Optional) return std::type_identity<std::optional<typename decltype(ViewTypeOf<typename OptionalTraits<M>::Type>())::type>>{};
|
||||
else return std::type_identity<M>{};
|
||||
}
|
||||
|
||||
template<class M>
|
||||
using View = typename decltype(ViewTypeOf<M>())::type;
|
||||
|
||||
// Zeroes the columns of leaves [First, First + Count): the payload of a
|
||||
// disengaged std::optional must not keep stale bytes (or stale heap
|
||||
// handles that Compact would then preserve).
|
||||
template<class T, std::size_t First, std::size_t Count>
|
||||
void ZeroLeaves(std::byte* row) noexcept {
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(std::memset(row + Layout<T>::Offset(First + I), 0, Layout<T>::columns[Layout<T>::Column(First + I)].size), ...);
|
||||
}(std::make_index_sequence<Count>{});
|
||||
}
|
||||
|
||||
// Copies the columns of leaves [First, First + Count) from one row image
|
||||
// to another, leaving every other byte (the flags byte above all) alone.
|
||||
template<class T, std::size_t First, std::size_t Count>
|
||||
void CopyLeaves(std::byte* destination, const std::byte* source) noexcept {
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(std::memcpy(destination + Layout<T>::Offset(First + I), source + Layout<T>::Offset(First + I), Layout<T>::columns[Layout<T>::Column(First + I)].size), ...);
|
||||
}(std::make_index_sequence<Count>{});
|
||||
}
|
||||
|
||||
// Values are converted implicitly, never static_cast: an int does not
|
||||
// silently become an enum, and swapped Emplace arguments do not compile.
|
||||
template<class T, std::size_t Leaf, class M, class V>
|
||||
void EncodeInto(std::byte* row, HeapWriter& heap, const V& value) {
|
||||
constexpr Shape shape = ShapeOf<M>();
|
||||
constexpr std::uint16_t offset = Layout<T>::Offset(Leaf);
|
||||
if constexpr (shape == Shape::Scalar) {
|
||||
const M scalar = value;
|
||||
std::memcpy(row + offset, &scalar, sizeof(M));
|
||||
} else if constexpr (shape == Shape::FixedBytes) {
|
||||
if constexpr (FixedStringTraits<M>::value) {
|
||||
const M fixed(value);
|
||||
std::memcpy(row + offset, fixed.chars.data(), fixed.chars.size());
|
||||
} else {
|
||||
const M& array = value;
|
||||
std::memcpy(row + offset, array.data(), sizeof(M));
|
||||
}
|
||||
} else if constexpr (shape == Shape::ScalarArray) {
|
||||
using E = typename StdArrayTraits<M>::Element;
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(EncodeInto<T, Leaf + I, E>(row, heap, value[I]), ...);
|
||||
}(std::make_index_sequence<StdArrayTraits<M>::size>{});
|
||||
} else if constexpr (shape == Shape::Optional) {
|
||||
using P = typename OptionalTraits<M>::Type;
|
||||
const std::uint8_t present = value.has_value() ? 1 : 0;
|
||||
std::memcpy(row + offset, &present, 1);
|
||||
if (value.has_value()) EncodeInto<T, Leaf + 1, P>(row, heap, *value);
|
||||
else ZeroLeaves<T, Leaf + 1, LeafCount<P>()>(row);
|
||||
} else if constexpr (shape == Shape::Var) {
|
||||
const Var var = Formatter<M>::Encode(value, heap);
|
||||
std::memcpy(row + offset, &var, sizeof(var));
|
||||
} else if constexpr (shape == Shape::Fixed) {
|
||||
using S = typename Formatter<M>::Stored;
|
||||
if constexpr (std::is_same_v<V, M>) {
|
||||
const S stored = Formatter<M>::Encode(value);
|
||||
EncodeInto<T, Leaf, S>(row, heap, stored);
|
||||
} else {
|
||||
const M converted = value;
|
||||
const S stored = Formatter<M>::Encode(converted);
|
||||
EncodeInto<T, Leaf, S>(row, heap, stored);
|
||||
}
|
||||
} else if constexpr (shape == Shape::Aggregate) {
|
||||
if constexpr (std::is_same_v<V, M>) {
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(EncodeInto<T, Leaf + LeafStart<M, I>(), MemberType<I, M>>(row, heap, Member<I>(value)), ...);
|
||||
}(std::make_index_sequence<memberCount<M>>{});
|
||||
} else {
|
||||
const M converted = value;
|
||||
EncodeInto<T, Leaf, M>(row, heap, converted);
|
||||
}
|
||||
} else {
|
||||
static_assert(shape == Shape::Scalar, "unsupported member shape reached the encoder; CheckTable should have rejected it");
|
||||
}
|
||||
}
|
||||
|
||||
template<class T, std::size_t Leaf, class M>
|
||||
M DecodeValue(const std::byte* row, const Heap& heap);
|
||||
|
||||
template<class T, std::size_t Leaf, class M>
|
||||
View<M> DecodeView(const std::byte* row, const Heap& heap) {
|
||||
constexpr Shape shape = ShapeOf<M>();
|
||||
constexpr std::uint16_t offset = Layout<T>::Offset(Leaf);
|
||||
if constexpr (shape == Shape::Var) {
|
||||
return Formatter<M>::Decode(row + offset, heap);
|
||||
} else if constexpr (shape == Shape::Optional) {
|
||||
using P = typename OptionalTraits<M>::Type;
|
||||
std::uint8_t present;
|
||||
std::memcpy(&present, row + offset, 1);
|
||||
if (present == 0) return std::nullopt;
|
||||
return View<M>(DecodeView<T, Leaf + 1, P>(row, heap));
|
||||
} else {
|
||||
return DecodeValue<T, Leaf, M>(row, heap);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T, std::size_t Leaf, class M>
|
||||
M DecodeValue(const std::byte* row, const Heap& heap) {
|
||||
constexpr Shape shape = ShapeOf<M>();
|
||||
constexpr std::uint16_t offset = Layout<T>::Offset(Leaf);
|
||||
if constexpr (shape == Shape::Scalar) {
|
||||
M scalar;
|
||||
std::memcpy(&scalar, row + offset, sizeof(M));
|
||||
return scalar;
|
||||
} else if constexpr (shape == Shape::FixedBytes) {
|
||||
M fixed{};
|
||||
if constexpr (FixedStringTraits<M>::value) std::memcpy(fixed.chars.data(), row + offset, fixed.chars.size());
|
||||
else std::memcpy(fixed.data(), row + offset, sizeof(M));
|
||||
return fixed;
|
||||
} else if constexpr (shape == Shape::ScalarArray) {
|
||||
using E = typename StdArrayTraits<M>::Element;
|
||||
M array{};
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
((array[I] = DecodeValue<T, Leaf + I, E>(row, heap)), ...);
|
||||
}(std::make_index_sequence<StdArrayTraits<M>::size>{});
|
||||
return array;
|
||||
} else if constexpr (shape == Shape::Optional) {
|
||||
using P = typename OptionalTraits<M>::Type;
|
||||
std::uint8_t present;
|
||||
std::memcpy(&present, row + offset, 1);
|
||||
if (present == 0) return std::nullopt;
|
||||
return M(DecodeValue<T, Leaf + 1, P>(row, heap));
|
||||
} else if constexpr (shape == Shape::Var) {
|
||||
return MaterializeVar<M>(Formatter<M>::Decode(row + offset, heap));
|
||||
} else if constexpr (shape == Shape::Fixed) {
|
||||
using S = typename Formatter<M>::Stored;
|
||||
return Formatter<M>::Decode(DecodeValue<T, Leaf, S>(row, heap));
|
||||
} else if constexpr (shape == Shape::Aggregate) {
|
||||
M aggregate{};
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
((Member<I>(aggregate) = DecodeValue<T, Leaf + LeafStart<M, I>(), MemberType<I, M>>(row, heap)), ...);
|
||||
}(std::make_index_sequence<memberCount<M>>{});
|
||||
return aggregate;
|
||||
} else {
|
||||
static_assert(shape == Shape::Scalar, "unsupported member shape reached the decoder");
|
||||
}
|
||||
}
|
||||
|
||||
// --- row level -----------------------------------------------------------
|
||||
|
||||
template<Aggregate T, std::size_t I>
|
||||
View<MemberType<I, T>> DecodeMember(const std::byte* row, const Heap& heap) {
|
||||
return DecodeView<T, LeafStart<T, I>(), MemberType<I, T>>(row, heap);
|
||||
}
|
||||
|
||||
template<Aggregate T, std::size_t I>
|
||||
MemberType<I, T> DecodeMemberValue(const std::byte* row, const Heap& heap) {
|
||||
return DecodeValue<T, LeafStart<T, I>(), MemberType<I, T>>(row, heap);
|
||||
}
|
||||
|
||||
template<Aggregate T>
|
||||
T DecodeRow(const std::byte* row, const Heap& heap) {
|
||||
T value{};
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
((Member<I>(value) = DecodeMemberValue<T, I>(row, heap)), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
return value;
|
||||
}
|
||||
|
||||
// Writes a live row (flags = 0) for `value` into `row`, which must be
|
||||
// Layout<T>::rowSize bytes of writable memory.
|
||||
template<Aggregate T>
|
||||
void EncodeRow(const T& value, std::byte* row, HeapWriter& heap) {
|
||||
std::memset(row, 0, Layout<T>::rowSize);
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(EncodeInto<T, LeafStart<T, I>(), MemberType<I, T>>(row, heap, Member<I>(value)), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
}
|
||||
|
||||
// Whether an Emplace argument of type A is acceptable for a member of type
|
||||
// M: what the formatter's Encode accepts for variable-length members, an
|
||||
// implicit conversion for everything else.
|
||||
template<class M, class A>
|
||||
consteval bool EmplaceAccepts() {
|
||||
constexpr Shape shape = ShapeOf<M>();
|
||||
if constexpr (shape == Shape::Var) return requires(const A& a, HeapWriter& w) { Formatter<M>::Encode(a, w); };
|
||||
else if constexpr (shape == Shape::FixedBytes && FixedStringTraits<M>::value) return std::is_constructible_v<M, const A&>;
|
||||
else return std::is_convertible_v<const A&, M>;
|
||||
}
|
||||
|
||||
template<Aggregate T, std::size_t I, class A>
|
||||
consteval bool CheckEmplaceArgument() {
|
||||
static_assert(EmplaceAccepts<MemberType<I, T>, A>(), Message({ "Crafter.DB: the Emplace argument for member '", MemberName<I, T>(), "' of ", TypeName<T>(), " has type ", TypeName<A>(), ", which does not convert to the member; arguments follow declaration order." }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// One argument per member, each encoded straight into its columns.
|
||||
template<Aggregate T, class... Args>
|
||||
void EmplaceRow(std::byte* row, HeapWriter& heap, const Args&... args) {
|
||||
static_assert(sizeof...(Args) == memberCount<T>, "Crafter.DB: Emplace takes exactly one argument per member of the table struct, in declaration order");
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(CheckEmplaceArgument<T, I, Args>(), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
std::memset(row, 0, Layout<T>::rowSize);
|
||||
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
(EncodeInto<T, LeafStart<T, I>(), MemberType<I, T>>(row, heap, args), ...);
|
||||
}(std::make_index_sequence<memberCount<T>>{});
|
||||
}
|
||||
|
||||
// Encodes member I only (Set); the rest of the row is left alone.
|
||||
template<Aggregate T, std::size_t I, class V>
|
||||
void EncodeMember(std::byte* row, HeapWriter& heap, const V& value) {
|
||||
EncodeInto<T, LeafStart<T, I>(), MemberType<I, T>>(row, heap, value);
|
||||
}
|
||||
|
||||
// Out-of-line heap bytes referenced by member I's columns.
|
||||
template<Aggregate T, std::size_t I>
|
||||
std::uint64_t MemberHeapBytes(const std::byte* row) noexcept {
|
||||
std::uint64_t bytes = 0;
|
||||
constexpr std::size_t first = LeafStart<T, I>();
|
||||
constexpr std::size_t count = LeafCount<MemberType<I, T>>();
|
||||
for (std::size_t leaf = first; leaf < first + count; leaf++) {
|
||||
const ColumnInfo& column = Layout<T>::columns[Layout<T>::Column(leaf)];
|
||||
if (column.kind != Kind::String && column.kind != Kind::Blob) continue;
|
||||
Var var;
|
||||
std::memcpy(&var, row + column.offset, sizeof(var));
|
||||
if (!VarInline(var.length, column.kind == Kind::Blob && column.paramNum != 0 ? column.paramNum : 1)) bytes += var.length;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// --- ordered column ------------------------------------------------------
|
||||
|
||||
template<class T>
|
||||
using OrderedKey = MemberTypeOf<T::orderedBy>;
|
||||
|
||||
template<class T> requires HasOrderedBy<T>
|
||||
OrderedKey<T> KeyOfSlot(const TableFile& file, std::uint64_t slot) {
|
||||
return DecodeMemberValue<T, Layout<T>::orderedMember>(file.Slot(slot), file.HeapView());
|
||||
}
|
||||
|
||||
template<class T> requires HasOrderedBy<T>
|
||||
std::uint64_t LowerBoundSlot(const TableFile& file, const OrderedKey<T>& key, std::uint64_t first, std::uint64_t last) {
|
||||
while (first < last) {
|
||||
const std::uint64_t mid = first + (last - first) / 2;
|
||||
if (KeyOfSlot<T>(file, mid) < key) first = mid + 1;
|
||||
else last = mid;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
template<class T> requires HasOrderedBy<T>
|
||||
std::uint64_t UpperBoundSlot(const TableFile& file, const OrderedKey<T>& key, std::uint64_t first, std::uint64_t last) {
|
||||
while (first < last) {
|
||||
const std::uint64_t mid = first + (last - first) / 2;
|
||||
if (key < KeyOfSlot<T>(file, mid)) last = mid;
|
||||
else first = mid + 1;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
// Throws OrderError when `key` would sit below the key of slot `before`
|
||||
// (or above the key of slot `after`, when given).
|
||||
template<class T> requires HasOrderedBy<T>
|
||||
void CheckOrder(const TableFile& file, const OrderedKey<T>& key, std::optional<std::uint64_t> before, std::optional<std::uint64_t> after) {
|
||||
if (before && key < KeyOfSlot<T>(file, *before)) {
|
||||
throw OrderError(std::format("{}: value would break the order of '{}' (it sorts before the previous row)", file.Path().string(), MemberName<Layout<T>::orderedMember, T>()));
|
||||
}
|
||||
if (after && KeyOfSlot<T>(file, *after) < key) {
|
||||
throw OrderError(std::format("{}: value would break the order of '{}' (it sorts after the next row)", file.Path().string(), MemberName<Layout<T>::orderedMember, T>()));
|
||||
}
|
||||
}
|
||||
|
||||
// Appends encoded rows. Checks the ordered column against the last slot
|
||||
// and within the batch before anything is written.
|
||||
template<Aggregate T, std::ranges::input_range R> requires std::convertible_to<std::ranges::range_reference_t<R>, const T&>
|
||||
std::uint64_t AppendRows(TableFile& file, R&& values) {
|
||||
file.RequireWritable();
|
||||
const std::uint64_t first = file.SlotCount();
|
||||
if constexpr (std::ranges::sized_range<R> && std::ranges::random_access_range<R>) {
|
||||
const std::uint64_t count = std::ranges::size(values);
|
||||
if (count == 0) return first;
|
||||
if constexpr (HasOrderedBy<T>) {
|
||||
std::optional<std::uint64_t> before;
|
||||
if (first != 0) before = first - 1;
|
||||
auto it = std::ranges::begin(values);
|
||||
CheckOrder<T>(file, static_cast<const T&>(*it).*T::orderedBy, before, std::nullopt);
|
||||
for (std::uint64_t i = 1; i < count; i++) {
|
||||
if (static_cast<const T&>(it[i]).*T::orderedBy < static_cast<const T&>(it[i - 1]).*T::orderedBy) {
|
||||
throw OrderError(std::format("{}: batch is not sorted by '{}'", file.Path().string(), MemberName<Layout<T>::orderedMember, T>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
std::byte* out = file.BeginAppend(count);
|
||||
HeapWriter& heap = file.Writer();
|
||||
try {
|
||||
for (const T& value : values) {
|
||||
EncodeRow<T>(value, out, heap);
|
||||
out += Layout<T>::rowSize;
|
||||
}
|
||||
} catch (...) {
|
||||
file.AbortAppend();
|
||||
throw;
|
||||
}
|
||||
file.CommitAppend(count);
|
||||
return first;
|
||||
} else {
|
||||
// A range that cannot be sized is buffered first, so the order
|
||||
// check runs over the whole batch before any row is written and a
|
||||
// failing batch leaves the table untouched.
|
||||
std::vector<T> buffered;
|
||||
for (const T& value : values) buffered.push_back(value);
|
||||
return AppendRows<T>(file, std::span<const T>(buffered));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue