277 lines
12 KiB
Text
277 lines
12 KiB
Text
|
|
// 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);
|
||
|
|
}
|
||
|
|
};
|