Initial commit: Crafter.DB
Header-only C++26 module library for typed, file-backed tables with schema reflection, migrations, queries, and crash-safe commits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
bfd0cc261b
34 changed files with 7506 additions and 0 deletions
277
interfaces/Crafter.DB-Row.cppm
Normal file
277
interfaces/Crafter.DB-Row.cppm
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
// 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);
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue