Crafter.DB/interfaces/Crafter.DB-Row.cppm

277 lines
12 KiB
Text
Raw Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Row<T>: 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<auto Ptr>
struct Column {
using Table = MemberClass<Ptr>;
using Type = MemberTypeOf<Ptr>;
static constexpr auto pointer = Ptr;
static constexpr std::size_t index = MemberIndex<Table, Ptr>();
static constexpr std::string_view Name() noexcept { return MemberName<index, Table>(); }
};
template<TableType T>
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<std::size_t I>
View<MemberType<I, T>> Get() const {
return DecodeMember<T, I>(row, file->HeapView());
}
// Member-pointer access, resolved at compile time: row.Get<&T::iban>().
template<auto Ptr> requires std::is_same_v<MemberClass<Ptr>, T>
View<MemberTypeOf<Ptr>> Get() const {
constexpr std::size_t index = MemberIndex<T, Ptr>();
static_assert(index != noMember, "Crafter.DB: pointer does not designate a direct member of the table struct");
return Get<index>();
}
// Member-pointer access with a run-time pointer: row[&T::iban]. Every
// call site passes a literal, so the lookup folds after inlining.
template<class M>
View<M> operator[](M T::* ptr) const {
const std::size_t index = MemberIndexOf<T>(ptr);
View<M> result{};
[&]<std::size_t... I>(std::index_sequence<I...>) {
((void)[&] {
if constexpr (std::is_same_v<MemberType<I, T>, M>) {
if (index == I) result = Get<I>();
}
}(), ...);
}(std::make_index_sequence<memberCount<T>>{});
return result;
}
template<auto Ptr> requires std::is_same_v<MemberClass<Ptr>, T>
View<MemberTypeOf<Ptr>> operator[](Column<Ptr>) const {
return Get<Ptr>();
}
// Copies every column out into a T (allocates for strings and blobs).
T Materialize() const { return DecodeRow<T>(row, file->HeapView()); }
explicit operator T() const { return Materialize(); }
RowId Id() const noexcept { return static_cast<RowId>((row - file->Rows()) / Layout<T>::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<TableType T>
class LiveIterator {
public:
using value_type = Row<T>;
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<T> operator*() const noexcept { return { current, file }; }
LiveIterator& operator++() noexcept {
current += Layout<T>::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<T>::rowSize;
}
const std::byte* current = nullptr;
const std::byte* last = nullptr;
const TableFile* file = nullptr;
bool checkFlags = false;
};
template<TableType T>
class LiveRange : public std::ranges::view_interface<LiveRange<T>> {
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<T> 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<std::uint64_t>((last - first) / Layout<T>::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<TableType T>
class SlotView : public std::ranges::view_interface<SlotView<T>> {
public:
class Iterator {
public:
using value_type = Row<T>;
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<T> operator*() const noexcept { return { file->Slot(index), file }; }
Row<T> 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<std::uint64_t>(static_cast<difference_type>(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<difference_type>(a.index) - static_cast<difference_type>(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<std::size_t>(last - first); }
Row<T> 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<class V> struct IsBlobRef : std::false_type {};
template<class E> struct IsBlobRef<BlobRef<E>> : std::true_type {};
template<class V>
void FormatValue(std::string& out, const V& value) {
if constexpr (std::is_same_v<V, StringRef>) out += std::format("\"{}\"", value.View());
else if constexpr (std::is_same_v<V, std::string>) out += std::format("\"{}\"", value);
else if constexpr (std::is_same_v<V, bool>) out += value ? "true" : "false";
else if constexpr (std::is_enum_v<V>) out += std::format("{}", std::to_underlying(value));
else if constexpr (std::is_same_v<V, std::byte>) out += std::format("{}", std::to_integer<std::uint32_t>(value));
else if constexpr (std::is_arithmetic_v<V>) out += std::format("{}", value);
else if constexpr (IsBlobRef<V>::value) out += std::format("<{} elements>", value.Size());
else if constexpr (FixedStringTraits<V>::value) out += std::format("\"{}\"", value.View());
else if constexpr (OptionalTraits<V>::value) {
if (value) FormatValue(out, *value);
else out += "null";
}
else if constexpr (StdArrayTraits<V>::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<V, char>) out += std::format("{}", value);
else if constexpr (Aggregate<V>) {
out += "{";
[&]<std::size_t... I>(std::index_sequence<I...>) {
((void)[&] {
if (I != 0) out += ", ";
out += MemberName<I, V>();
out += "=";
FormatValue(out, Member<I>(value));
}(), ...);
}(std::make_index_sequence<memberCount<V>>{});
out += "}";
}
else if constexpr (requires { value.size(); value.begin(); }) out += std::format("<{} elements>", value.size());
else out += "?";
}
template<TableType T>
std::string DescribeRow(Row<T> row) {
std::string out(TypeName<T>());
out += "{";
[&]<std::size_t... I>(std::index_sequence<I...>) {
((void)[&] {
if (I != 0) out += ", ";
out += MemberName<I, T>();
out += "=";
FormatValue(out, row.template Get<I>());
}(), ...);
}(std::make_index_sequence<memberCount<T>>{});
out += "}";
return out;
}
}
// Neither view owns the mapping, so iterators may outlive the view object.
template<class T>
inline constexpr bool std::ranges::enable_borrowed_range<Crafter::DB::SlotView<T>> = true;
template<class T>
inline constexpr bool std::ranges::enable_borrowed_range<Crafter::DB::LiveRange<T>> = true;
template<Crafter::DB::TableType T>
struct std::formatter<Crafter::DB::Row<T>> : std::formatter<std::string> {
auto format(const Crafter::DB::Row<T>& row, std::format_context& ctx) const {
return std::formatter<std::string>::format(Crafter::DB::DescribeRow(row), ctx);
}
};
template<>
struct std::formatter<Crafter::DB::StringRef> : std::formatter<std::string_view> {
auto format(const Crafter::DB::StringRef& ref, std::format_context& ctx) const {
return std::formatter<std::string_view>::format(ref.View(), ctx);
}
};