// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Typed encode/decode against a Layout. 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 consteval auto ViewTypeOf() { constexpr Shape shape = ShapeOf(); if constexpr (shape == Shape::Var) return std::type_identity::View>{}; else if constexpr (shape == Shape::Optional) return std::type_identity::Type>())::type>>{}; else return std::type_identity{}; } template using View = typename decltype(ViewTypeOf())::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 void ZeroLeaves(std::byte* row) noexcept { [&](std::index_sequence) { (std::memset(row + Layout::Offset(First + I), 0, Layout::columns[Layout::Column(First + I)].size), ...); }(std::make_index_sequence{}); } // 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 void CopyLeaves(std::byte* destination, const std::byte* source) noexcept { [&](std::index_sequence) { (std::memcpy(destination + Layout::Offset(First + I), source + Layout::Offset(First + I), Layout::columns[Layout::Column(First + I)].size), ...); }(std::make_index_sequence{}); } // Values are converted implicitly, never static_cast: an int does not // silently become an enum, and swapped Emplace arguments do not compile. template void EncodeInto(std::byte* row, HeapWriter& heap, const V& value) { constexpr Shape shape = ShapeOf(); constexpr std::uint16_t offset = Layout::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::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::Element; [&](std::index_sequence) { (EncodeInto(row, heap, value[I]), ...); }(std::make_index_sequence::size>{}); } else if constexpr (shape == Shape::Optional) { using P = typename OptionalTraits::Type; const std::uint8_t present = value.has_value() ? 1 : 0; std::memcpy(row + offset, &present, 1); if (value.has_value()) EncodeInto(row, heap, *value); else ZeroLeaves()>(row); } else if constexpr (shape == Shape::Var) { const Var var = Formatter::Encode(value, heap); std::memcpy(row + offset, &var, sizeof(var)); } else if constexpr (shape == Shape::Fixed) { using S = typename Formatter::Stored; if constexpr (std::is_same_v) { const S stored = Formatter::Encode(value); EncodeInto(row, heap, stored); } else { const M converted = value; const S stored = Formatter::Encode(converted); EncodeInto(row, heap, stored); } } else if constexpr (shape == Shape::Aggregate) { if constexpr (std::is_same_v) { [&](std::index_sequence) { (EncodeInto(), MemberType>(row, heap, Member(value)), ...); }(std::make_index_sequence>{}); } else { const M converted = value; EncodeInto(row, heap, converted); } } else { static_assert(shape == Shape::Scalar, "unsupported member shape reached the encoder; CheckTable should have rejected it"); } } template M DecodeValue(const std::byte* row, const Heap& heap); template View DecodeView(const std::byte* row, const Heap& heap) { constexpr Shape shape = ShapeOf(); constexpr std::uint16_t offset = Layout::Offset(Leaf); if constexpr (shape == Shape::Var) { return Formatter::Decode(row + offset, heap); } else if constexpr (shape == Shape::Optional) { using P = typename OptionalTraits::Type; std::uint8_t present; std::memcpy(&present, row + offset, 1); if (present == 0) return std::nullopt; return View(DecodeView(row, heap)); } else { return DecodeValue(row, heap); } } template M DecodeValue(const std::byte* row, const Heap& heap) { constexpr Shape shape = ShapeOf(); constexpr std::uint16_t offset = Layout::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::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::Element; M array{}; [&](std::index_sequence) { ((array[I] = DecodeValue(row, heap)), ...); }(std::make_index_sequence::size>{}); return array; } else if constexpr (shape == Shape::Optional) { using P = typename OptionalTraits::Type; std::uint8_t present; std::memcpy(&present, row + offset, 1); if (present == 0) return std::nullopt; return M(DecodeValue(row, heap)); } else if constexpr (shape == Shape::Var) { return MaterializeVar(Formatter::Decode(row + offset, heap)); } else if constexpr (shape == Shape::Fixed) { using S = typename Formatter::Stored; return Formatter::Decode(DecodeValue(row, heap)); } else if constexpr (shape == Shape::Aggregate) { M aggregate{}; [&](std::index_sequence) { ((Member(aggregate) = DecodeValue(), MemberType>(row, heap)), ...); }(std::make_index_sequence>{}); return aggregate; } else { static_assert(shape == Shape::Scalar, "unsupported member shape reached the decoder"); } } // --- row level ----------------------------------------------------------- template View> DecodeMember(const std::byte* row, const Heap& heap) { return DecodeView(), MemberType>(row, heap); } template MemberType DecodeMemberValue(const std::byte* row, const Heap& heap) { return DecodeValue(), MemberType>(row, heap); } template T DecodeRow(const std::byte* row, const Heap& heap) { T value{}; [&](std::index_sequence) { ((Member(value) = DecodeMemberValue(row, heap)), ...); }(std::make_index_sequence>{}); return value; } // Writes a live row (flags = 0) for `value` into `row`, which must be // Layout::rowSize bytes of writable memory. template void EncodeRow(const T& value, std::byte* row, HeapWriter& heap) { std::memset(row, 0, Layout::rowSize); [&](std::index_sequence) { (EncodeInto(), MemberType>(row, heap, Member(value)), ...); }(std::make_index_sequence>{}); } // 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 consteval bool EmplaceAccepts() { constexpr Shape shape = ShapeOf(); if constexpr (shape == Shape::Var) return requires(const A& a, HeapWriter& w) { Formatter::Encode(a, w); }; else if constexpr (shape == Shape::FixedBytes && FixedStringTraits::value) return std::is_constructible_v; else return std::is_convertible_v; } template consteval bool CheckEmplaceArgument() { static_assert(EmplaceAccepts, A>(), Message({ "Crafter.DB: the Emplace argument for member '", MemberName(), "' of ", TypeName(), " has type ", TypeName(), ", which does not convert to the member; arguments follow declaration order." })); return true; } // One argument per member, each encoded straight into its columns. template void EmplaceRow(std::byte* row, HeapWriter& heap, const Args&... args) { static_assert(sizeof...(Args) == memberCount, "Crafter.DB: Emplace takes exactly one argument per member of the table struct, in declaration order"); [&](std::index_sequence) { (CheckEmplaceArgument(), ...); }(std::make_index_sequence>{}); std::memset(row, 0, Layout::rowSize); [&](std::index_sequence) { (EncodeInto(), MemberType>(row, heap, args), ...); }(std::make_index_sequence>{}); } // Encodes member I only (Set); the rest of the row is left alone. template void EncodeMember(std::byte* row, HeapWriter& heap, const V& value) { EncodeInto(), MemberType>(row, heap, value); } // Out-of-line heap bytes referenced by member I's columns. template std::uint64_t MemberHeapBytes(const std::byte* row) noexcept { std::uint64_t bytes = 0; constexpr std::size_t first = LeafStart(); constexpr std::size_t count = LeafCount>(); for (std::size_t leaf = first; leaf < first + count; leaf++) { const ColumnInfo& column = Layout::columns[Layout::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 using OrderedKey = MemberTypeOf; template requires HasOrderedBy OrderedKey KeyOfSlot(const TableFile& file, std::uint64_t slot) { return DecodeMemberValue::orderedMember>(file.Slot(slot), file.HeapView()); } template requires HasOrderedBy std::uint64_t LowerBoundSlot(const TableFile& file, const OrderedKey& key, std::uint64_t first, std::uint64_t last) { while (first < last) { const std::uint64_t mid = first + (last - first) / 2; if (KeyOfSlot(file, mid) < key) first = mid + 1; else last = mid; } return first; } template requires HasOrderedBy std::uint64_t UpperBoundSlot(const TableFile& file, const OrderedKey& key, std::uint64_t first, std::uint64_t last) { while (first < last) { const std::uint64_t mid = first + (last - first) / 2; if (key < KeyOfSlot(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 requires HasOrderedBy void CheckOrder(const TableFile& file, const OrderedKey& key, std::optional before, std::optional after) { if (before && key < KeyOfSlot(file, *before)) { throw OrderError(std::format("{}: value would break the order of '{}' (it sorts before the previous row)", file.Path().string(), MemberName::orderedMember, T>())); } if (after && KeyOfSlot(file, *after) < key) { throw OrderError(std::format("{}: value would break the order of '{}' (it sorts after the next row)", file.Path().string(), MemberName::orderedMember, T>())); } } // Appends encoded rows. Checks the ordered column against the last slot // and within the batch before anything is written. template requires std::convertible_to, 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 && std::ranges::random_access_range) { const std::uint64_t count = std::ranges::size(values); if (count == 0) return first; if constexpr (HasOrderedBy) { std::optional before; if (first != 0) before = first - 1; auto it = std::ranges::begin(values); CheckOrder(file, static_cast(*it).*T::orderedBy, before, std::nullopt); for (std::uint64_t i = 1; i < count; i++) { if (static_cast(it[i]).*T::orderedBy < static_cast(it[i - 1]).*T::orderedBy) { throw OrderError(std::format("{}: batch is not sorted by '{}'", file.Path().string(), MemberName::orderedMember, T>())); } } } std::byte* out = file.BeginAppend(count); HeapWriter& heap = file.Writer(); try { for (const T& value : values) { EncodeRow(value, out, heap); out += Layout::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 buffered; for (const T& value : values) buffered.push_back(value); return AppendRows(file, std::span(buffered)); } } }