// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // The customisation point for member types, in the spirit of std::formatter: // specialise Crafter::DB::Formatter to say how a T is stored. Fixed-width // formatters map T to a storable `Stored` type; variable-length formatters // map it to the 16-byte Var handle and write the bytes into the heap. This // partition also holds the reader-side handle views (StringRef, BlobRef), the // pre-encoded search Key, and the built-in formatters for std::string, // std::vector and std::chrono types. export module Crafter.DB:Formatter; import std; import :Format; import :Error; import :Reflect; export namespace Crafter::DB { template concept Scalar = (std::is_arithmetic_v && !std::same_as) || std::is_enum_v || std::same_as; template concept ByteLike = std::same_as || std::same_as || std::same_as || std::same_as || std::same_as; // The column kind of a scalar. `char` is Int8 whatever the platform's // signedness, so a file's fingerprint does not depend on the ABI. template consteval Kind ScalarKind() { if constexpr (std::same_as) return Kind::Bool; else if constexpr (std::is_enum_v) return ScalarKind>(); else if constexpr (std::same_as) return Kind::UInt8; else if constexpr (std::same_as) return Kind::Int8; else if constexpr (std::same_as) return sizeof(wchar_t) == 2 ? Kind::UInt16 : Kind::Int32; else if constexpr (std::is_floating_point_v) return sizeof(T) == 4 ? Kind::Float32 : Kind::Float64; else if constexpr (std::is_signed_v) return sizeof(T) == 1 ? Kind::Int8 : sizeof(T) == 2 ? Kind::Int16 : sizeof(T) == 4 ? Kind::Int32 : Kind::Int64; else return sizeof(T) == 1 ? Kind::UInt8 : sizeof(T) == 2 ? Kind::UInt16 : sizeof(T) == 4 ? Kind::UInt32 : Kind::UInt64; } // A 32-bit code that identifies a type in the schema: the scalar kind // for scalars, a hash of the compiler's spelling otherwise. template consteval std::uint32_t TypeCode() { if constexpr (Scalar) return static_cast(ScalarKind()); else return static_cast(Fnv1a(fnvOffset, TypeName()) & 0xFFFFFFFF) | 0x80000000u; } // Reader-side view of a table's heap region. `used` is the published // length, so a handle pointing past it (a corrupt file) is caught before // the bytes are touched. struct Heap { const std::byte* base = nullptr; const std::atomic* used = nullptr; std::span Read(std::uint64_t offset, std::uint64_t length) const { const std::uint64_t limit = used->load(std::memory_order_acquire); if (offset > limit || length > limit - offset) throw CorruptError(std::format("heap reference [{}, +{}) is outside the committed heap of {} bytes", offset, length, limit)); return { base + offset, static_cast(length) }; } }; // Writer-side cursor into the heap region. Formatters append through it; // the table decides when the bytes become visible. Grow() is only reached // when an extent fills up, so the virtual call never sits on the fast path. class HeapWriter { public: virtual ~HeapWriter() = default; std::uint64_t Write(std::span bytes, std::uint32_t align = 1) { std::uint64_t at = cursor; if (align > 1) at = (at + align - 1) & ~std::uint64_t(align - 1); if (at + bytes.size() > capacity) Grow(at + bytes.size()); if (!bytes.empty()) std::memcpy(base + at, bytes.data(), bytes.size()); cursor = at + bytes.size(); return at; } std::uint64_t Cursor() const noexcept { return cursor; } void Rewind(std::uint64_t to) noexcept { cursor = to; } protected: virtual void Grow(std::uint64_t neededCapacity) = 0; std::byte* base = nullptr; std::uint64_t cursor = 0; std::uint64_t capacity = 0; }; // Whether a value of this length and element alignment is stored inside // the handle. Alignments above 4 cannot be inline because the inline bytes // start at handle offset 4. constexpr bool VarInline(std::uint64_t length, std::uint32_t align) noexcept { return length <= inlineVarBytes && align <= 4; } inline Var EncodeVar(std::span bytes, HeapWriter& heap, std::uint32_t align = 1) { if (bytes.size() >= std::uint64_t(1) << 32) throw Error(std::format("a value of {} bytes does not fit a Crafter.DB column (limit 4 GiB)", bytes.size())); Var var{}; var.length = static_cast(bytes.size()); if (VarInline(bytes.size(), align)) { if (!bytes.empty()) std::memcpy(reinterpret_cast(&var) + 4, bytes.data(), bytes.size()); } else { // An empty blob can land here when its element alignment forbids // inline storage; it has no prefix bytes to copy. if (!bytes.empty()) std::memcpy(&var.prefix, bytes.data(), std::min(bytes.size(), 4)); var.tailOrOffset = heap.Write(bytes, align); } return var; } // The bytes a handle refers to, inline or in the heap. template std::span VarBytes(const std::byte* handle, const Heap& heap) { Var var; std::memcpy(&var, handle, sizeof(var)); if (VarInline(var.length, Align)) return { handle + 4, var.length }; return heap.Read(var.tailOrOffset, var.length); } // A search key encoded once, so a scan compares one 8-byte word per row. // `head` is the first 8 bytes of a matching handle (length | prefix << 32) // and `tail` the second 8 bytes when the value is inline. class Key { public: explicit Key(std::string_view value) : text(value) { head = static_cast(static_cast(text.size())); std::uint32_t prefix = 0; if (!text.empty()) std::memcpy(&prefix, text.data(), std::min(text.size(), 4)); head |= std::uint64_t(prefix) << 32; tail = 0; if (text.size() > 4 && text.size() <= inlineVarBytes) std::memcpy(&tail, text.data() + 4, text.size() - 4); } std::uint64_t head; std::uint64_t tail; std::string text; }; // Handle view of a string column. Points into the mapped row (and, for // long values, the mapped heap); valid for as long as the table and that // row are. Converts to std::string_view on demand; its comparison operators // avoid that conversion and reject almost every non-match on the first // 8-byte compare. class StringRef { public: StringRef() = default; StringRef(const std::byte* handle, const Heap* heap) noexcept : handle(handle), heap(heap) {} std::uint32_t Length() const noexcept { std::uint32_t length; std::memcpy(&length, handle, sizeof(length)); return length; } bool Empty() const noexcept { return Length() == 0; } std::string_view View() const { const std::span bytes = VarBytes<1>(handle, *heap); return { reinterpret_cast(bytes.data()), bytes.size() }; } operator std::string_view() const { return View(); } std::uint64_t Head() const noexcept { std::uint64_t head; std::memcpy(&head, handle, sizeof(head)); return head; } std::uint64_t Tail() const noexcept { std::uint64_t tail; std::memcpy(&tail, handle + 8, sizeof(tail)); return tail; } friend bool operator==(StringRef ref, const Key& key) { if (ref.Head() != key.head) return false; if (key.text.size() <= inlineVarBytes) return ref.Tail() == key.tail; const std::span bytes = ref.heap->Read(ref.Tail(), key.text.size()); return std::memcmp(bytes.data() + 4, key.text.data() + 4, key.text.size() - 4) == 0; } friend bool operator==(StringRef ref, std::string_view value) { std::uint64_t head = static_cast(static_cast(value.size())); std::uint32_t prefix = 0; if (!value.empty()) std::memcpy(&prefix, value.data(), std::min(value.size(), 4)); head |= std::uint64_t(prefix) << 32; if (ref.Head() != head) return false; if (value.size() <= inlineVarBytes) { std::uint64_t tail = 0; if (value.size() > 4) std::memcpy(&tail, value.data() + 4, value.size() - 4); return ref.Tail() == tail; } const std::span bytes = ref.heap->Read(ref.Tail(), value.size()); return std::memcmp(bytes.data() + 4, value.data() + 4, value.size() - 4) == 0; } friend bool operator==(StringRef a, StringRef b) { if (a.Head() != b.Head()) return false; if (a.Length() <= inlineVarBytes) return a.Tail() == b.Tail(); return a.View() == b.View(); } friend std::strong_ordering operator<=>(StringRef ref, std::string_view value) { const std::int32_t c = ref.View().compare(value); return c < 0 ? std::strong_ordering::less : c > 0 ? std::strong_ordering::greater : std::strong_ordering::equal; } friend std::strong_ordering operator<=>(StringRef a, StringRef b) { return a <=> b.View(); } private: const std::byte* handle = nullptr; const Heap* heap = nullptr; }; // Handle view of a blob column holding elements of E. template class BlobRef { public: BlobRef() = default; BlobRef(const std::byte* handle, const Heap* heap) noexcept : handle(handle), heap(heap) {} std::size_t Size() const noexcept { std::uint32_t length; std::memcpy(&length, handle, sizeof(length)); return length / sizeof(E); } std::span View() const { const std::span bytes = VarBytes(handle, *heap); return { reinterpret_cast(bytes.data()), bytes.size() / sizeof(E) }; } operator std::span() const { return View(); } friend bool operator==(BlobRef ref, std::span value) { const std::span mine = ref.View(); return mine.size() == value.size() && std::memcmp(mine.data(), value.data(), value.size_bytes()) == 0; } private: const std::byte* handle = nullptr; const Heap* heap = nullptr; }; // Specialise this for your own member types. Left undefined on purpose: // "no Formatter" is then a clean concept failure with a readable message. template struct Formatter; // A fixed-width formatter maps T to a `Stored` type that is itself storable // (scalar, enum, std::array, aggregate of storables). Optional members: // `kind` (overrides the column kind), `paramNum`/`paramDen` (chrono period). template concept FixedFormatter = requires { typename Formatter::Stored; } && !std::same_as::Stored, Var> && requires(const T& value, const typename Formatter::Stored& stored) { { Formatter::Encode(value) } -> std::same_as::Stored>; { Formatter::Decode(stored) } -> std::convertible_to; }; // A variable-length formatter maps T to a Var handle. `View` is what a Row // hands out (zero-copy); Materialize(View) -> T is optional and defaults to // T(view). Optional: `kind` (String or Blob), `align` for blob elements. template concept VarFormatter = requires { typename Formatter::Stored; typename Formatter::View; } && std::same_as::Stored, Var> && requires(const T& value, HeapWriter& writer, const std::byte* handle, const Heap& heap) { { Formatter::Encode(value, writer) } -> std::same_as; { Formatter::Decode(handle, heap) } -> std::same_as::View>; }; template concept HasFormatterKind = requires { { Formatter::kind } -> std::convertible_to; }; template concept HasFormatterPeriod = requires { { Formatter::paramNum } -> std::convertible_to; { Formatter::paramDen } -> std::convertible_to; }; template concept HasFormatterVariant = requires { { Formatter::variant } -> std::convertible_to; }; template concept HasMaterialize = VarFormatter && requires(typename Formatter::View view) { { Formatter::Materialize(view) } -> std::same_as; }; template T MaterializeVar(typename Formatter::View view) { if constexpr (HasMaterialize) return Formatter::Materialize(view); else return T(view); } template<> struct Formatter { using Stored = Var; using View = StringRef; static constexpr Kind kind = Kind::String; static Var Encode(std::string_view value, HeapWriter& heap) { return EncodeVar(std::as_bytes(std::span(value)), heap, 1); } static StringRef Decode(const std::byte* handle, const Heap& heap) noexcept { return { handle, &heap }; } static std::string Materialize(StringRef view) { return std::string(view.View()); } }; template requires (std::is_trivially_copyable_v && !std::is_pointer_v && !std::is_member_pointer_v) struct Formatter> { using Stored = Var; using View = BlobRef; static constexpr Kind kind = Kind::Blob; static constexpr std::uint32_t paramNum = alignof(E); static constexpr std::uint32_t paramDen = sizeof(E); static constexpr std::uint32_t variant = TypeCode(); // the element type is part of the schema static Var Encode(std::span value, HeapWriter& heap) { return EncodeVar(std::as_bytes(value), heap, alignof(E)); } static BlobRef Decode(const std::byte* handle, const Heap& heap) noexcept { return { handle, &heap }; } static std::vector Materialize(BlobRef view) { const std::span elements = view.View(); return std::vector(elements.begin(), elements.end()); } }; template struct Formatter> { static_assert(Period::num > 0 && Period::num <= 0xFFFFFFFF && Period::den > 0 && Period::den <= 0xFFFFFFFF, "chrono period does not fit the column descriptor"); using Stored = Rep; static constexpr Kind kind = Kind::Duration; static constexpr std::uint32_t paramNum = static_cast(Period::num); static constexpr std::uint32_t paramDen = static_cast(Period::den); static constexpr Rep Encode(const std::chrono::duration& value) noexcept { return value.count(); } static constexpr std::chrono::duration Decode(const Rep& stored) noexcept { return std::chrono::duration(stored); } }; template struct Formatter> { using Period = typename Duration::period; static_assert(Period::num > 0 && Period::num <= 0xFFFFFFFF && Period::den > 0 && Period::den <= 0xFFFFFFFF, "chrono period does not fit the column descriptor"); using Stored = typename Duration::rep; static constexpr Kind kind = Kind::TimePoint; static constexpr std::uint32_t paramNum = static_cast(Period::num); static constexpr std::uint32_t paramDen = static_cast(Period::den); static constexpr std::uint32_t variant = TypeCode(); // which clock the epoch belongs to static constexpr Stored Encode(const std::chrono::time_point& value) noexcept { return value.time_since_epoch().count(); } static constexpr std::chrono::time_point Decode(const Stored& stored) noexcept { return std::chrono::time_point(Duration(stored)); } }; // An inline, fixed-capacity string column: N bytes in the row, NUL padded, // no heap. For codes with a known maximum length (BIC, currency, ISIN). template struct FixedString { std::array chars{}; constexpr FixedString() = default; constexpr FixedString(std::string_view value) { if (value.size() > N) throw std::length_error(std::format("'{}' does not fit a FixedString<{}>", value, N)); std::ranges::copy(value, chars.begin()); } constexpr std::string_view View() const noexcept { const auto end = std::ranges::find(chars, '\0'); return { chars.data(), static_cast(end - chars.begin()) }; } constexpr operator std::string_view() const noexcept { return View(); } friend constexpr bool operator==(const FixedString&, const FixedString&) = default; friend constexpr auto operator<=>(const FixedString&, const FixedString&) = default; friend constexpr bool operator==(const FixedString& a, std::string_view b) noexcept { return a.View() == b; } }; static_assert(VarFormatter); static_assert(VarFormatter>); static_assert(VarFormatter>); static_assert(FixedFormatter); static_assert(FixedFormatter); static_assert(HasMaterialize); }