Crafter.DB/interfaces/Crafter.DB-Codec.cppm

335 lines
16 KiB
Text
Raw Normal View History

// 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));
}
}
}