// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Row: a 16-byte view of one stored row that decodes columns on demand, // plus the ranges over rows: LiveRange skips tombstones, SlotView is random // access over every slot (RowId space). export module Crafter.DB:Row; import std; import :Reflect; import :Format; import :Formatter; import :Schema; import :Storage; import :Codec; export namespace Crafter::DB { using RowId = std::uint64_t; // Column tag: `inline constexpr Column<&Transaction::iban> iban{};` // lets you write row[iban] and iban == x. The operators live in :Query. template struct Column { using Table = MemberClass; using Type = MemberTypeOf; static constexpr auto pointer = Ptr; static constexpr std::size_t index = MemberIndex(); static constexpr std::string_view Name() noexcept { return MemberName(); } }; template class Row { public: Row() = default; Row(const std::byte* row, const TableFile* file) noexcept : row(row), file(file) {} // Positional access: member I, resolved at compile time. template View> Get() const { return DecodeMember(row, file->HeapView()); } // Member-pointer access, resolved at compile time: row.Get<&T::iban>(). template requires std::is_same_v, T> View> Get() const { constexpr std::size_t index = MemberIndex(); static_assert(index != noMember, "Crafter.DB: pointer does not designate a direct member of the table struct"); return Get(); } // Member-pointer access with a run-time pointer: row[&T::iban]. Every // call site passes a literal, so the lookup folds after inlining. template View operator[](M T::* ptr) const { const std::size_t index = MemberIndexOf(ptr); View result{}; [&](std::index_sequence) { ((void)[&] { if constexpr (std::is_same_v, M>) { if (index == I) result = Get(); } }(), ...); }(std::make_index_sequence>{}); return result; } template requires std::is_same_v, T> View> operator[](Column) const { return Get(); } // Copies every column out into a T (allocates for strings and blobs). T Materialize() const { return DecodeRow(row, file->HeapView()); } explicit operator T() const { return Materialize(); } RowId Id() const noexcept { return static_cast((row - file->Rows()) / Layout::rowSize); } bool Erased() const noexcept { return TableFile::Deleted(row); } const std::byte* Bytes() const noexcept { return row; } const TableFile* File() const noexcept { return file; } friend bool operator==(const Row&, const Row&) = default; private: const std::byte* row = nullptr; const TableFile* file = nullptr; }; // Forward iterator over live rows of a slot range. The end of the range // is a sentinel, so the iterator's own snapshot of the last slot is the // only bound it ever compares against: a concurrent Append between two // reads of the slot count cannot make it walk into unpublished memory. // `checkFlags` is false when the table has no tombstones, which removes // the per-row test. template class LiveIterator { public: using value_type = Row; using difference_type = std::ptrdiff_t; using iterator_concept = std::forward_iterator_tag; using iterator_category = std::forward_iterator_tag; LiveIterator() = default; LiveIterator(const std::byte* current, const std::byte* last, const TableFile* file, bool checkFlags) noexcept : current(current), last(last), file(file), checkFlags(checkFlags) { Skip(); } Row operator*() const noexcept { return { current, file }; } LiveIterator& operator++() noexcept { current += Layout::rowSize; Skip(); return *this; } LiveIterator operator++(int) noexcept { LiveIterator copy = *this; ++*this; return copy; } friend bool operator==(const LiveIterator& a, const LiveIterator& b) noexcept { return a.current == b.current; } friend bool operator==(const LiveIterator& it, std::default_sentinel_t) noexcept { return it.current >= it.last; } private: void Skip() noexcept { if (!checkFlags) return; while (current < last && TableFile::Deleted(current)) current += Layout::rowSize; } const std::byte* current = nullptr; const std::byte* last = nullptr; const TableFile* file = nullptr; bool checkFlags = false; }; template class LiveRange : public std::ranges::view_interface> { public: LiveRange() = default; LiveRange(const TableFile* file, std::uint64_t firstSlot, std::uint64_t endSlot) noexcept : file(file), first(file->Slot(firstSlot)), last(file->Slot(endSlot)) {} // The tombstone flag is read when iteration starts, not when the range // was built, so a query object created before an Erase still skips it. LiveIterator begin() const noexcept { return { first, last, file, file->HasTombstones() }; } std::default_sentinel_t end() const noexcept { return {}; } std::uint64_t SlotCount() const noexcept { return static_cast((last - first) / Layout::rowSize); } private: const TableFile* file = nullptr; const std::byte* first = nullptr; const std::byte* last = nullptr; }; // Random access over every slot, tombstones included (row.Erased() tells). template class SlotView : public std::ranges::view_interface> { public: class Iterator { public: using value_type = Row; using difference_type = std::ptrdiff_t; using iterator_concept = std::random_access_iterator_tag; using iterator_category = std::random_access_iterator_tag; Iterator() = default; Iterator(const TableFile* file, std::uint64_t index) noexcept : file(file), index(index) {} Row operator*() const noexcept { return { file->Slot(index), file }; } Row operator[](difference_type n) const noexcept { return *(*this + n); } Iterator& operator++() noexcept { ++index; return *this; } Iterator operator++(int) noexcept { Iterator c = *this; ++index; return c; } Iterator& operator--() noexcept { --index; return *this; } Iterator operator--(int) noexcept { Iterator c = *this; --index; return c; } Iterator& operator+=(difference_type n) noexcept { index = static_cast(static_cast(index) + n); return *this; } Iterator& operator-=(difference_type n) noexcept { return *this += -n; } friend Iterator operator+(Iterator it, difference_type n) noexcept { return it += n; } friend Iterator operator+(difference_type n, Iterator it) noexcept { return it += n; } friend Iterator operator-(Iterator it, difference_type n) noexcept { return it -= n; } friend difference_type operator-(const Iterator& a, const Iterator& b) noexcept { return static_cast(a.index) - static_cast(b.index); } friend bool operator==(const Iterator& a, const Iterator& b) noexcept { return a.index == b.index; } friend auto operator<=>(const Iterator& a, const Iterator& b) noexcept { return a.index <=> b.index; } private: const TableFile* file = nullptr; std::uint64_t index = 0; }; SlotView() = default; SlotView(const TableFile* file, std::uint64_t firstSlot, std::uint64_t endSlot) noexcept : file(file), first(firstSlot), last(endSlot) {} Iterator begin() const noexcept { return { file, first }; } Iterator end() const noexcept { return { file, last }; } std::size_t size() const noexcept { return static_cast(last - first); } Row operator[](std::uint64_t index) const noexcept { return { file->Slot(first + index), file }; } private: const TableFile* file = nullptr; std::uint64_t first = 0; std::uint64_t last = 0; }; // --- debug formatting --------------------------------------------------- template struct IsBlobRef : std::false_type {}; template struct IsBlobRef> : std::true_type {}; template void FormatValue(std::string& out, const V& value) { if constexpr (std::is_same_v) out += std::format("\"{}\"", value.View()); else if constexpr (std::is_same_v) out += std::format("\"{}\"", value); else if constexpr (std::is_same_v) out += value ? "true" : "false"; else if constexpr (std::is_enum_v) out += std::format("{}", std::to_underlying(value)); else if constexpr (std::is_same_v) out += std::format("{}", std::to_integer(value)); else if constexpr (std::is_arithmetic_v) out += std::format("{}", value); else if constexpr (IsBlobRef::value) out += std::format("<{} elements>", value.Size()); else if constexpr (FixedStringTraits::value) out += std::format("\"{}\"", value.View()); else if constexpr (OptionalTraits::value) { if (value) FormatValue(out, *value); else out += "null"; } else if constexpr (StdArrayTraits::value) { out += "["; for (std::size_t i = 0; i < value.size(); i++) { if (i != 0) out += ", "; FormatValue(out, value[i]); } out += "]"; } else if constexpr (std::formattable) out += std::format("{}", value); else if constexpr (Aggregate) { out += "{"; [&](std::index_sequence) { ((void)[&] { if (I != 0) out += ", "; out += MemberName(); out += "="; FormatValue(out, Member(value)); }(), ...); }(std::make_index_sequence>{}); out += "}"; } else if constexpr (requires { value.size(); value.begin(); }) out += std::format("<{} elements>", value.size()); else out += "?"; } template std::string DescribeRow(Row row) { std::string out(TypeName()); out += "{"; [&](std::index_sequence) { ((void)[&] { if (I != 0) out += ", "; out += MemberName(); out += "="; FormatValue(out, row.template Get()); }(), ...); }(std::make_index_sequence>{}); out += "}"; return out; } } // Neither view owns the mapping, so iterators may outlive the view object. template inline constexpr bool std::ranges::enable_borrowed_range> = true; template inline constexpr bool std::ranges::enable_borrowed_range> = true; template struct std::formatter> : std::formatter { auto format(const Crafter::DB::Row& row, std::format_context& ctx) const { return std::formatter::format(Crafter::DB::DescribeRow(row), ctx); } }; template<> struct std::formatter : std::formatter { auto format(const Crafter::DB::StringRef& ref, std::format_context& ctx) const { return std::formatter::format(ref.View(), ctx); } };