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>
205 lines
9.3 KiB
C++
205 lines
9.3 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Queries are C++. A lambda over Row<T> is the general form; column tags
|
|
// give a readable expression form (iban == x && amount > 100) whose types
|
|
// the engine can look into: comparisons on the orderedBy column found in a
|
|
// top-level && chain become binary-search bounds instead of a scan, and
|
|
// string comparisons pre-encode their Key once instead of once per row.
|
|
export module Crafter.DB:Query;
|
|
import std;
|
|
import :Reflect;
|
|
import :Formatter;
|
|
import :Schema;
|
|
import :Storage;
|
|
import :Codec;
|
|
import :Row;
|
|
|
|
export namespace Crafter::DB {
|
|
enum class CompareOp : std::uint8_t { Equal, NotEqual, Less, LessEqual, Greater, GreaterEqual };
|
|
|
|
template<auto Ptr, CompareOp Op, class Operand>
|
|
struct Compare {
|
|
using Table = MemberClass<Ptr>;
|
|
static constexpr auto pointer = Ptr;
|
|
static constexpr CompareOp op = Op;
|
|
Operand operand;
|
|
|
|
bool operator()(Row<Table> row) const {
|
|
const auto value = row.template Get<Ptr>();
|
|
if constexpr (Op == CompareOp::Equal) return value == operand;
|
|
else if constexpr (Op == CompareOp::NotEqual) return !(value == operand);
|
|
else if constexpr (Op == CompareOp::Less) return value < operand;
|
|
else if constexpr (Op == CompareOp::LessEqual) return value <= operand;
|
|
else if constexpr (Op == CompareOp::Greater) return value > operand;
|
|
else return value >= operand;
|
|
}
|
|
};
|
|
|
|
template<class A, class B>
|
|
struct And {
|
|
A a;
|
|
B b;
|
|
template<class R> bool operator()(R row) const { return a(row) && b(row); }
|
|
};
|
|
|
|
template<class A, class B>
|
|
struct Or {
|
|
A a;
|
|
B b;
|
|
template<class R> bool operator()(R row) const { return a(row) || b(row); }
|
|
};
|
|
|
|
template<class A>
|
|
struct Not {
|
|
A a;
|
|
template<class R> bool operator()(R row) const { return !a(row); }
|
|
};
|
|
|
|
template<class P> struct IsExpression : std::false_type {};
|
|
template<auto Ptr, CompareOp Op, class V> struct IsExpression<Compare<Ptr, Op, V>> : std::true_type {};
|
|
template<class A, class B> struct IsExpression<And<A, B>> : std::true_type {};
|
|
template<class A, class B> struct IsExpression<Or<A, B>> : std::true_type {};
|
|
template<class A> struct IsExpression<Not<A>> : std::true_type {};
|
|
|
|
template<class P>
|
|
concept Expression = IsExpression<std::remove_cvref_t<P>>::value;
|
|
|
|
template<class M>
|
|
concept StringColumn = std::is_same_v<M, std::string>;
|
|
|
|
template<auto Ptr, CompareOp Op, class V>
|
|
auto MakeCompare(V&& value) {
|
|
using M = MemberTypeOf<Ptr>;
|
|
if constexpr (StringColumn<M>) {
|
|
if constexpr (Op == CompareOp::Equal || Op == CompareOp::NotEqual) return Compare<Ptr, Op, Key>{ Key(std::string_view(value)) };
|
|
else return Compare<Ptr, Op, std::string>{ std::string(std::string_view(value)) };
|
|
} else {
|
|
return Compare<Ptr, Op, M>{ static_cast<M>(std::forward<V>(value)) };
|
|
}
|
|
}
|
|
|
|
template<auto Ptr, class V> auto operator==(Column<Ptr>, V&& value) { return MakeCompare<Ptr, CompareOp::Equal>(std::forward<V>(value)); }
|
|
template<auto Ptr, class V> auto operator!=(Column<Ptr>, V&& value) { return MakeCompare<Ptr, CompareOp::NotEqual>(std::forward<V>(value)); }
|
|
template<auto Ptr, class V> auto operator<(Column<Ptr>, V&& value) { return MakeCompare<Ptr, CompareOp::Less>(std::forward<V>(value)); }
|
|
template<auto Ptr, class V> auto operator<=(Column<Ptr>, V&& value) { return MakeCompare<Ptr, CompareOp::LessEqual>(std::forward<V>(value)); }
|
|
template<auto Ptr, class V> auto operator>(Column<Ptr>, V&& value) { return MakeCompare<Ptr, CompareOp::Greater>(std::forward<V>(value)); }
|
|
template<auto Ptr, class V> auto operator>=(Column<Ptr>, V&& value) { return MakeCompare<Ptr, CompareOp::GreaterEqual>(std::forward<V>(value)); }
|
|
|
|
template<Expression A, Expression B> auto operator&&(A a, B b) { return And<A, B>{ std::move(a), std::move(b) }; }
|
|
template<Expression A, Expression B> auto operator||(A a, B b) { return Or<A, B>{ std::move(a), std::move(b) }; }
|
|
template<Expression A> auto operator!(A a) { return Not<A>{ std::move(a) }; }
|
|
|
|
// Bounds on the ordered column harvested from a predicate's top-level
|
|
// conjunction. Anything under || or ! contributes nothing (the whole
|
|
// predicate still runs as the filter, so this is only ever a narrowing).
|
|
template<class K>
|
|
struct Bounds {
|
|
std::optional<K> lower;
|
|
bool lowerExclusive = false;
|
|
std::optional<K> upper;
|
|
bool upperExclusive = false;
|
|
|
|
void Lower(const K& key, bool exclusive) {
|
|
if (!lower || *lower < key || (*lower == key && exclusive)) {
|
|
lower = key;
|
|
lowerExclusive = exclusive;
|
|
}
|
|
}
|
|
|
|
void Upper(const K& key, bool exclusive) {
|
|
if (!upper || key < *upper || (*upper == key && exclusive)) {
|
|
upper = key;
|
|
upperExclusive = exclusive;
|
|
}
|
|
}
|
|
};
|
|
|
|
template<class P> struct IsAndType : std::false_type {};
|
|
template<class A, class B> struct IsAndType<And<A, B>> : std::true_type {};
|
|
template<class P> struct IsCompareType : std::false_type {};
|
|
template<auto Ptr, CompareOp Op, class V> struct IsCompareType<Compare<Ptr, Op, V>> : std::true_type {};
|
|
|
|
template<class T, class P> requires HasOrderedBy<T>
|
|
void CollectBounds(const P& predicate, Bounds<OrderedKey<T>>& bounds) {
|
|
if constexpr (IsAndType<P>::value) {
|
|
CollectBounds<T>(predicate.a, bounds);
|
|
CollectBounds<T>(predicate.b, bounds);
|
|
} else if constexpr (IsCompareType<P>::value) {
|
|
if constexpr (SameMemberPointer<P::pointer, T::orderedBy>()) {
|
|
const OrderedKey<T> key = static_cast<OrderedKey<T>>(predicate.operand);
|
|
switch (P::op) {
|
|
case CompareOp::Equal: bounds.Lower(key, false); bounds.Upper(key, false); break;
|
|
case CompareOp::Less: bounds.Upper(key, true); break;
|
|
case CompareOp::LessEqual: bounds.Upper(key, false); break;
|
|
case CompareOp::Greater: bounds.Lower(key, true); break;
|
|
case CompareOp::GreaterEqual: bounds.Lower(key, false); break;
|
|
case CompareOp::NotEqual: break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// A lazy range of Row<T> with the conveniences a result needs. Chainable:
|
|
// table.Range<...>(a, b).Where(pred).Select<&T::amount>().
|
|
template<TableType T, std::ranges::view R> requires std::is_same_v<std::ranges::range_value_t<R>, Row<T>>
|
|
class Query : public std::ranges::view_interface<Query<T, R>> {
|
|
public:
|
|
Query() = default;
|
|
explicit Query(R range) : range(std::move(range)) {}
|
|
|
|
// A Query is iterable as a const object whenever its underlying view
|
|
// is (LiveRange is; std::ranges::filter_view, by the standard's
|
|
// design, is not).
|
|
template<class Self> requires std::ranges::range<std::conditional_t<std::is_const_v<std::remove_reference_t<Self>>, const R, R>>
|
|
auto begin(this Self&& self) { return std::ranges::begin(self.range); }
|
|
|
|
template<class Self> requires std::ranges::range<std::conditional_t<std::is_const_v<std::remove_reference_t<Self>>, const R, R>>
|
|
auto end(this Self&& self) { return std::ranges::end(self.range); }
|
|
|
|
template<class Self> requires std::ranges::range<std::conditional_t<std::is_const_v<std::remove_reference_t<Self>>, const R, R>>
|
|
std::vector<T> Materialize(this Self&& self) {
|
|
std::vector<T> out;
|
|
for (Row<T> row : self.range) out.push_back(row.Materialize());
|
|
return out;
|
|
}
|
|
|
|
template<class Self> requires std::ranges::range<std::conditional_t<std::is_const_v<std::remove_reference_t<Self>>, const R, R>>
|
|
std::uint64_t Count(this Self&& self) {
|
|
std::uint64_t n = 0;
|
|
for ([[maybe_unused]] Row<T> row : self.range) n++;
|
|
return n;
|
|
}
|
|
|
|
template<class Self> requires std::ranges::range<std::conditional_t<std::is_const_v<std::remove_reference_t<Self>>, const R, R>>
|
|
std::optional<Row<T>> First(this Self&& self) {
|
|
for (Row<T> row : self.range) return row;
|
|
return std::nullopt;
|
|
}
|
|
|
|
template<class Self> requires std::ranges::range<std::conditional_t<std::is_const_v<std::remove_reference_t<Self>>, const R, R>>
|
|
bool Any(this Self&& self) { return self.First().has_value(); }
|
|
|
|
template<class Pred>
|
|
auto Where(Pred pred) && {
|
|
return Query<T, std::ranges::filter_view<R, Pred>>(std::ranges::filter_view<R, Pred>(std::move(range), std::move(pred)));
|
|
}
|
|
|
|
template<class Pred>
|
|
auto Where(Pred pred) const& {
|
|
return Query<T, std::ranges::filter_view<R, Pred>>(std::ranges::filter_view<R, Pred>(range, std::move(pred)));
|
|
}
|
|
|
|
// Lazy view of one column's values (views for strings, values otherwise).
|
|
template<auto Ptr> requires std::is_same_v<MemberClass<Ptr>, T>
|
|
auto Select() && {
|
|
return std::views::transform(std::move(range), [](Row<T> row) { return row.template Get<Ptr>(); });
|
|
}
|
|
|
|
const R& Range() const noexcept { return range; }
|
|
|
|
private:
|
|
R range;
|
|
};
|
|
}
|